SmartHR Tech Blog

SmartHR 開発者ブログ

SmartHR最大のRailsアプリケーションをRuby 3.4(+YJIT)にアップデートしました

こんにちは、SmartHR プロダクトエンジニアの B6 です。 「SmartHR 最大の Rails アプリケーションで YJIT を有効化しました」の記事を投稿してから 5 ヶ月ほど経ちました。 その後、「基本機能」と呼ばれる SmartHR 最大の Rails アプリケーションでは Ruby、Rails のアップデートを行いました。 本記事では、YJIT を主題に、Rails・Ruby アップデートと YJIT によるパフォーマンス変化について共有いたします。

アップデート後の環境

アップデート後の Ruby、Rails のバージョンは以下の通りです。

  • Ruby 3.4
  • Rails 7.2

Rails アップデートによる YJIT 有効化方法の変更

Rails は、以前の記事執筆時点の 7.1 から、7.2 にアップデートしました。 Rails 7.2 では Ruby 3.3 以降を使用している場合、YJIT がデフォルトで有効になっています。 そのため、本アプリケーションでも Rails 7.2 へのアップデートに合わせて、以前使用していた RubyVM::YJIT.enableを用いた明示的な YJIT 有効化のコードを削除しました。 次のコードを削除しています。

Rails.application.config.after_initialize do
  RubyVM::YJIT.enable(stats: true)
end

Ruby アップデート

Rails のアップデートに続いて先日、Ruby を 3.3 から 3.4 にアップデートしました。 Ruby 3.4 では、YJIT に関して改善や変更が追加されています。 YJIT 3.4: Even Faster and More Memory-Efficientによると、YJIT 3.4 は YJIT 3.3.6 と比較して、ベンチマークで 5~7%高速化するとのことでした。

変わらないメトリクスと、 load_defaults 未設定による YJIT 無効化

Ruby 3.4 へのアップデートをリリース後、レスポンスタイムやメモリ使用量を確認したところ、特にメトリクスに変化が見られませんでした。 おかしいと思い詳しく調査したところ、YJIT が本番環境で有効になっていないことが判明しました。

> Rails.env
#=> "production"

> Rails.configuration.yjit
#=> false

原因は、アプリケーションにload_defaults を設定していなかったことにありました。 load_defaultsは、ターゲットとなるバージョンを指定することで、そのバージョンで Rails が提供しているデフォルトの設定値をアプリケーションに適用できるものです。

> Rails.configuration.loaded_config_version
#=> nil

Rails::Application::Configurationのソースコードを確認すると、 config.yjitの設定は、初期化時点では false で、load_defaults7.2の場合のみ明示的にtrue に上書きされています。

load_defaults が設定されていなかったのは、プロジェクトの歴史的な経緯によるものです。 私たちの開発チームでは、Rails のアップデートに際して、既存の設定値を維持することで影響範囲を最小限に抑える方針を採っています。 とはいえ、load_defaults が設定されていない状態はあまり望ましいものではありません。現在は、設定値をひとつずつ確認しながら、段階的に Rails 7.2 のデフォルト設定に追いついていく方針で移行作業を進めています。

いずれにせよ、結果として私たちのアプリケーションでは、数ヶ月前に Rails のアップデートを行った時点で YJIT が無効になってしまい、その恩恵を受けられない状態になっていました。 実際に、Rails アップデートした日に遡って、前後でのレスポンスタイム(p50)を確認すると、以下のグラフのように変化が見られます。 Rails アップデートを行った日付を境に、グラフの概形からもパフォーマンスの劣化が見て取れる結果となっていました。

YJITが無効になる前後でのp50のレスポンスタイムグラフ
YJITが無効になる前後でのp50のレスポンスタイム

YJIT 再有効化によるレスポンスタイムの改善、メモリ使用量の増加

原因が判明したため、config.yjit = trueを明示的に設定することで、再度 YJIT を有効化しました。

24 時間のレスポンスタイムの p90(90-percentile)・p50(50-percentile)・平均を 1 週間前のデータと比較した結果です。 それぞれ実線が YJIT 有効化後のデータ、点線がその 1 週間前のデータを表しています。 前述した理由から、以下のデータは Ruby 3.4(YJIT 無効)と Ruby 3.4(YJIT 有効)での比較であることに注意してください。

p90、p50、平均のレスポンスタイムのグラフ
p90、p50、平均のレスポンスタイム

メトリクス YJIT 有効化前 YJIT 有効化後 変化率
p90 960 ms 719 ms -25.1%
p50 211 ms 180 ms -14.7%
平均 426 ms 355 ms -16.7%

全体として、YJIT を有効化することでレスポンスタイムが明確に改善されたことが確認できました。

参考までに、Ruby 3.3 時代に YJIT を有効化していた際の数値も以下に再掲します。 ただし、Ruby 3.3 で YJIT を利用していた時期から数ヶ月が経過しており、その間にトラフィックの傾向やアプリケーション全体のベースとなるレスポンスタイムが変化しているため、当時との厳密な比較は難しい状況です。

メトリクス YJIT 有効化前 YJIT 有効化後 変化率
p90 656 ms 594 ms -9.47%
p50 164 ms 133 ms -18.90%
平均 305 ms 288 ms -5.57%

今回の Ruby 3.4 での YJIT 有効化は、p90・平均で以前より大きな改善幅が見られ、効果がよりはっきり表れている印象です。 一方で、全体的なレスポンスタイムは当時と比べてやや悪化しているようにも見受けられます。 背景にはトラフィックの変化やアプリケーションの機能追加など、さまざまな要因が関係している可能性がありますが、本記事では詳細な分析は行っていません。

まとめ

Rails 7.2 へのアップデートに際し、load_defaults を設定していなかったことが原因で、意図せず YJIT が無効化されるという事態が発生していました。 その状態に気づかず運用を続けたため、パフォーマンスが劣化した状態が数ヶ月間続きました。また、Ruby 3.3(+YJIT) を利用していた当時との正確な比較も難しくなってしまいました。 本来であれば Rails アップデート後、設定値を確認しながらload_defaultsのバージョンを順次上げていくのが理想だと思います。 現在は設定値を確認しながら、load_defaults のバージョンを段階的に上げる作業を進めています。

今回、YJIT を再度有効化したことで p90 で 25.1%、p50 で 14.7%、平均で 16.7% と、レスポンスタイムには大きな改善が見られました。 これにより、改めて JIT によるパフォーマンス改善効果を実感することとなりました。

数ヶ月前の記事で、RubyKaigi 2025 を目前にして「どのようなものになるのか楽しみです」と書いていた ZJIT が、現在では Ruby 本体にマージされており、開発が進んでいます。 JIT の導入によるパフォーマンス改善の大きさを考えると、ZJIT の今後の動向にも引き続き注目せずにはいられません。 日々開発を支えてくださっている Ruby コミッターの方々への感謝とともに記事を締めくくりたいと思います。

We Are Hiring!

SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です! 少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!

hello-world.smarthr.co.jp