Rails: Autotuner gemでRailsアプリを高速化する(翻訳)
原注
本記事は、Rails Worldでの私の発表「Rails and the Ruby Garbage Collector: How to Speed Up Your Rails App」を元にしています。
Rubyのガベージコレクタは、短いRubyスクリプトから数百万件のリクエストを処理するアプリの実行にいたるまでスケーリング可能になるよう設計されていますが、あらゆるユースケースで最適に動作するとは限りません。このため、Rubyのガレージコレクタでは、チューニングに利用可能なパラメータを多数サポートしています(執筆時点では19個)。ただし、これらのパラメータを使いこなすには、Ruby内部におけるガベージコレクタの動作についての知識が必要です。
また、Rubyのメジャーリリースでガベージコレクタが変更されると、これらのパラメータの一部が非推奨になったり新しいパラメータが追加されたりする可能性があるのも辛い点です。つまり、その場合はガベージコレクタの変更内容を理解して、最適なパフォーマンスを取り戻すためにガベージコレクタを再度チューニングする必要があるということです。このあたりが複雑なせいで、Rails開発者がガベージコレクタのチューニングになかなか踏み切れないことがよくあります。
そういうわけで、私たちShopifyはAutotunerというgemを作り上げました。Autotunerは、Railsアプリのトラフィックを分析して、アプリのガベージコレクタを最適化する提案を提供します。Autotunerは、READMEに沿って進めれば簡単にセットアップできます。
本記事では、Autotuner gemを作成した動機と仕組み、Shopifyでガベージコレクタのチューニングをどのように実験したかというプロセスについて詳しく解説します。
🔗 Autotunerを作った動機
Railsアプリを高速化する戦略には、「サーバーの高速化」「データベースクエリの改善」「バックグラウンドジョブへのロジックの移動」などさまざまなものがありますが、アプリがガベージコレクタで思った以上の時間を食っている可能性もあります。Jean Boussierによる以下のブログ記事にも示されているように、ガベージコレクタをチューニングしたことで、ガベージコレクタの99.9パーセンタイル時間が「1秒以上」から「0.15秒」へ、つまり87%まで短縮されました。
参考: Adventures in Garbage Collection: Improving GC Performance in our Massive Monolith (2024)
Shopifyの「Storefront Renderer」でも同様に、ガベージコレクタのチューニングによってガベージコレクタの99.9パーセンタイル時間が59%短縮され、レスポンス時間は18%短縮されました。
参考: Two Garbage Collection Improvements Made Our Storefronts 8% Faster | Rails at Scale
ガベージコレクタによる影響は、多くの場合テイルレイテンシ(99パーセンタイルや99.9パーセンタイルなど)に集中します。ガベージコレクタの実行はそれほど頻繁ではないのが普通です(なお、頻繁に実行されているとしたら別の問題が発生しています!)。テイルレイテンシは多くの場合、メジャーガベージコレクションサイクルが原因で発生します(ガベージコレクションのメジャーとマイナーの違いについて詳しくは私の個人ブログ記事をご覧ください)。
そして驚くべきことに、Railsアプリで実行されるメジャーガベージコレクションサイクルは、実はほとんどが不要です。しかし、負荷の最適化やメモリ使用量削減のためのさまざまなヒューリスティック(heuristics: 発見的方法)1によって、最終的にRubyのガベージコレクタが必要以上のガベージコレクションサイクルを実行することになります。ガベージコレクタでチューニングすべきパラメータがどれなのかを理解していれば、チューニングは簡単で、Railsアプリのレスポンス時間を大幅に改善できます。
仮にガベージコレクタの調整方法を熟知していて、Railsアプリでガベージコレクタを最適化したことがあるとしましょう。しかし、Rubyの次のメジャーリリース(1年またはそれより短いサイクル)にアップグレードするときは、最適なパフォーマンスを取り戻すためにガベージコレクタの変更内容を把握して再度チューニングしなければならなくなることがあります。チューニング設定が古いままだと、場合によってはRailsアプリが遅くなる可能性もあります。
🔗 Autotunerのしくみ
AutotunerはRackプラグインとして動作し、個別のリクエストの前後にガベージコレクション関連のデータと、リクエストの処理に要した時間を収集します。リクエストが完了すると、これらすべてのデータがヒューリスティックのリストに渡されます。個別のヒューリスティックには、ガベージコレクション時間を最適化する特定の戦略が盛り込まれています。
個別のヒューリスティックは、必要なデータを選択して現在の傾向を特定するために保存できます。たとえば、HeapSizeWarmup
というヒューリスティックを詳しく見てみましょう。
このヒューリスティックは、アプリのウォームアップが完了してパフォーマンスが最大に達した後のタイミングで生成されるメモリヒープの大きさを示します。アプリの起動中に多数のオブジェクトがアロケーションされてRubyのガベージコレクタによってヒープが増大すると、ガベージコレクションのサイクルが頻繁に実行されることを意味します。Rubyのガベージコレクタはすべてをストップさせるので、パフォーマンスに悪影響を及ぼします。つまり、ガベージコレクタの実行中はRailsアプリのコードも実行を一時停止します。Railsアプリの場合、ヒープが安定するまでのウォームアップ期間でレスポンスタイムが長くなるという形で発現します。以下はその様子を表す図です。
HeapSizeWarmup
ヒューリスティックは、個別のリクエストで要した時間とヒープのサイズを記録することで、リクエスト時間が頭打ちになってパフォーマンスがピークに達する時期を判定します。続いてHeapSizeWarmup
ヒューリスティックは、Rubyのヒープをこのサイズに設定するよう提案します。これにより、起動処理中にヒープサイズが時間をかけて肥大化するのではなく、ただちにそのヒープサイズに達するようになります。
🔗 ガベージコレクタのチューニングをアプリで実験する方法
🔗 メトリクスを収集する
実験を行うときは、ボトルネックを理解することと、改善したい指標を特定すること、そして改善の測定方法を知ることが重要です。Autotunerが提供するAutotuner.metrics_reporter
コールバックを使えば、「リクエストの処理に要した時間」「ガベージコレクションに要した時間」「実行されたメジャー/マイナーガベージコレクションサイクルの回数」「Rubyヒープのサイズ」といったメトリクスがリクエスト単位で報告されるので、これらの作業を開始するのに有用です。
得られたメトリクスを手がかりに、ガベージコレクタをチューニングする価値があるかどうかを判断できます。最適化する主な項目は以下のとおりです。
- 起動時のパフォーマンス
アプリの起動が遅く、ガベージコレクションの実行時間が長くなっている場合は、ガベージコレクタをチューニングすることでアプリがピークパフォーマンスに達するまでの時間を短縮できる可能性があります。 - 平均レスポンスタイム
アプリのリクエストタイムのうち、ガベージコレクタが処理時間を消費する割合が高い場合は、ガベージコレクタをチューニングすることで、ガベージコレクションサイクルの頻度が下がって平均レスポンスタイムが改善される可能性があります。 -
極端な場合(99パーセンタイル、99.9パーセンタイル)のレスポンスタイム
時間がかかる上位リクエスト群でガベージコレクタが処理時間を消費する割合が高い場合は、ガベージコレクタをチューニングすることで、これらのリクエストに対するガベージコレクションの影響を縮小して、極端なレスポンスタイムを短縮します。
🔗 Shopifyで実施されているガベージコレクタのチューニング実験方法
Autotunerによる提案がすべてパフォーマンスに好影響をもたらすとは限らず、ものによってはトレードオフとなります。よくあるトレードオフの1つは、ガベージコレクタやレスポンスタイムが改善される代わりに極端な場合(99パーセンタイルや99.9パーセンタイルなど)の値が悪化するというもので、ガベージコレクションサイクルの頻度を下げるチューニングでよく起きます。ガベージコレクションサイクルの頻度を下げると、多くの場合平均のパフォーマンスが向上しますが、その分ガベージコレクション実行時に片付けなければならない「死んだオブジェクト」が増加して作業が増える可能性があります。負荷や要件によっては、平均パフォーマンスを改善するために極端な場合のパフォーマンスを犠牲にしたくない場合もありえます。
レスポンスタイムは、「負荷」「トラフィックのパターン」「データベースやキャッシュのレスポンスタイム」といった要因によって大きく左右される可能性があることがわかってきました。このため、2つの期間を取り出してチューニングの影響を精密に比較することは困難です。
以上の理由を考慮して、productionトラフィックの一部に対してテストを実施し、以下の3つのグループに分けます。
- 未調整群
このグループはガベージコレクタをチューニングしていません。これはチューニングによる最終的な改善を比較するための統制群(control group)です。 -
安定群
このグループは、ガベージコレクタのパフォーマンスを改善するチューニングが施されています。 -
実験群
このグループには、Autotunerによるチューニング提案を1つずつ適用します。
高トラフィックアプリの場合は、3つのグループからサーバートラフィックの1〜5%をそれぞれ選択します。
低トラフィックアプリの場合はデータの分散を抑えるために、選択する割合をもっと高くします。
ガベージコレクタのチューニング実験は、比較的安全に行えます。途中でインシデントが発生したりパフォーマンスが急低下したりすることは普通起きないので、トラフィックの大部分を対象に実験しても、さほど危険はありません。
私たちの場合、ガベージコレクタのチューニング実験を以下の手順に沿って進めることにしています。
- Autometerによるチューニング提案の1つを取り出して、実験群に適用する。
- 実験群と安定群のさまざまなパフォーマンスメトリクスを数日〜1週間比較する
- チューニングでパフォーマンスが著しく向上した場合は、そのチューニング提案を安定群にも適用する
- チューニングでパフォーマンスの大きな改善が見られない場合や、発生するトレードオフが望ましくない場合は、そのチューニング設定を実験群から削除する
- 残りのチューニング提案についてもステップ1を繰り返す。
- 安定群と未調整群のパフォーマンスを比較して、全体的なパフォーマンス向上が見られるかどうかを比較する。
以上のプロセスを経れば、Railsアプリは理想的な形で高速化するはずです。Rubyバージョンをアップグレードする場合や、アプリが大幅に変更された場合は、Autotunerを削除して実験をやり直せるようになりました。
🔗 まとめ
Rubyのガベージコレクタは、メモリ使用量とパフォーマンスのバランスを取りながらさまざまな負荷状況に適応するよう設計されており、多くの場合良好なパフォーマンスを得られますが、そのままで最大のパフォーマンスを得られるわけではありません。ガベージコレクタをチューニングすることで、特定の負荷状況や気になるメトリクスに合わせて最適化を行えます。
しかしRubyのガベージコレクタは、多くのRails開発者にとってブラックボックスなので、チューニングの方法を見出すのは一苦労です。Autotuner gemは、Railsアプリのガベージコレクタのパフォーマンス改善方法の発見を支援するよう設計されています。
本記事では、ガベージコレクタをチューニングする動機、Autotunerの仕組み、Autotunerによる変更の実験方法についてひととおり見てきました。Autotunerの完全なセットアップ方法についてはREADMEをお読みください。
概要
CC BY-NC-SA 4.0 Deedに基づいて翻訳・公開いたします。
CC BY-NC-SA 4.0 Deed | 表示 - 非営利 - 継承 4.0 国際 | Creative Commons
日本語タイトルは内容に即したものにしました。