Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

tetoru は Ruby 3.3 + YJIT で本番運用しています

プロダクト本部 tetoru 開発部の中田です。普段は giraffate という ID を使っていることが多いです。 ここでは、弊社が提供している小中学校向け保護者連絡配信サービス tetoru の利用している Ruby のバージョンを 3.2 から 3.3 にバージョンアップしたときの話を共有します。

概要

tetoru では、本文章の執筆時点で Ruby 3.3.4 + YJIT で Rails アプリケーションを動かしています。YJIT とは、Ruby が備えている Just-In-Time (JIT) コンパイラの機能で、これを有効化することで実行時に機械語が生成されアプリケーションの高速化につながります。YJIT に関する詳細についてはこちらの記事をご覧ください。 Ruby 3.2.2 + YJIT から Ruby 3.3.0 + YJIT にバージョンアップした時には多少レイテンシの改善が見られました。一方で、バージョンアップしてから tetoru の Rails アプリケーションを動かしている ECS のメモリ使用率が上がり続けてしまったため、これの改善対応を行いました。また、その後 Ruby 3.3.1 にバージョンアップしたタイミングで ECS メモリ使用率の改善が見られ、前述した改善対応を元に戻しデフォルトの設定で動かしています。

レイテンシの改善

Ruby 3.2.2 + YJIT から Ruby 3.3.0 + YJIT にバージョンアップした時に、全体の平均で 10% ほどレイテンシの改善が見られました。以下は tetoru 全体のレイテンシ(90パーセンタイル)です。黄実線において 24 日 12:00 過ぎあたりに Ruby 3.3.0 + YJIT にバージョンアップしています。点線は黄実線 1 週間前のレイテンシです。Ruby 3.3.0 + YJIT にバージョンアップした後、黄線が点線を下回っておりレイテンシが小さくなっていることが確認できます。バージョンアップするだけで10%も レイテンシを改善できるのはとてもありがたく、改めて Ruby に貢献している方々に感謝したいです。

メモリ使用率の悪化

リリース直後から tetoru の Rails アプリケーションを動かしている ECS のメモリ使用率が上がり続けるという事象が発生しました。

下図はその ECS メモリ使用率のグラフです(70% に位置する赤い点線は tetoru で定めているアラートの閾値です)。下図で 24 日 12:00 過ぎに Ruby 3.3.0 にバージョンアップしましたが、その後通常時のメモリ使用率を超えても上がり続けていました。実は、数ヶ月前にも一度 Ruby 3.3.0へのバージョンアップを試みたことがあるのですが、その時も同様にメモリ使用率が上がり続けていたため、一時的に Ruby 3.2.2 へ戻したことがありました。このため、今回のバージョンアップでは、Ruby 3.3 YJITのメモリ管理とRJITを参考にYJITが生成するコード量を小さくするため --yjit-exec-mem-size に 3.3.0 のデフォルト値 64MiB(ここを参照)より小さい値 32 MiB を設定し、また tetoru では Datadog を利用していますがYJITのコード生成サイズを示す code_region_size を Datadog で取得できるようにしたりと準備していました(YJIT関連のメトリクスは ddtrace のバージョン 1.13.0 から取得できるようです)。しかし、それでもなおメモリ使用率が上がり続けてしまっていました。

このため、更なる対策として Ruby 3.3.0 からデフォルトでオフになった Code GC をオンにすることを決めました。Code GC とはYJIT生成コードのサイズが --yjit-exec-mem-size に達すると全ての生成コードを破棄して以降呼ばれたメソッドを再度コンパイルし直すという機能で、Ruby 3.2 ではこれがデフォルトでオンでしたが、Ruby 3.3 からデフォルトでオフになりました。この Code GC をオンにする修正をリリースしたのが 24 日 18:00 ごろで、その後は Code GC がオフの時に比べてメモリ使用率の傾きが落ち着いたのが見て取れるかと思います(一部傾きが大きい時間帯もありますが)。

最終的にメモリ使用率が落ち着いたのは Ruby 3.3.1 へバージョンアップしてからでした。これをリリースしたのは 26 日 12:00 前で、これ以降はメモリ使用率が上がり続けるようなことはなくなりました。また、--yjit-exec-mem-size を小さい値に設定したり Code GC をオンにするといった対応を行っていましたが、Ruby 3.3 でのパフォーマンス改善の恩恵をより受けるために、その後 --yjit-exec-mem-size をデフォルト値に戻し、さらに Code GC もデフォルト値のオフに戻しましたが、メモリ使用率が極端に上がり続けるようなことはなくなりました。

ちなみに、なぜ Ruby 3.3.0 から Ruby 3.3.1 にバージョンアップしたことでメモリ使用率が安定したかについては、根本の原因については特定できていませんが、3.3.1 での修正を確認していると memory leak がいくつか修正されていたので、もしかするとそのいずれかが原因だったのではないかと推測しています。

所感

Ruby 3.3 YJITのメモリ管理とRJIT などの記事がすでに公開されており、メモリ使用率が上がった場合に取るべき手段をいくつか準備できていたのはとても心強かったです。改めてこのような情報を提供してくれる Ruby コミュニティに感謝したいと同時に、自分たちも Ruby に関する事例や知見などを積極的にコミュニティに還元したいという気持ちになりました。

また、今回のバージョンアップにあたって他社の事例を事前に調査していました。この調査をもとに、バージョンアップ時にメモリ使用率の増加が発生する可能性があるため増加した場合には(自分が調べた範囲では) --yjit-exec-mem-size を調整する、という手順を事前に準備していました。しかし、結果としてはこの予想は外れてしまい、前述の通り --yjit-exec-mem-size を調整してもメモリ使用率は落ち着かず追加の対応を行いました。このとき、改めて YJIT のドキュメントを見直して、Ruby 3.2 では有効化されていた Code GC を 3.3 でも有効化すれば、閾値を超えたタイミングで YJIT の生成コードが破棄される(パフォーマンスは多少犠牲にされることが予想されるが)ため結果としてメモリの使用率が抑えられるのではないかと仮説を立てこれを実行しました。このように、YJIT という難しい領域に対してもしっかりとドキュメント(場合によってはコード)を読み込めば仮説・実行し前進するためのアクションを行えると学びました。

最後に、この文章が Ruby 3.2 から 3.3 へアップデートする方の一助となれば幸いです。  

© 2020 Classi Corp.