Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

10年もののSaaSをRuby 3.3 にバージョンアップ + YJITを有効にしたら何もしなくてもパフォーマンスが向上した話

こんにちは、クラウド経費 テックリードの宮村(みやむー)@miyamura.koyo です。

10年選手になってきたRails製SaaSをメンテしています。以下の通り継続的にRuby / Railsのバージョンアップを実施しています。

moneyforward-dev.jp

moneyforward-dev.jp

今回はその続編のRuby 3.3のバージョンアップと、これまでバージョンアップを頑張ってきたご褒美とも言える、YJITの有効化によるパフォーマンス向上についてお話しします。

YJIT

Ruby 3.1からYJITという技術が導入されRubyのランタイム性能が向上しています。

www.ruby-lang.org

Railsを使って開発している他社のプロダクトも、YJITによる恩恵を受けて性能向上がなされたという報告を上げています。

product.st.inc

tech.smarthr.jp

Ruby 3.3では RubyVM::YJIT.enable が追加されており、実行時にYJITを有効にできるようになり、また改善も加えられています。

Ruby 3.3 バージョンアップ

我々のアプリでは後述のように、テストのためにステージング環境(以下、stg)のみ先にYJITを有効にしたいと考えました。そのためには、実行時にYJITを有効にする必要がありました。そのため、先にRuby 3.3のバージョンアップを行いました。

なおRuby 3.3へのアップデートでは、特にアプリケーションの修正は不要でした。 CRE(Customer Reliability Engineer)チームであるガーディアングループが定期的にDependabotを更新してくれているおかげで、特にgem周りでも問題は発生しませんでした(いつもありがとうございます)。

ということで難なくリリース完了しました。この時点ではYJITは有効化しておらず、無効化しています。

YJIT の有効化

次にYJITを有効にします。Rails 7.1か、Rails 7.2以降かで方法が異なります。

Rails 7.2 以降の方法

Rails 7.2では config/application.rb に以下を記述することで有効にできます。

Rails.application.config.yjit = true

なおRails 7.2ではデフォルトで有効なので config.load_defaults '7.2' にしていれば特に作業は必要ありません。

Rails 7.1 の方法

Rails 7.1では以下のようなコードを config/initializers 配下のファイルに記載します。例えば config/initializers/enable_yjit.rb に以下のような記述をします。

  if defined? RubyVM::YJIT.enable
    Rails.application.config.after_initialize do
      RubyVM::YJIT.enable
    end
  end

なおこちらのコードはGitHub上のrails/railsのコードを参考にしています。 github.com

今回YJITを有効にした段階ではRails 7.1だったため、こちらの手法を採用しました。

まず stg で有効にする

さて、YJITはランタイムを変更するため、原理上は全ての動作に影響する可能性があります。どのように品質保証をすればいいでしょうか?

我々のプロダクトでは「stgのみ有効化して数週間監視する」戦略を取りました。ランタイム中に以下のようにstg環境のみで有効にするようなコードを記載しました。

これがランタイムでYJITを有効にできる強みです。ビルド時に有効にするかを決定しないといけない場合、prodとstgでコンテナを分ける必要があり、やや煩雑になってしまいます。

Ruby 3.3にしてからYJITを有効にすることで、より安全な戦略を選択できます。

if HogeEnv.stg? # ここは各プロダクトの環境判定のメソッドを用いる
  if defined? RubyVM::YJIT.enable
    Rails.application.config.after_initialize do
      RubyVM::YJIT.enable
    end
  end
end

stgで数週間有効にしましたが、特にエラーなども発生しませんでした。そのため、別途本番環境で有効にしてリリースしました! 10年もののプロダクトでも、ついにYJITが有効に!

YJIT の効果測定

さてYJITを有効にしたので、続いて効果測定をしました。効果計測にはDatadogを用いました。 プロダクトの性質上、月初付近に負荷のピークが発生します。今回は月初第一営業日を選択しました。 測定方法としては、YJIT有効化前後の2つのピークタイムにおけるHTTPリクエストのレイテンシを比較することとします。

また、今回は比較用のDatadogのダッシュボードを用意し、プロダクトの他のエンジニアやマネージャーもより改善を実感しやすい形式で比較しました。

測定結果

Datadogの rack.request メトリクスを用いてピークタイム1日におけるP95レイテンシをグラフにしました。 灰色の点線のグラフが改善前、青色の実線のグラフが改善後のグラフを表します。

1日全域に渡って大きく改善されていることが視覚的にわかります。

Datadog の結果

また、各指標について、ピークタイム1日の平均を取って改善率を計算しました。

((before - after) / before) * 100

その結果は以下です。なお小数点第二位を四捨五入しています。

指標 ピークタイム1日の改善率の平均
p99 17.0%
p95 21.3%
p90 22.4%
avg 22.6%

上記のように数値的にも改善されたことがわかります。

CPU / メモリ 使用率の変化

また、別途、各PodのCPU / メモリ使用率の増加も比較しましたが、こちらは大きな変化が見られませんでした。 YJITを有効にすることでこれらの数値が増加することはなく性能向上が達成できました。

まとめ

Ruby 3.3, Rails 7.1でYJITを有効にすることで20%前後の性能向上が達成できました。 すでに有効にしてから数ヶ月以上経過していますが、Ruby 3.3バージョンアップや、YJITを有効にしたことに起因する問題は発生していません。 つまり何もしなくても約20%の性能向上ができた、ということです。 我々の提供しているアプリケーションは多くの機能を備えていますが、それらに機能的な変更を加えずに、全体的な高速化が達成できたというのは非常に大きな価値です。 これまでRuby / Railsのバージョンアップを頑張ってきた甲斐がありました! 同時にYJITを開発したRubyコミュニティの方々に改めて感謝を申し上げます。

リスクが低くリターンの大きい、Ruby 3系の目玉機能であるYJITをぜひ皆さんも有効化してみてください。

おまけ

今回のプロジェクト中にYJITにまつわるSlackスタンプが多く作られました。

YJITをじっと待つ開発者

YJITにかけたダジャレその1

YJITがリリースされてわいわいする開発者

YJITにかけたダジャレその2


マネーフォワード福岡開発拠点では、技術の進歩を多くのユーザーに還元したいエンジニアも募集しています!

hrmos.co

福岡開発拠点のサイトもあるのでぜひご覧ください!

fukuoka.moneyforward.com