自己紹介とセッションのアジェンダ

磯田浩靖氏(以下、磯田):「JVMパラメータチューニングにおけるOptunaの活用事例」の話をします。

2021年5月に「JJUG CCC」というJavaのカンファレンスがあり、その中でOptunaを使ったJVMパラメーターチューニングの話をしました。この資料は、そのOptunaに絞った内容です。同じ内容を話すのはもったいないと思ったので、JJUCよりもサンプルコードの量を増やしています。代わりにチューニングの背景やGCログの話など、いくつか割愛しているので、興味があれば資料を参照してください。すでにTwitterに「#Optuna」のハッシュタグを付けてこの資料のリンクを貼っているので、読むペースを自分で調整したい方はこちらを見てください。

今日の流れです。自己紹介のあと、チューニングしている「ZGC」について少しだけ触れて、Optunaを使ってZGCをどのようにチューニングしていったかについて話します。最後に、実際にOptunaを使ったユーザー目線の感想を話して締めたいと考えています。

僕は磯田浩靖といいます。ウルシステムズ株式会社に所属しています。今日はもう1人、SMN株式会社の栗原秀馬さんと一緒に話そうと思います。

JVMパラメーターのチューニングにOptunaを採用

では、チューニング対象について触れます。Javaのガベージコレクションの1つであるZGCをチューニングしました。ZGCはJava11くらいから使えるようになっていますが、今使っている「G1GC」と呼ばれるGCから置き換えることができるかを、JVMパラメーターのチューニングを行いながら検証し、その中でOptunaを使いました。

少しだけ対象システムの紹介をします。「Logicad」と呼ばれるオンライン広告入札システムです。このシステムは、秒間40万件くらいのリクエストを受けていて、一つひとつに対してだいたい3〜5ミリ秒でレスポンスすることが求められています。ただ、99パーセンタイル値を見ていくと75ミリ秒くらいかかる時があり、これを速いGCと言われるZGCを使って解決できないかと思ったことがきっかけです。

ZGCを使うにあたって、JVMパラメーターのチューニングをしたわけですが、ZGCのパラメーターにはけっこうオプションが多いんです。これを人がやるとメチャクチャ時間がかかって大変なので「どうしたものかな」と調査をしていた中で、今回のOptunaを使うことに決めました。Optunaの話はもう十分だと思いますが、要は人が一つひとつ組み合わせを決めて検証を繰り返すと、すごく時間がかかるので、Optunaで自動的に組み合わせのセットを決めて計測していくということです。

「枝刈り機能」で検証時間を短縮

もう1つ、僕がすごく好きな「枝刈り機能」の話をします。これを使えば、見込みのない組み合わせの処理を途中で打ち切ることができます。(スライドを指して)例えば枝刈りがないと、この下の青いライン(性能が出ていない例)を長時間実施しても結果は変わりません。枝刈りがある場合が、右側のグラフのグレーのラインです。バチッと途中で切れていますが、性能が出ないと予測される組み合わせについては、Optunaによってこのように打ち切ることができます。つまり、トータルの検証時間を抑えることができます。

(スライドを指して)左側が、OptunaでJVMパラメーターの組み合わせをいろいろ変えて計測したイメージ図です。Trial 1としてOptunaでJVMパラメーターをセット。例えばメモリ32GB、スレッド数5をOptunaが決めてくれます。それをアプリケーションの起動スクリプトに渡してJavaサーバーを起動して、そのサーバーに対して負荷をかけ、負荷をかけている間にメトリクスを取ってそれをOptunaに記録するという流れです。

これを1つのトライアルとして、次のトライアルでは再びOptunaが、メモリサイズを40GB、スレッド数を3に変えると決めてくれて、同じようにサーバーを起動してリクエストを受けてメトリクスを取得するという流れを、グルグル回して自動化できました。

(スライドを指して)こちらがもう少し踏み込んだ具体例です。Optunaに渡すオブジェクティブ関数の中でJVMパラメーターの決定を行っています。真ん中辺りに「heap_size = trial.suggest_int('heap_size', 32, 80, step=8)」という行がありますが、これは「32GBから80GB内で、8GB間隔でどれかを選んで」という指定です。なので、32や40、48といった値が返ってきます。このように、他のZGCパラメーターも同じく範囲指定をして、今回のトライアルで実行するパラメーターセットを決めて、このベンチ関数(独自の関数)の中でJavaのアプリケーションを起動してメトリクスを取っています。

