java -jar myapplication-fat.jar
と、JVMはアプリケーションのパフォーマンスを最適化する
java -jar myapplication-fat.jar
にいくつかのパラメータを個別に設定します。
![](https://habrastorage.org/files/fe8/813/ea3/fe8813ea38464784a44159a19ab68111.png)
この記事では、開発者がJavaアプリケーションをLinuxコンテナーにパックする前に知っておくべきことについて説明します。
仮想プロセッサの形式とメモリの量を設定できる仮想マシンの形のコンテナを検討します。 コンテナは、特定のプロセスに割り当てられたリソース(プロセッサ、メモリ、ファイルシステム、ネットワークなど)が他のプロセスから分離される分離メカニズムに似ています。 この分離は、Linux cgroupsカーネルメカニズムのおかげで可能です。
作業時にランタイムから取得したデータに依存する一部のアプリケーションは、cgroupが現れる前に作成されたことに注意する必要があります。
top
、
free
、
ps
、さらにはJVMのようなユーティリティは、コンテナ内での実行に最適化されていません。実際、非常に限られたLinuxプロセスです。 プログラムがコンテナ内の作業の機能を考慮しない場合に何が起こるかを見て、エラーを回避する方法を見つけましょう。
問題文
デモンストレーションのために、次のコマンドを使用して、1 GBのRAMを持つ仮想マシンにdockerデーモンを作成しました。
docker-machine create -d virtualbox –virtualbox-memory '1024' docker1024
次に、
-m
および
--memory-swap
指定された100 MBの制限を使用して、コンテナーで実行されている3つの異なるLinuxディストリビューションで
free -h
コマンドを実行しました。 その結果、それらはすべて995 MBの合計メモリ容量を示しました。
![](https://habrastorage.org/getpro/habr/post_images/951/7a4/262/9517a42621a2e8a7ee43e59a1ee1848b.png)
無料の-hコマンドの結果
Kubernetes / OpenShiftクラスターでも同様の結果が得られます。 次のコマンドを使用して、メモリが制限されたKubernetesコンテナーグループを実行しました。
kubectl run mycentos –image=centos -it –limits='memory=512Mi'
この場合、クラスターには15 GBのメモリが割り当てられました。 その結果、システムによって報告されるメモリの総量は14 GBでした。
![](https://habrastorage.org/getpro/habr/post_images/3c0/b4b/766/3c0b4b766933c6b54259d6abd1943db8.png)
15 GBのメモリを備えたクラスターを調べる
何が起こっているのかの理由を理解するために、Linuxコンテナでメモリを操作する機能に関するこの資料を読むことをお勧めします。
Dockerキー(
-m,
--memory
および
--memory-swap
)、およびKubernetes(--
--limits)
キーは、指定された制限を超えようとするとLinuxカーネルにプロセスを停止するように指示することを理解する必要があります。 ただし、JVMはこれについて何も認識しておらず、このような制限を超えると、良いことは期待できません。
指定されたメモリ制限を超えた後、システムがプロセスを停止する状況を再現するために、次のコマンドを使用して、メモリ制限が50 MBのコンテナでWildFly Application Serverを起動できます 。
docker run -it –name mywildfly -m=50m jboss/wildfly
これで、コンテナの実行中に、制限を確認するために
docker stats
実行できます。
![](https://habrastorage.org/getpro/habr/post_images/260/400/32d/26040032dfffb311d00d7a93537f27d6.png)
コンテナ情報
数秒後、WildFlyコンテナの実行が中断され、メッセージが表示されます。
*** JBossAS process (55) received KILL signal ***
次のコマンドを実行します。
docker inspect mywildfly -f '{{json .State}}
彼女は、コンテナがOOM(メモリ不足、メモリ不足)状況のために停止したことを報告します。 コンテナの状態は
OOMKilled=true
であることに注意してください。
![](https://habrastorage.org/getpro/habr/post_images/4e3/1ef/2ab/4e31ef2ab284f103c85cf1d22963364c.png)
コンテナを停止する理由の分析
Javaアプリケーションでの不適切なメモリ処理の影響
1 GBのメモリ(以前は
docker-machine create -d virtualbox –virtualbox-memory '1024' docker1024
によって作成された)を搭載したマシンで実行されるDockerデーモンでは、コンテナメモリは150 MBに制限され、 Springアプリケーションには十分と思われるブートすると 、Javaアプリケーションがパラメーター
XX:+PrintFlagsFinal
および
-XX:+PrintGCDetails
Dockerfileで指定された
-XX:+PrintGCDetails
されます。 これにより、JVMエルゴノミクスメカニズムの初期パラメーターを読み取り、ガベージコレクションの開始(GC、ガベージコレクション)の詳細を確認できます。
これを試してみましょう:
$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m 150M rafabene/java-container:openjdk
/api/memory/
にエンドポイントを準備しました。これは、文字列オブジェクトをJVMメモリにロードして、大量のメモリを消費する操作をシミュレートします。 この呼び出しを行いましょう:
$ curl http://`docker-machine ip docker1024`:8080/api/memory
エンドポイントはおよそ次のように応答します。
Allocated more than 80% (219.8 MiB) of the max allowed JVM memory size (241.7 MiB)
これはすべて、少なくとも2つの質問につながる可能性があります。
- 最大許容JVMメモリサイズが241.7 MiBに等しいのはなぜですか?
- コンテナのメモリ制限が150 MBの場合、Javaで220 MB近くを割り当てることができたのはなぜですか?
これに対処するには、最初にJVMエルゴノミクスのドキュメントで最大ヒープサイズについて述べられていることを覚えておく必要があります 。 最大ヒープサイズは物理メモリのサイズの1/4です。 JVMはコンテナで何が実行されているかを知らないため、最大ヒープサイズは260 MBに近くなります。 コンテナの初期化時に
-XX:+PrintFlagsFinal
を追加した場合、この値を確認できます。
$ docker logs mycontainer150|grep -i MaxHeapSize uintx MaxHeapSize := 262144000 {product}
ここで、Dockerコマンドラインで
m 150M
パラメーターを使用すると、Dockerデーモンがメモリとスワップファイルのサイズを150メガバイトに制限することを理解する必要があります。 その結果、プロセスは300メガバイトを割り当てることができます。これは、プロセスがLinuxカーネルからKILLシグナルを受信しなかった理由を説明しています。
Dockerコマンドラインで 、メモリ制限パラメーター(
--memory
)とスワップファイル(
--swap
)のさまざまな組み合わせの機能については、 こちらをご覧ください 。
問題に対する誤った解決策の例としてのメモリ容量の増加
何が起きているのか本質を理解していない開発者は、上記の問題は環境がJVMを実行するのに十分なメモリを提供しないことであると信じがちです。 その結果、この問題に対する頻繁な解決策は、使用可能なメモリの量を増やすことですが、実際には、このアプローチは状況を悪化させるだけです。
デーモンに1 GBのメモリを提供するのではなく、8 GBを提供したとします。 作成するには、次のコマンドが適しています。
docker-machine create -d virtualbox –virtualbox-memory '8192' docker8192
同じ考えに従って、コンテナの制限を弱め、150ではなく800 MBのメモリを提供します。
$ docker run -it --name mycontainer -p 8080:8080 -m 800M rafabene/java-container:openjdk
curl http://`docker-machine ip docker8192`:8080/api/memory
コマンド
curl http://`docker-machine ip docker8192`:8080/api/memory
は、8 GBのメモリーを備えた環境でJVMの計算された
MaxHeapSize
パラメーターが2092957696バイトになるため、このような条件下でも実行できないことに注意してください。 (約2 GB)。 次のコマンドでこれを確認できます。
docker logs mycontainer|grep -i MaxHeapSize
![](https://habrastorage.org/getpro/habr/post_images/b88/cd4/aaa/b88cd4aaa431b04be4ee215b25b7fcb6.png)
MaxHeapSizeの確認
アプリケーションは、コンテナの制限(800 MBのRAMとスワップファイル内の同じ量)を超える1.6 GBを超えるメモリを割り当てようとします。その結果、プロセスは停止します。
メモリ量を増やし、JVMが独自のパラメータを設定できるようにすることは、コンテナでアプリケーションを実行するときに常に正しいとはほど遠いことは明らかです。 Javaアプリケーションをコンテナで実行する場合、アプリケーションのニーズとコンテナの制限に基づいて、(-
--Xmx
パラメータを使用して)自分で最大ヒープサイズを設定する必要があります。
問題の正しい解決策
Dockerfileの小さな変更により、JVMの追加パラメーターを定義する環境変数を設定できます。 次の行を見てください。
CMD java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar java-container.jar
これで、
JAVA_OPTIONS
環境変数を使用して、システムにJVMヒープのサイズを伝えることができます。 このアプリケーションには十分な300 MBがあるようです。 後で、ログを見て、そこに314572800バイト(300 MiB )の値を見つけることができます。
-e
スイッチを使用してDockerの環境変数を設定できます。
$ docker run -d --name mycontainer8g -p 8080:8080 -m 800M -e JAVA_OPTIONS='-Xmx300m' rafabene/java-container:openjdk-env $ docker logs mycontainer8g|grep -i MaxHeapSize uintx MaxHeapSize := 314572800 {product}
Kubernetesでは、環境変数は
–env=[key=value]
スイッチを使用して設定できます。
$ kubectl run mycontainer --image=rafabene/java-container:openjdk-env --limits='memory=800Mi' --env="JAVA_OPTIONS='-Xmx300m'" $ kubectl get pods NAME READY STATUS RESTARTS AGE mycontainer-2141389741-b1u0o 1/1 Running 0 6s $ kubectl logs mycontainer-2141389741-b1u0o|grep MaxHeapSize uintx MaxHeapSize := 314572800 {product}
問題の正しい解決策を改善する
コンテナの制限に基づいてヒープサイズを自動的に計算できるとしたらどうでしょうか?
これは、 Fabric8コミュニティによって準備された基本的なDockerイメージを使用する場合に実現可能です。 fabric8 / java-jboss-openjdk8-jdkイメージは、コンテナの制限を検出し、利用可能なメモリの50%を上限として使用するスクリプトを使用します 。 50%の代わりに、別の値を使用できることに注意してください。 さらに、このイメージを使用すると、デバッグ、診断などを有効または無効にできます。 SpringブートアプリケーションのDockerfileがどのように見えるかを見てください。
FROM fabric8/java-jboss-openjdk8-jdk:1.2.3 ENV JAVA_APP_JAR java-container.jar ENV AB_OFF true EXPOSE 8080 ADD target/$JAVA_APP_JAR /deployments/
これですべてが正常に機能するようになります。 コンテナのメモリ制限に関係なく、Javaアプリケーションは、デーモンのパラメータに基づいてではなく、コンテナのパラメータに応じて常にヒープサイズを調整します。
![](https://habrastorage.org/getpro/habr/post_images/c3c/a0e/0e6/c3ca0e0e6880312c1b21637e541267e4.png)
![](https://habrastorage.org/getpro/habr/post_images/c90/26b/05e/c9026b05e657f3b23d7028c3e601b10f.png)
![](https://habrastorage.org/getpro/habr/post_images/c3c/a0e/0e6/c3ca0e0e6880312c1b21637e541267e4.png)
Fabric8開発の使用
まとめ
JVMには、コンテナ化された環境で実行することを決定し、メモリやプロセッサなどの一部のリソースの制限を考慮する手段がまだありません。 したがって、JVMエルゴノミクスメカニズムに最大ヒープサイズを単独で設定させることはできません。
この問題を解決する1つの方法は、Fabric8 Baseイメージを使用することです。これにより、システムはコンテナーのパラメーターに基づいてヒープサイズを自動的に調整できます。 このパラメーターは個別に設定できますが、自動化されたアプローチの方が便利です。
JDK9には、コンテナ(たとえばDocker)のcgroupsメモリ制限に対する実験的なJVMサポートが含まれています。 ここで詳細を見つけることができます。
ここで、JVMとメモリ使用の機能について説明したことに注意してください。 プロセッサは別のトピックであるため、さらに詳しく説明します。
親愛なる読者! LinuxコンテナでJavaアプリケーションを操作するときに問題が発生しましたか? 出会ったら、どう対処したか教えてください。