この広告は、90日以上更新していないブログに表示しています。
JVMにチューニング項目は多々あれど、プロダクションで運用する際に予めおさえておきたい項目をまとめてみるエントリです。*1勿論、OSもJVMもデフォルトである程度のパフォーマンスは発揮でき、計測を伴わないチューニングは悪手であることはよく知られています。しかし、設定しておかないとパフォーマンスにそのまま影響すると分かるものを調べないのは裸で戦場に赴くようなものです。*2どんな項目をどう変更すれば良いのか知っていることは重要な武器なのです。
今回、チューニングポイントを調べるにあたって、私のモチベーションはどこにあるのかを考えると、以下の要件を満たしたいということがあげられます。
ここでいう品質水準・異常とは、パフォーマンスが明らかに低い、アプリケーションがクラッシュする、などの(JVM・アプリケーション双方の)期待しない振る舞いを指します。
やみくもにチューニングしても終わりがなく、何をチューニングすれば良いかも判断しづらいものです。性能目標を定めることで、このラインをクリアし続けることを目指す方が健全です。例えば以下のような項目があげられるかと思います。
だいたいのことはJavaパフォーマンスに書いてあるので、これを読もう。
なお、今回の主な関心範囲はウェブアプリケーション(JVMおよびそのうえで動くアプリケーション)を想定しています。*3代表的なものをあげつつ、これは最低限チェックしたいというものに ★ を付けています。
JVMの前に土台についても少し考えます。ホストのリソースを十分解放しないと、当然JVMもパフォーマンスを発揮できません。昨今ではDockerなどのコンテナ技術も当たり前になりつつあり、ホストだけでなくコンテナのチューニングも必要になっているように思います。
ulimitfs.file-maxメモリのページサイズが大きくなれば、それだけ少ない回数でI/Oができるため高速化につながります。JVM以外にもシステム上のすべてのプログラムが使用するようになる点に注意が必要です。JVM側へのオプションの指定は不要(-XX:+UseLargePagesは別のものなので無効のままにしておく)です。
# cat /sys/kernel/mm/transparent_hugepage/enabledalways [madvise] never# echo always > /sys/kernel/mm/transparent_hugepage/enabled# cat /sys/kernel/mm/transparent_hugepage/enabled[always] madvise never
コンテナオーケストレーションのためにKubernetesを使うケースも多くなってきました。アプリケーションはPodの上のコンテナ単位で動作しますが、このコンテナにリソースのrequest/limitを指定できます。明示しなくても動作させられますが、指定した方がアプリ側の見積もりもしやすいかなと思っています。このlimitを元にアプリケーション(JVM)の設定を決めていけば良いでしょう。*4
1 cpu = 1 vCPUいずれも、リソースを指定しない場合、あるだけ使おうとするため、複数のPodを一つのNodeに載せたときの動作が不安定となる恐れがあります。*5大きい数を指定してもNodeのリソースが足りないと起動すらできません。
see also:
JDK 10からコンテナサポートが強化されます。-XX:-UseContainerSupport がデフォルトで有効となり、JDK 10からはDockerの設定*6から値を取得するようになるようです。
DockerではいくつかのCPU設定が可能ですが、JVMはmin(cpuset-cpus, cpu-shares/1024, cpus) という計算によりactive_processor_count を決定するようです。端数の場合は切り上げされて整数になります。cpusはcpu_quota / cpu_period によって求められる値です。この値は-XX:ActiveProcessorCount=N によって上書きすることができます。
メモリはJDK 10からFraction(1/N)による指定ではなくPercentage(%)による指定ができるようになります。使用可能なホストメモリの割合を指定します。
Kubernetes上で動作する場合、Kubernetesのドキュメントによると、次のようにコンテナにパラメータが渡されるとあります。
spec.containers[].resources.requests.cpu はコア値に変換し1024を掛けた値が--cpu-shares として渡されるspec.containers[].resources.limits.cpu はミリコア値に変換し100を掛けた値が--cpu-quota として渡される*7spec.containers[].resources.limits.memory はそのまま--memory として渡されるたとえば、request.cpuとlimits.cpuの両方を1とした場合は、cpu-shares: 1 * 1024 / 1024 = 1cpu とcpu-quota: 1000m * 100 / 100ms = 1cpu/ms となるのでJVMにはCPU 1としてセットされます。つまり、Kubernetesの定義する1cpuは、そのままJVMから見るCPU数となります。ですので、両者の値に差があるとき、JDK 10では前述の計算式で最小値がCPUとなると思われます。※未検証
see also:
書籍「Javaパフォーマンス(オライリー)」は、一般に必要になることを網羅しており、とりあえずコレを読んでおけばだいたいのケースでは十分でしょうと思える一冊です。
-server -XX:+TieredCompilation ★-XX:ReservedCodeCacheSize=N-XX:-UseCodeCacheFlushingReservedCodeCacheSizeと併せて指定する。-XX:CompileThreshold=N-XX:+DoEscapeAnalysis-Xms<N> -Xmx<N>-Xms と-Xmx は同じ値にしたほうが、拡張/シュリンク時のコストが発生しない。-XX:+UseCGroupMemoryLimitForHeap というものもある-XX:InitialRAMFraction などはdeprecatedとなり-XX:InitialRAMPercentage が設定できるようになる。-XX:MaxRAMPercentage を使うことでコンテナのメモリ量1/2以上のヒープを割り当てることが可能-XX:NewRatio=N-XX:+UseSerialGC or-XX:+UseParallelGC or-XX:+UseConcMarkSweepGC -XX:+UseParNewGC or-XX:+UseG1GC ★-XX:InitiatingHeapOccupancyPercent 大きなヒープを前提に作られているのでヒープサイズが小さい場合はこのオプションで調整する。-XX:+HeapDumpOnOutOfMemoryError ★-XX:+CrashOnOutOfMemoryError or-XX:+ExitOnOutOfMemoryError ★-XX:OnOutOfMemoryError='bin/kill -ABRT %p'-XX:HeapDumpPath=<path>-XX:+HeapDumpBeforeFullGCCMSGCはJava9で非推奨となっているため、現実的にはスループット型GCまたはG1GCのいずれを選択するかになるように思います。どちらを選択した方が良いかは、CPUの余力とヒープメモリのサイズによって変わります。両方が十分に大きい(多く必要とする)のであればG1GCの方が基本的に適しています。フルGCに伴う停止を極力なくしたい場合はCPUとメモリを積んでG1GCを選択しましょう。
see also:HotSpot Virtual Machineガベージ・コレクション・チューニング・ガイド
-XX:MaxGCPauseMillis=N-XX:GCTimeRatio=N ★最大一時停止時間目標→スループット目標→最少フットプリント目標(-Xmx)の順に達成するよう動作します。JVMはadaptive sizingがデフォルトで有効で、指定された目標値になるように自動的にメモリの拡張など調整を行います。そのため、必要なヒープサイズが予め分かる場合には-Xms -Xmxを同値に指定するとGCが最小限に留められるためパフォーマンスにプラスとなるようです。しかし、GC時間以上にスループット(Ops)の目標を十分に満たすことが重要であり、適切なサイズのヒープ(-Xmx)とGCTimeRatioを指定してJVMに委ねても手作業でチューニングする場合と同等のスループットを達成できることがある点には留意が必要です。(上記Oracleのガイドでもデフォルトより必要なヒープサイズが大きい場合を除きXmxを指定しないことを勧めている。)
-XX:MaxGCPauseMillis=N ★-XX:ParallelGCThreads=N(8 + (CPU数 - 8) * (5 / 8)))だが、スレッドが多すぎても効果が薄くなるため小さくする。-XX:ConcGCThreads=N(ParallelGCThreads + 2) / 4-XX:InitiatingHeapOccupancyPercent=NまずはMaxGCPauseMillisのチューニングから始め、それでもフルGCが発生する場合に他のチューニングを行った方が良いでしょう。
see also:Understanding Play thread pools
maximumPoolSize ★pool size = 最大スレッド数 * (最大同時接続数 - 1) + 1connectionInitSql ★transactionIsolation = TRANSACTION_READ_COMMITTEDsee also:HikariCP設定
ステートメントキャッシュなどはドライバの設定で調整します。Aurora(MySQL)への接続にMariaDB Connector/Jを使用しており、ここではそのドライバの設定項目を検討しています。*13
useServerPrepStmtsserverTimezonejdbc:mysql:aurora://<hostDescription>[,<hostDescription>...]/useBatchMultiSendチューニングには計測がセットでなければいけないことは周知の通りです。例えば、以下のような方法が考えられます。
-XX:+PrintCompilation-verbose:gc (=-XX:+PrintGC)-Xloggc:<path>-XX:+PrintGCDetails-XX:+PrintGCTimeStamps or-XX:+PrintGCDateStamps-XX:+PrintReferenceGC-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=N -XX:GCLogFileSize=N-XX:+PrintTLABnetstat -anTIME_WAITが大量発生しているとエフェメラルポートが不足している。ここまで見てきたように、最初に検討すべき項目は少なく、基本的にJVMは優秀です。パフォーマンスに影響する多くのケースではアプリケーションコードが原因であることがほとんどであるように思います。チューニングポイントを知るだけでなく、内部動作(スレッドモデルやメモリモデル、GCの仕組みなど)を知ることも不可欠です。
また、制約理論によれば局所最適化の罠などもあります。システム全体のボトルネックを探すことが最重要ですが、パフォーマンス劣化に直面した際はパニックにもなるものです。予めどのようなポイントがあるのか知ることができるのなら、備えるにこしたことはありません。アプリケーションの性格によって最適解は異なりますが、最初に考える範囲は似通うのかなと思います。
更新履歴
-XX:+CrashOnOutOfMemoryError or-XX:+ExitOnOutOfMemoryError を追記*1:何番煎じのエントリだという感じですが、自分でまとめる方が学習になるので…
*2:早まった最適化は悪だが、最適化を何もせずに本番にあてろということではないと思う
*3:より具体的にはScala + Play framework、OpenJDK 8〜10
*4:Storageはほぼ使わないので、ここでは考慮外としています
*5:特にCPUのバーストやメモリが溢れた時は巻き添えをくらうことがコワイ
*6:cgroup filesystem?
*7:ドキュメントにquota引数とは書いていないのですがこの質問とDocker公式ドキュメントおよびZalandoの公開情報 からquotaと推測しました
*8:default period
*9:滅多になさそう
*10:エスケープ分析(逸出分析)とはローカルオブジェクトの参照がヒープに公開されていない(=実質、ロックが不要)箇所を検出して、ランタイムコンパイラがこれをインライン化することで最適化すること。ロック省略ともいう。
*11:色々な都合の結果JSTを指定せざるを得ないケースはある…
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。