(スライドを指して)このベンチ関数の、「test_case」と書いてあるのが今回のトライアルで使うJVMパラメーターの組み合わせです。これをJavaサーバーを起動する処理に渡して、Javaサーバーが起動するまで待って、起動したら「report関数」でメトリクスを取っています。

(スライドを指して)次のページにreportの中身を記しています。真ん中のほうに「get_metrics(Metrics.QPS5)」とあります。こちらでJavaのJMXという機能からメトリクスを取得しています。例えばQPS 5分間平均なら5097QPSという数値が返ってくるので、それをOptunaのAPIに渡して情報を記録します。30分や1時間という一定時間に対して定期的にメトリクスを取って、トライアルを実行しています。(スライドを指して)その中で枝刈りをしたい場合は、今は2行ほどコメントアウトしていますが、この「trial.should_prune()」を呼び出すと、Optuna側でこのトライアルは打ち切るべきだと判定をしてくれるので、それをもとにトライアルを終了できます。

Javaからメトリクスを取得する方法

どのようにJavaからメトリクスを取っていくかという話もしておきます。Javaにはいくつか「メトリクスの収集ライブラリ」があります。MicrometerやMeterと呼ばれるものですが、それらを経由して、JMXの機能を使ってJavaのプロセスの外から取れるようになっています。例えば、qpsというメトリクスを定義しておいて、アプリケーション側でqpsを記録したいタイミングでメトリクスをマークしておくと、(スライドを指して)左下のJMXから「このqpsの情報は、例えばOneMinuteRate(1分間平均)で4012QPS受けています、5分間平均で5097QPS受けています」という情報が取れます。この値を先ほどのget_metrics関数などで取得してOptunaに渡しています。

(スライドを指して)こちらが今までの流れのまとめです。まずOptuna側で、そのobjective関数の中でJVMパラメーターのセットを決めます。右側のスライドは、JVMパラメーターでJavaサーバーを起動して、起動したらメトリクスを定期的に取ってOptunaに記録していきます。この結果はRDBに保存することも、グラフで確認することもできます。

JVMパラメーターの組み合わせの結果を確認する方法

では、実際にどのようなグラフを見ていくか。(スライドを指して)左側は「Hyperparameter Importance」として、どのJVMパラメーターが寄与しているか、などがわかります。このスライドだと説明の端が切れていますが、スレッドのチューニングをすると寄与が大きいことがわかります。右側は各トライアルでのQPSの変遷です。茶色や上のほうのオレンジのトライアルの結果が良かったので、このトライアルのJVMパラメーターはどのような組み合わせだったかを確認しながらやっていました。

(スライドを指して)さらに、一つひとつのパラメーターに着目する例です。図の縦軸がQPSで、高ければ高いほど良いです。横軸はメモリサイズです。このプロットを見ると、右上に良い結果が寄っているので、基本的にはメモリサイズを上げれば上げるほど良い結果になることがわかります。このくらいわかりやすいといいのですが、対して「ZFragmentationLimit」というJVMのパラメーターでは、例えば単に80や90という高い値を指定しても良い性能になることはなく、やはりデフォルト値近辺の性能が良さそうだとわかります。

(スライドを指して)さらに2つのパラメーターの相関を見たい場合は、等高線形式で見ることができます。この青色が濃いところは性能が出ていない部分、左下のように色が薄くなっているところは性能が良い部分らしいです。

例えば、ZMarkStackSpaceLimitとZFragmentationLimitという2つのパラメーターの組み合わせを見ると、左下に色が薄いものが寄っているので、ともに低い値のほうが性能が良さそうだとわかります。

対してヒープサイズとZMarkStackSpaceLimitの組み合わせを見ると、基本的には上のほうが色が薄くなっています。なので、2つのパラメーターにはあまり相関がなく、基本的にはヒープサイズが大きければ大きいほど良いことがわかります。このような情報を活用して、どのJVMパラメーターが良いかを検証していました。

JVMパラメーターをチューニングする意味はある

まとめです。Optuna側でJVMパラメーターのセットを決めて、それをもとにアプリケーションサーバーを起動して、リクエストを受けてメトリクスを取ってきます。このトライアルを繰り返して各JVMパラメーターのQPSの結果を溜めて、Optuna Dashboardなどでグラフを見たり、RDBに入っている数値からどの結果が良かったのかを確認したりして、めぼしい組み合わせを見つけていました。

