はじめに
こんにちは! WEARバックエンド部バックエンドブロックの小島(@KojimaNaoyuki)です。普段は弊社サービスであるWEARのバックエンド開発・保守を担当しています。
WEARのバックエンドはRubyで動作しており、Ruby 3.3.6にアップデートしたことを機にYJITを有効化しました。本記事ではWEARにYJITを導入した際の効果とその考察をご紹介します。
目次
YJITとは
YJITはJITコンパイラの一種で、Rubyの処理速度の向上を図るものです。多くの企業でパフォーマンスの向上が報告されています。一方でマシンコードやそのメタデータをメモリに保管するため、メモリ使用量の増加には注意が必要です。詳しくはYJIT READMEをご覧ください。
パフォーマンス計測条件
Ruby 3.3.6 YJIT無効化状態とRuby 3.3.6 YJIT有効化状態でAPIのパフォーマンス計測を実施してそれらを比較しました。
--yjit-exec-mem-size
などのパフォーマンスに影響するオプションはデフォルト設定のまま実施しました。
WEARを構成するシステムの中でもRailsで構築している認可サーバーとリソースサーバーが存在しており、今回はその2つで計測しました。また、それぞれの規模を示すため、コード行数の概算を以下に示します。
- 認可サーバー: 1,000 ~ 5,000行程度
- リソースサーバー: 300,000行程度
事前準備
事前準備として、本番環境へ適用する前にステージング環境でYJITを有効化し、パフォーマンス計測を実施しました。
結果、レスポンスタイムが改善し、メモリ使用量なども過度に悪化することはありませんでした。しかし、YJITは頻繁に利用されるコードをコンパイルしてメモリに保存するため、本番環境に比べて極端にリクエスト数が少ないステージング環境では正確な検証ができていませんでした。
実際に、リソースサーバーにおいてステージング環境ではメモリ使用量が13%増加に留まっていましたが、本番環境では31%もの増加(詳しい結果は後述)をしてしまいました。これを回避するためには、負荷試験ツールなどを利用して本番同様に様々なAPIをリクエストすることでより正確な検証が可能と思います。
本番環境の計測結果
実際に本番環境でYJITを有効化した際の効果をご紹介します。
一週間の平均値をYJIT無効化状態とYJIT有効化状態に分け、それぞれレスポンスタイムとメモリ使用量を計測しました。
結果は、認可サーバーにおいて全体のレスポンスタイムは約11%改善し、メモリ使用量は約20%増加しました。リソースサーバーにおいて全体のレスポンスタイムは約19%改善し、メモリ使用量は約31%増加しました。
以降に計測結果の詳細を記載します。
認可サーバー
レスポンスタイム(99th percentile)
YJIT無効状態(紫) | YJIT有効状態(青) |
---|---|
47ms | 42ms |
約11%の改善が見られました。
メモリ使用量
YJIT無効状態(紫) | YJIT有効状態(青) |
---|---|
541MiB | 650MiB |
メモリ使用量は稼働しているpodsの合計値です。
約20%の増加が見られました。
リソースサーバー
レスポンスタイム(99th percentile)
YJIT無効状態(紫) | YJIT有効状態(青) |
---|---|
549ms | 445ms |
約19%の改善が見られました。
メモリ使用量
YJIT無効状態(紫) | YJIT有効状態(青) |
---|---|
64.3GiB | 84.2GiB |
メモリ使用量は稼働しているpodsの合計値です。
約31%の増加が見られました。
計測結果についての考察
結果は概ね期待通りのものでした。レスポンスタイムの改善が見られ、メモリ使用量は増加しましたが許容値以内の増加となりました。
リソースサーバーに関してはメモリ使用量が31%と大きく上昇してしまったのですが、メモリ使用率は元から余裕があったため、許容値の範囲に収まりました。
以下に今回の結果で気になった点で調査したことなどを掲載します。
リソースサーバーのメモリ使用量の増加が大きいことについて
リソースサーバーはYJIT有効化状態では約31%もメモリ使用量が増加しており、認可サーバーの増加率よりも高いことが気になりました。それは、リソースサーバーでは提供しているAPIの数が多く、使用されているRubyコードの種類も多いことが原因と考えられます。
YJITは一定の回数以上呼び出されたコードがYJITのコンパイルに対応していたらコンパイルしメモリに保存します。そのため、サービスで使われているコードの種類が多いほどメモリ使用量が増加すると考えられます。
そこで、YJITがコンパイルして生成したコードサイズを示すcode_region_sizeを確認しました。
上記グラフの通り、認可サーバー(緑)では平均7.7MiBだったのに対してリソースサーバー(赤)では平均42.9MiBとなっていました。code_region_sizeの他にそれと比例してメタデータも生成されメモリに保管されるなど、実際はより多くのメモリを使用していることになります。
このことから、認可サーバーよりもリソースサーバーにおいてメモリ使用量が増加してしまっているのは、使用されているコードの種類が多いことに原因があると考えられます。
さらにYJITを効果的に使うには
YJITによるメモリ使用量を抑えるならば、--yjit-exec-mem-size
や--yjit-call-threshold
の値調整で可能です。--yjit-exec-mem-size
はYJITが生成するコードサイズの上限を指定できるのでメモリ使用量を抑えることができます。--yjit-call-threshold
はYJITが関数のコンパイルを開始するまでの呼び出し回数を指定できます。大きくすることであまり呼び出されない関数はコンパイルしなくなるためメモリ使用量を抑えられます。詳細はYJIT README Command-Line Optionsをご覧ください。
しかし、これらの値の調節はYJITの効果が薄れる可能性もあります。そのため、メモリ使用量とパフォーマンスのバランスを考慮して設定する必要があります。
WEARでは現在のメモリ使用量でも問題ないため実施していませんが、今後改善が必要になったら検討しようと思っています。
また、Ruby 3.4にアップデートすることでもさらなるパフォーマンス改善が期待できます。Ruby 3.4.0 リリースより、「x86-64とarm64の両方のプラットフォームにおいて、ほとんどのベンチマークのパフォーマンスが向上しました」や「メタデータの圧縮と統一的なメモリ使用量制限によりメモリ使用量を削減しました」と記載されています。そのため、Ruby 3.4でもYJITの改善が期待できます。
まとめ
本記事ではWEARでのYJITの効果と考察についてご紹介しました。今回改めて、既存コードの修正が不要で全体のパフォーマンスを改善できるYJITは素晴らしいと感じました。今後のYJITの更なる発展に期待したいと思います。
参考文献
本記事を作成するにあたり、以下の文献を参考にさせていただきました。貴重な情報を公開してくださった著者の皆様に感謝申し上げます。
- YJIT README
- Ruby 3.3 YJITのメモリ管理とRJIT 〜すべてが新しくなった2つのJITを使いこなす
- YJITの性能を最大限引き出す方法 - k0kubun's blog
- Ruby 3.4.0 リリース
お知らせ
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。