Ruby 3.4に導入される次世代の帯域外ガベージコレクション(翻訳)
私は2023年に、RubyのガベージコレクタをShopifyのモノリス向けにチューニングしたときの方法について以下の記事を書きました。この記事では、メジャーガベージコレクション1によるレイテンシへの影響を軽減するために帯域外ガベージコレクションを実装する方法についても触れました。
参考: Adventures in Garbage Collection: Improving GC Performance in our Massive Monolith (2023) - Shopify
レイテンシの改善は著しかったものの、帯域外ガベージコレクションをトリガーする全体的なヒューリスティック(発見的方法)については満足できたわけではありませんでした。このヒューリスティックの基礎となったのは純粋な平均値であったため、レイテンシとパフォーマンスをトレードオフしなければなりませんでした。さらに重要なことは、リクエストサイクルからメジャーガベージコレクションを完全には排除しきれなかったことです(ごくまれにしか発生しませんが)。
しかし2023年12月に#50449のコメントでKoichi Sasadaと話し合ううちに、新しいアイデアを思いついたのです。
🔗 メジャーガベージコレクションを完全に無効化する
リクエストサイクルでメジャーガベージコレクションが絶対トリガーされないようにしたいなら、いっそ完全に無効にしてはどうでしょうか?
2024年3月に開催された、年に一度のRubyインフラストラクチャチームの会合で、そのために必要な新機能の詳細を詰め、Matthew Valentine-Houseが概念実証に取り組み始めました。その後、私たちのproductionサーバーのごく一部にデプロイして効果を検証してみました。
まず、ガベージコレクタによるメジャーガベージコレクションの自動実行を完全に防ぐ方法が必要でしたが、オブジェクトを古いオブジェクトに昇格させない方法も同時に必要でした。
理想としては、Webアプリケーションでリクエストの一環としてアロケーションされるオブジェクトは、そのリクエスト自身よりも長生きすべきではありません(一部のインメモリキャッシュを除く)。
長生きしているオブジェクトがあるとすれば、おそらく起動時にeager loadingされたオブジェクトか、さもなければリクエスト間でステートが漏洩しているオブジェクトです。リクエストサイクルの間にそのような古いオブジェクトに昇格されると、常駐させる意味がある可能性がほぼなくなるので、昇格は無駄になります。
また、リクエストサイクルの外で必要最小限のトリガーを手動で発生させるために、メジャーガベージコレクションを実行するかどうかをガベージコレクタに問い合わせる方法も必要でした。
当初#20443で提案されたのは、以下の3つのメソッドでした。
GC.disable_major
GC.enable_major
GC.needs_major?
他のRubyコミッターたちと何度かやりとりした結果、3つのメソッドはGC.config(rgengc_allow_full_mark: true/false)
という単一のメソッドに結実しました。
また、GC.latest_gc_info
と:needs_major_by
という新しいキーも公開して、メジャーガベージコレクションを実行するかどうかをGC.latest_gc_info(:needs_major_by)
のようにチェック可能にしました。
この新機能は、Ruby 3.4.0-preview2
の一部としてリリースされています。
🔗 効果
ShopifyのモノリスはRubyのmaster
ブランチで実行されるため、これらの新機能を使うために12月のリリースまで待つ必要はありません。そこで、新しい「帯域外GC」を有効する作業に取りかかったところ、すべてのメトリクスで著しい結果を得られました。
最初は、予想どおりリクエストサイクルの最後尾(p95/p99/p99.99)でGCに費やされる時間が著しく短縮されました。
しかしさらに驚いたのは、平均レイテンシも改善されたことです。
もちろん、サービスのレイテンシにおける全体的な好影響はもっと穏当なものでしたが、それでも平均レイテンシが5%、p99レイテンシが10%削減されたのは非常によい結果です。
ただし、パフォーマンスへの好影響は思ったほどではなく、デプロイが頻繁に行われる日中は目立った変化がありませんでした。ただし、デプロイが数時間ほど行われなくなる時間帯では、新しい帯域外ガベージコレクタの実行頻度は大幅に下がりました。
🔗 実装
さらに、私たちの場合はPitchforkというHTTPサーバーが提供するフックのおかげで、この新しい実装は大幅にシンプルなものになっています2。
# pitchfork.conf.rb
after_worker_fork do |_server, _worker|
GC.config(rgengc_allow_full_mark: false)
end
after_request_complete do |_server, _worker, _rack_env|
if GC.latest_gc_info(:needs_major_by)
GC.start
end
end
🔗 次のステップ
メジャーガベージコレクションを一掃できたので、次のステップではマイナーガベージコレクションを見ていくことになります
マイナーガベージコレクションを無効にするわけにはいきません(さもないと大きなアロケーションを行うリクエストでメモリ不足が発生します)。しかし、GC.stat
で得たヒューリスティックをさらに追加することでガベージコレクションを帯域外から積極的にトリガーすることで、大半のリクエストがGCに時間を費やさずに済むようにすることを見込めるでしょう。
ただし、マイナーガベージコレクションは既に私たちのモノリスでも非常に高速なので、改善による伸びしろはずっと小さくなります。
関連記事
- 訳注: メジャーガベージコレクション(メジャーGC)はフルガベージコレクション(フルGC)とも呼ばれ、古いオブジェクトと新しいオブジェクトの両方をGCの対象とします。マイナーGCは、新しいオブジェクトのみをGCの対象とします。参考: RubyのGCを動かして、ざっくり理解する #Ruby - Qiita ↩
-
訳注: 原文サンプルコードの
need_major_by
は#10598に基づいてneeds_major_by
に修正しました。 ↩
概要
CC BY-NC-SA 4.0に基づいて翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
CC BY-NC-SA 4.0| 表示 - 非営利 - 継承 4.0 国際 | Creative Commons