JVMアプリケーションのCPUボトルネックをasync-profilerで調べてみる
こんにちは、事業開発部の齋藤です。 ここ最近ずっと負荷試験をやっていました。
今日はJVMのアプリケーションのプロファイルを取ることが出来る、「async-profiler」を使ってみたので そのツールの紹介記事です。
はじめに
今回使ってみよう、というきっかけになったのは 負荷試験実行中にやけにCPU負荷が高い、ということで プロファイラを試してみるか、という話になりました。
このasync-profilerを選んだ理由は 単に知っている中で一番楽に使えそう、というので選びました。 また、Javaのプロファイラはいくつか商用製品もあるみたいなのですが 今回はちょっと調べてみたい、ということでasync-profilerを選びました。
今回の記事ではprofilerを掛けて 「Flame Graph」というグラフを見るところまでをやってみます。
詳しくは以下の流れでやっていきます。
- インストール
- プロファイラを掛ける
- Flame Graphを見る
Flame Graphについては以下のサイトを参考にしながら 見ていきました。
インストール
Profileは今回はMacで行うので ダウンロードしてくるのはMac向けのアーカイブです。 Linux向けのアーカイブも提供されているので Linux上でのProfileを行いたい方は GitHubのリポジトリにあるREADMEを確認してください。
$ curl -L https://github.com/jvm-profiling-tools/async-profiler/releases/download/v1.4/async-profiler-1.4-macos-x64.tar.gz -O $ tar xvf async-profiler-1.4-macos-x64.tar.gz
Usageが表示されるので必要になったら見てください。
$ ./profiler.sh Usage: ./profiler.sh [action] [options] <pid> Actions: start start profiling and return immediately stop stop profiling status print profiling status list list profiling events supported by the target JVM collect collect profile for the specified period of time and then stop (default action) Options: -e event profiling event: cpu|alloc|lock|cache-misses etc. -d duration run profiling for <duration> seconds -f filename dump output to <filename> -i interval sampling interval in nanoseconds -j jstackdepth maximum Java stack depth -b bufsize frame buffer size -t profile different threads separately -s simple class names instead of FQN -o fmt[,fmt...] output format: summary|traces|flat|collapsed|svg|tree|jfr --title string SVG title --width px SVG width --height px SVG frame height --minwidth px skip frames smaller than px --reverse generate stack-reversed FlameGraph / Call tree -v, --version display version string <pid> is a numeric process ID of the target JVM or 'jps' keyword to find running JVM automatically Example: ./profiler.sh -d 30 -f profile.svg 3456 ./profiler.sh start -i 999000 jps ./profiler.sh stop -o summary,flat jps
プロファイルを取ってみる
今回は単純に60秒間プロファイルを取ってみましょう。
$ jps # プロファイルを掛けたいJVMのPIDを調べる $ ./profiler.sh -d 120 -f `pwd`/flamegraph.svg <PID> # 120秒ぐらい待つ
これだけでFlame Graphを取ることが可能です。
取ったFlame GraphはChromeなどのブラウザで開くと良いです。 内部にスクリプトが埋め込まれており、ズームイン、ズームアウトすることが可能です。
今回負荷調べる時にどうやってプロファイルを取ったかについて
今回はいくつかの方針を立ててプロファイルを取っていきました。
- ローカルで実行出来る範囲でプロファイルを取る
- JVMおよびフレームワークの起動が完全に終わった後にプロファイルを取る
- watch -n1 scenario.sh といった形で負荷を掛けるシナリオなどを用意して、JVMの状態を見たい状況にする
- JITコンパイルやクラスローディングの話が絡んでくるので軽くJVMを暖気してからプロファイルを取る
上記方針でプロファイルを掛けました。
Flame Graphについては冒頭でも上げましたが 以下のリンク先の内容を参考に見ていきました。
今回取ったプロファイラでの結果を元に以下のようなFlame Graphを取ることができました。
スクリーンショットは断片的なものですが、見たところ、spring-data-mirageのメソッド内部でCPUを使っているように見えます。 ZIPへのIOをしているようなので、IOを減らす意味でも、この部分を減らせないか実装コードを確認しました。 インスタンス作成時に1回だけ実行する形に出来そうなので、以下の修正のPRを送りました。
まとめ
今回の記事では、async-profilerを使って アプリケーションにプロファイラを掛けてみました。 今回の利用用途は非常に限定的ですが簡単にプロファイルを取ることが出来ました。 また、今回取ったプロファイルから、Flame Graphを見てライブラリを直すことが出来ました。 その結果、CPU利用率が抑えられ、WebアプリケーションからRDBにもっと負荷が掛かるようになりました!
皆さんも、CPUボトルネックを調べるために使ってみてはいかがでしょうか。 ボトルネックを修正出来ると楽しいですよ!
Docker コンテナ上でのプロファイルも取ってみたいところですが linux perf eventsを制限していてプロファイルが取れませんでした。 Docker コンテナ側の修正が必要になりそうです。
今度試す時はDockerコンテナ上動くJVMアプリケーションに対してプロファイルを掛けてみたいところです。
今回の記事はこれで終わりです。
TIPS: PID楽に調べたい。
以下のコマンドで JVMのPIDを調べるのが楽でした。
ソートしておくと 1回1回表記が変らなくて良いです。
$ watch -n1 jps | sort -k1,1 -n