JVMアプリケーションを運用する際のメジャーどころチューニングポイントメモ
JVMにチューニング項目は多々あれど、プロダクションで運用する際に予めおさえておきたい項目をまとめてみるエントリです。*1 勿論、OSもJVMもデフォルトである程度のパフォーマンスは発揮でき、計測を伴わないチューニングは悪手であることはよく知られています。 しかし、設定しておかないとパフォーマンスにそのまま影響すると分かるものを調べないのは裸で戦場に赴くようなものです。*2 どんな項目をどう変更すれば良いのか知っていることは重要な武器なのです。
なぜ調べるのか
今回、チューニングポイントを調べるにあたって、私のモチベーションはどこにあるのかを考えると、以下の要件を満たしたいということがあげられます。
- アプリケーションとして求められる品質水準として動作する → 性能目標
- 異常時に事象を追うことができる
ここでいう品質水準・異常とは、パフォーマンスが明らかに低い、アプリケーションがクラッシュする、などの(JVM・アプリケーション双方の)期待しない振る舞いを指します。
性能目標
やみくもにチューニングしても終わりがなく、何をチューニングすれば良いかも判断しづらいものです。 性能目標を定めることで、このラインをクリアし続けることを目指す方が健全です。 例えば以下のような項目があげられるかと思います。
TL;DR
だいたいのことはJavaパフォーマンスに書いてあるので、これを読もう。
なお、今回の主な関心範囲はウェブアプリケーション(JVMおよびそのうえで動くアプリケーション)を想定しています。*3 代表的なものをあげつつ、これは最低限チェックしたいというものに ★ を付けています。
Kernel
JVMの前に土台についても少し考えます。 ホストのリソースを十分解放しないと、当然JVMもパフォーマンスを発揮できません。 昨今ではDockerなどのコンテナ技術も当たり前になりつつあり、ホストだけでなくコンテナのチューニングも必要になっているように思います。
- ファイルディスクリプタ
ulimit
fs.file-max
- fluentdを使用するケースでは強く推奨されている
- TCP
- large page(huge page)
- OSでのメモリの管理単位
- Linux 2.6.32以降ではtransparent huge pageが導入されている。
- アプリケーションが目に見えて高速化する(らしい)。
transparent huge page
メモリのページサイズが大きくなれば、それだけ少ない回数でI/Oができるため高速化につながります。
JVM以外にもシステム上のすべてのプログラムが使用するようになる点に注意が必要です。
JVM側へのオプションの指定は不要(-XX:+UseLargePages
は別のものなので無効のままにしておく)です。
# cat /sys/kernel/mm/transparent_hugepage/enabled always [madvise] never # echo always > /sys/kernel/mm/transparent_hugepage/enabled # cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never
Kubernetes上で動く際のリソース制限
コンテナオーケストレーションのためにKubernetesを使うケースも多くなってきました。 アプリケーションはPodの上のコンテナ単位で動作しますが、このコンテナにリソースのrequest/limitを指定できます。明示しなくても動作させられますが、指定した方がアプリ側の見積もりもしやすいかなと思っています。 このlimitを元にアプリケーション(JVM)の設定を決めていけば良いでしょう。*4
- CPUは絶対値指定
- いくつCPUを使うかを指定
- milli単位で指定可能
- NodeにいくつvCPUがあっても絶対値指定
1 cpu = 1 vCPU
- Memoryも絶対値指定
- 基本はメビバイト(MiB)で指定するのが楽そう
いずれも、リソースを指定しない場合、あるだけ使おうとするため、複数のPodを一つのNodeに載せたときの動作が不安定となる恐れがあります。*5 大きい数を指定してもNodeのリソースが足りないと起動すらできません。
see also:
- Managing Compute Resources for Containers
- Assign Memory Resources to Containers and Pods
- Assign CPU Resources to Containers and Pods
- Understanding Kubernetes Resources
- Kubernetes アンチパターン
JDK 10のコンテナサポート
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
として渡される*7- コンテナが100ms*8に使用できるCPU時間の合計量
spec.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:
- [JDK-8189497] Improve docker container detection and resource configuration usage - Java Bug System
- [JDK-8146115] Improve docker container detection and resource configuration usage - Java Bug System
- [JDK-8186315] Allow more flexibility in selecting Heap % of available RAM - Java Bug System
- New thing in JDK10 even that scala-er should know
- Better containerized JVMs in JDK 10
- Kubernetes on AWS - Container resource limits
- JVMのヒープサイズとコンテナ時代のチューニング
JVM
書籍「Javaパフォーマンス(オライリー)」は、一般に必要になることを網羅しており、とりあえずコレを読んでおけばだいたいのケースでは十分でしょうと思える一冊です。
JIT
-server -XX:+TieredCompilation
★-XX:ReservedCodeCacheSize=N
-XX:-UseCodeCacheFlushing
- CodeCacheが限界に近づくとコンパイルしたコードを捨てる
- デフォルト有効
- JMXなどを通してキャッシュサイズを見ながら、必要なコードキャッシュサイズが分かれば
ReservedCodeCacheSize
と併せて指定する。 - Javaの謎のパフォーマンス劣化現象との戦い
-XX:CompileThreshold=N
-XX:+DoEscapeAnalysis
Memory/GC
-Xms<N> -Xmx<N>
- ヒープサイズ
- 物理メモリを超えてはいけない。(スワッピングが発生するとパフォーマンスが悪化する)
-Xms
と-Xmx
は同じ値にしたほうが、拡張/シュリンク時のコストが発生しない。- Linux 64bit serverのデフォルトは、Xmsが512MBと物理メモリの1/64のうち小さい方、Xmxが32GBと物理メモリの1/4のうち小さい方
- JDK 8u131〜9ではexperimentalなオプションとして
-XX:+UseCGroupMemoryLimitForHeap
というものもある - JDK 10からコンテナのサポートが強化される
-XX:InitialRAMFraction
などはdeprecatedとなり-XX:InitialRAMPercentage
が設定できるようになる。- see also: [JDK-8196595] Release Note: Java Improvements for Docker Containers - Java Bug System
-XX:MaxRAMPercentage
を使うことでコンテナのメモリ量1/2以上のヒープを割り当てることが可能
-XX:NewRatio=N
- young領域に対するold領域の比率
- デフォルトは2(young領域は33%)
-XX:+UseSerialGC
or-XX:+UseParallelGC
or-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
or-XX:+UseG1GC
★- アプリケーションの性質により適切なGCを選択する必要がある。
- Java8デフォルトはスループット型GC(ParallelGC)
- SerialGCとParallelGCではSTW(Stop the world)がほぼ避けられない。
- CMS GCではアプリケーションスレッドと並行してヒープを探索するためCPUにそれなりの余裕が必要で、コンパクト化を行わないためヒープが断片化したままとなる。バックグラウンドスレッドのCPUリソースが確保できない場合や断片化によりオブジェクトの割当ができなくなると、シリアル型と同じくすべてのアプリケーションスレッドを停止して、単一のスレッドでGCが行われる。
参考:Concurrent Mark-Sweep Garbage Collection - G1 GCではヒープをリージョンに分割して管理し、CMSと同じくコンカレント型であり、ほとんどの場合でアプリケーションスレッドは停止しない。オブジェクトをリージョン移動させることでヒープの断片化も起きにくいが、CPU時間は多く消費されるようになる。4GB以下のヒープサイズの場合はCMSの方が基本的に高速。
参考:Garbage First Garbage Collection- Java9で標準GC化
-XX:InitiatingHeapOccupancyPercent
大きなヒープを前提に作られているのでヒープサイズが小さい場合はこのオプションで調整する。
-XX:+HeapDumpOnOutOfMemoryError
★- OutOfMemoryErrorが発生した際にヒープダンプを実行
- OOMEについての参考:java.lang.OutOfMemoryError
-XX:+CrashOnOutOfMemoryError
or-XX:+ExitOnOutOfMemoryError
★- OutOfMemoryErrorが発生した際にプロセスを終了させる
- 前者はエラーファイルを出力する
- OutOfMemoryErrorが発生すると、その後の挙動は不定状態となるため、すぐにKillすることが好ましい。
- Java 8u92から使用できる
- see also: Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その15 )( -XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError オプションのどちらを指定すべきか? )
-XX:OnOutOfMemoryError='bin/kill -ABRT %p'
- OutOfMemoryErrorが発生した際にコマンドを実行できる
- Java 8u91以前などで前述のオプションが使用できないときや、他に行いたいことがある場合に使用する
-XX:HeapDumpPath=<path>
-XX:+HeapDumpBeforeFullGC
- フルGCの前にヒープダンプを生成する。
- ヒープサイズが大きい程ヒープダンプには時間がかかるため使用に注意。
CMS GCはJava9で非推奨となっているため、現実的にはスループット型GCまたはG1 GCのいずれを選択するかになるように思います。 どちらを選択した方が良いかは、CPUの余力とヒープメモリのサイズによって変わります。両方が十分に大きい(多く必要とする)のであればG1 GCの方が基本的に適しています。フルGCに伴う停止を極力なくしたい場合はCPUとメモリを積んでG1 GCを選択しましょう。
see also: HotSpot Virtual Machineガベージ・コレクション・チューニング・ガイド
スループット型GC(ParallelGC)の設定
-XX:MaxGCPauseMillis=N
-XX:GCTimeRatio=N
★
最大一時停止時間目標→スループット目標→最少フットプリント目標(-Xmx
)の順に達成するよう動作します。
JVMはadaptive sizingがデフォルトで有効で、指定された目標値になるように自動的にメモリの拡張など調整を行います。
そのため、必要なヒープサイズが予め分かる場合には-Xms -Xmx
を同値に指定するとGCが最小限に留められるためパフォーマンスにプラスとなるようです。しかし、GC時間以上にスループット(Ops)の目標を十分に満たすことが重要であり、適切なサイズのヒープ(-Xmx
)とGCTimeRatioを指定してJVMに委ねても手作業でチューニングする場合と同等のスループットを達成できることがある点には留意が必要です。(上記Oracleのガイドでもデフォルトより必要なヒープサイズが大きい場合を除きXmxを指定しないことを勧めている。)
G1 GCの設定
-XX:MaxGCPauseMillis=N
★- 前述の通り
- G1 GCの場合デフォルトは200
- ほとんどこのフラグのチューニングが主となる(G1は予測モデルを用いて、この値をもとに動作を自動調整する)。
-XX:ParallelGCThreads=N
- GCスレッドの数
- デフォルトはCPU1つにつき1スレッド(9CPU以降は
(8 + (CPU数 - 8) * (5 / 8))
)だが、スレッドが多すぎても効果が薄くなるため小さくする。
-XX:ConcGCThreads=N
- マーク付けを行うバックグラウンドスレッドの数
- デフォルトは
(ParallelGCThreads + 2) / 4
- 十分なCPUが搭載されているなら、マーク付けを行うスレッドを増やすことでパフォーマンスにプラスとなる。
-XX:InitiatingHeapOccupancyPercent=N
まずはMaxGCPauseMillisのチューニングから始め、それでもフルGCが発生する場合に他のチューニングを行った方が良いでしょう。
その他
Application
Thread Pool
- プールサイズ
- DBアクセスのためのスレッドプールのように、ブロッキングが発生する部分のプールを分離した方が良い。
- DBにアクセスするクライアント処理があるスレッドプールでブロック状態となるスレッドが増える場合はプール数を増やすことで、そうでないプール(利用側)の処理が玉突きでブロック状態となることを緩和することができる(増やしすぎると前述のとおり外部リソースの負荷が拡大する)。
see also: Understanding Play thread pools
DB Connection Pool
maximumPoolSize
★- コネクションをプールする最大数
pool size = 最大スレッド数 * (最大同時接続数 - 1) + 1
- スレッド一つあたりが同時にいくつのコネクションを持つか?ということ
connectionInitSql
★- kamipo TRADITIONAL
- パフォーマンスというより事故を未然に防ぐため。
transactionIsolation = TRANSACTION_READ_COMMITTED
see also: HikariCP設定
JDBC driver
ステートメントキャッシュなどはドライバの設定で調整します。 Aurora(MySQL)への接続にMariaDB Connector/Jを使用しており、ここではそのドライバの設定項目を検討しています。*13
- SSL
- 必要に応じて
useServerPrepStmts
- サーバサイドでクエリ実行前にプリペアードステートメントの処理を行うようにする。
- デフォルト無効
- HikariCPのドキュメントではMySQLドライバにおいて有効化を推奨しているが、MariaDBドライバでも同様か不明であり過去に有効であったものを無効化された歴史もあるようなので、様子をみながら設定を検討した方がよい。
serverTimezone
- アプリケーションと異なるタイムゾーンである場合は指定が必要
Aurora(MySQL) failover support
jdbc:mysql:aurora://<hostDescription>[,<hostDescription>...]/
- ドライバレベルでフェイルオーバーがサポートされている。
- Auroraもフェイルオーバーを備えているが、ドライバ側で死活をチェックするためより早く切り替わる。
useBatchMultiSend
- Aurora failoverを利用する場合はfalseにする。
Coding
計測について
チューニングには計測がセットでなければいけないことは周知の通りです。 例えば、以下のような方法が考えられます。
- サーバメトリクス
- CPU
- Memory
- Network
- etc
- JMXによるアプリケーションでのメモリ、スレッド、DBコネクションリソース
-XX:+PrintCompilation
- GCログ ★
-verbose:gc
(=-XX:+PrintGC
)-Xloggc:<path>
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
or-XX:+PrintGCDateStamps
-XX:+PrintReferenceGC
- GCの際にソフト参照と弱い参照に対して行われた処理を記録
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=N -XX:GCLogFileSize=N
-XX:+PrintTLAB
- jcmd
netstat -an
TIME_WAIT
が大量発生しているとエフェメラルポートが不足している。
おわりに
ここまで見てきたように、最初に検討すべき項目は少なく、基本的にJVMは優秀です。 パフォーマンスに影響する多くのケースではアプリケーションコードが原因であることがほとんどであるように思います。 チューニングポイントを知るだけでなく、内部動作(スレッドモデルやメモリモデル、GCの仕組みなど)を知ることも不可欠です。
また、制約理論によれば局所最適化の罠などもあります。 システム全体のボトルネックを探すことが最重要ですが、パフォーマンス劣化に直面した際はパニックにもなるものです。 予めどのようなポイントがあるのか知ることができるのなら、備えるにこしたことはありません。 アプリケーションの性格によって最適解は異なりますが、最初に考える範囲は似通うのかなと思います。
更新履歴
- 2017/11/7 はてブコメントにあった発表資料「Kubernetesアンチパターン」へのリンクを追記
- 2017/11/7 はてブコメントにあった
-XX:+CrashOnOutOfMemoryError
or-XX:+ExitOnOutOfMemoryError
を追記 - 2018/3/20 JDK 10でのコンテナサポートについて追記
- 2019/10/6 良記事 JVMのヒープサイズとコンテナ時代のチューニング へのリンクを追記
*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を指定せざるを得ないケースはある…