では、実際にJVMパラメーターをチューニングする意味はあるのか。これはQPSのグラフですが、右側がZGCのデフォルト設定で動かした場合です。3,000QPSくらい出ていますが、きちんとJVMパラメーターをチューニングしていくと、6,000~6,200くらい出る組み合わせもあるので、新しいGCだからといって単純にデフォルトで使っていいわけではなく、きちんとパラメーターをチューニングしていく意味はあります。(スライドを指して)そのパラメーターは、途中で見せたとおり組み合わせがすごく多いので、人の手でやるとメチャクチャ大変です。なので、今回のOptunaのように自動化してくれる仕組みがあってすごく助かりました。

Optunaを利用した感想

今回同席している栗原さんが、Optunaまわりをいろいろとがんばってくれたので、栗原さんに感想を聞いて締めたいと思います。

栗原秀馬氏(以下、栗原):栗原です。「せっかくがんばったので感想だけでも発表しな」という気遣いですね。まとめると、OptunaのおかげでJVMパラメーターの良い組み合わせをグラフや数値ベースで議論できて、本番のリリースまでこぎ着けました。感想ですが、まずAPIは非常にわかりやすいのではないかと思います。「Optunaを使ってこういうチューニングをした」とチームメンバーの磯田さんに説明した時も、ほぼコードを見せるだけで済んだので、書きやすいとともに読みやすい。Pythonにあまり触れたことがない人にもわかりやすいと思ったので、急にチームに持っていっても受け入れられるのではないかと思います。

2つ目は、最適化を抜きにしても試験の自動化ツールとして非常に優秀だと思いました。結果をDBに保存したり、実は今回は並列でベンチしていますが、ベンチ用のサーバーを2台用意して実施した時に、そこら辺の本来細々して面倒な部分を全部Optunaが吸収してくれたのでスクリプトを書くのが非常に楽になった。グリッド探索のみを使う場合でも非常に便利だろうと思いました。

3つ目は、周辺ツールです。試験が順調か、異常がないかはダッシュボードを見ればわかりました。ほかにも、スライドにもありましたが、ビジュアライザーは表ベースにするとどのような値が良いかがグッとわかりやすくなって共有しやすいと思います。その辺が充実しているのでいいと思います。

最後に、今後について。人の手でチューニングしている場所はいろいろあるので、もっと使いたいと思っています。組み合わせが膨大すぎて人の勘に頼っている場所もそれなりにあります。例えば、不要な入札を防ぐ枝刈りのロジックなどは順序を変えると性能が変わります。どのような順序で実行すれば速いかを人の勘に頼って判断してきましたが、そこもOptunaを使って実験してみたいと思っています。

勘に頼る要素をどんどん排除してシステム化していけたらと思える、とても便利なツールです。開発者のみなさん、本当にありがとうございます。感想は以上です。

磯田:栗原さん、ありがとうございます。僕たちの発表は以上です。

質疑応答

司会者:べた褒めされてありがたいです。いくつか質問が来ています。1つ目は、おそらく途中のテストについて。「異常終了した場合でも問題ありませんか? 再開できますか?」という質問です。テストしている最中に異常終了したケースはありましたか?

栗原:ありました。ヒープサイズをあまりに少なくすると、そもそもアプリケーションが起動しませんでした。スライドの中に、1点だけQPSがすごく下がっているグラフがあったと思いますが、異常終了したことがありました。その時はあまり良い方法がわかりませんでしたが、枝刈りをしました。アプリケーションが起動しないパラメーターはどうせあまり有効ではないので枝刈りさせて、そのまま次の試行に移動させました。

司会者:もう1つ「今回はテスト用の負荷で最適化していると思いますが、実際にプロダクション環境などでテストと想定の違う負荷のパターンが来た場合、かえってパフォーマンスが悪くなることが考えられますが、どのように対策していますか?」と質問が来ています。

栗原:そうですね、試験と違うパターンが来ることは考えられます。それによって性能が落ちてもしょうがないと、ある程度我慢のうえで合意してもらって本番にこぎ着けました。完全に本番のリクエストに近付けることはできないので、そこは切り捨てても仕方がないと思っています。

磯田:少し補足をします。スライドには書いていませんが、Nginxで入口を切って、Nginxのミラーリング機能で本番相当のデータをコピーして、別のリクエストを検証用のサーバーに送ることもできます。そこで実際のデータのバリエーションをなるべくカバーしてベンチマークを取りましたが、その環境でもそんなに性能は落ちないという確認は一応しています。

司会者:質疑応答は以上です。ありがとうございました。