💎

Ruby3.4.1+YJIT本番運用を開始し、20%のパフォーマンス向上を達成しました

に公開

はじめに

ご無沙汰しております。LRMでエンジニアをしている大場です。

先日、弊社が開発しているセキュリティ教育SaaS「セキュリオ」のRailsアプリケーションにYJITを導入し、本番環境での運用を開始しました。結果として大きくパフォーマンスを向上させることに成功しましたので、YJIT導入手順とその結果についての記事をお届けします。

YJIT導入時のRuby、Railsのバージョンは以下の通りです。
・Ruby 3.4.1
・Rails 7.1

概要

YJITはJITコンパイラの一種で、コード実行の速度向上を図るものです。下記の記事でも触れているので参考にして頂ければ幸いです。

https://zenn.dev/lrm/articles/5ab63eff5360ab

YJITの導入事例はここ数年で増えています。調査した内容は参考文献に記載しました。

導入結果に関して調査したところ、ほとんどの企業でパフォーマンスが向上していました。しかし、導入前後のサーバーのリソース消費量変化には結果にばらつきがありました。

多くの場合、メモリ消費量が若干増加していましたが、著しく増加した例もあり、中にはむしろメモリ使用量が減少したという例もありました。導入にはサーバーのリソース消費に配慮が必要そうです。

今回は、リソース消費に配慮した導入手順と具体的な取り組み、実際の導入結果に触れます。

作業手順

まずは、config/initializers/yjit.rbに以下の変更を加えました。

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

YJITのスタッツに関するログ出力が不要であれば、(stats:true)は不要です。今回はローカル環境、検証環境、本番環境全てでログを確認するため、一時的に付与しました。

実際のログ出力処理はapplication_controller.rbに追記しました。

class ApplicationController < ActionController::Base
  before_action :logging_yjit_stats

・・・  省略   ・・・

  def logging_yjit_stats
    return if defined?(RubyVM::YJIT).nil? || !RubyVM::YJIT.enabled?

    # TODO: 本番環境では1/5000の確率でYJITの統計情報をログに出力
    # return unless Random.rand(5_000) < 1

    stats = RubyVM::YJIT.runtime_stats
    code_region_size, yjit_alloc_size, ratio_in_yjit = stats.values_at(:code_region_size, :yjit_alloc_size, :ratio_in_yjit)
    total_memory_usage = code_region_size + yjit_alloc_size
    Rails.logger.info(" [YJIT]: code_region_size: #{code_region_size}, yjit_alloc_size: #{yjit_alloc_size}, ratio_in_yjit: #{ratio_in_yjit}")
    Rails.logger.info(" [YJIT]: total memory usage: #{total_memory_usage.to_f / 1024 / 1024} MB")
  end
end

変更を加えた後にローカルで動作確認を行い、出力されたYJITのスタッツを確認しました。

I, [2025-06-09T11:59:37.012105 #7]  INFO -- : [192.168.65.1]  [YJIT]: code_region_size: 46084096, yjit_alloc_size: 40567943, ratio_in_yjit: 97.02192876498957
I, [2025-06-09T11:59:37.012992 #7]  INFO -- : [192.168.65.1] total memory usage:85.64040088653564 MB

JIT領域のコミッターであるk0kubunさんのブログYJITのREADMEによると、YJITのカバー率ratio_in_yjitは99%以上が推奨とのことでした。ローカルでの動作確認時では1000~1500リクエスト近くを処理させて97%でした。

97%到達時点でメモリ使用量は85MB程度だったためYJITに割り当てるメモリ使用量を指定できる--yjit-exec-mem-sizeはデフォルトの128MiBのままで、カバー率は理想値の99%に到達すると判断しました。

次に、インフラ側のリソース使用状況も確認しました。

調査時点では2GB割り当てたうちの35%である700MB程度を使用しており、未使用のメモリが1300MB程度ありました。このことから、YJITが追加で128MiB(134MB)を消費しても問題ないと判断しました。導入後のメモリ使用率は134÷2000=0.067の式より約7%増加する予想を立てました。

また、慎重を期すためにRailsサーバーのプロセスを実行しているコンテナのリソース消費を直接確認しました。具体的には、ECSタスクによって起動したrails serverのプロセスを実行中のコンテナ内でfree -mコマンドによる確認と/sys/fs/cgroup/memory/memory.statの確認を行いました。

$ aws ecs execute-command --cluster prd-seculio --task <タスクID> --container rails-puma --interactive --command "free -m"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-aiqiiljysnr4qddloj5k5arpny
               total        used        free      shared  buff/cache   available
Mem:            3850        1274         127           0        2770        2615
Swap:              0           0           0
{
  "read": "2025-06-10T04:41:26.290527762Z",
  "preread": "2025-06-10T04:41:16.290947033Z",
  "pids_stats": {},

    ・・・   省略   ・・・

  "memory_stats": {
    "usage": 710815744,
    "max_usage": 711073792,
    "stats": {
      "active_anon": 0,
      "active_file": 10121216,
      "cache": 44199936,
      "dirty": 0,
      "hierarchical_memory_limit": 2147483648,
      "hierarchical_memsw_limit": 9223372036854772000,
      "inactive_anon": 635019264,
      "inactive_file": 34263040,
      "mapped_file": 4866048,
      "pgfault": 369303,
      "pgmajfault": 66,
      "pgpgin": 299838,
      "pgpgout": 133938,
      "rss": 635019264,
      "rss_huge": 0,
      "total_active_anon": 0,
      "total_active_file": 10121216,
      "total_cache": 44199936,
      "total_dirty": 0,
      "total_inactive_anon": 635019264,
      "total_inactive_file": 34263040,
      "total_mapped_file": 4866048,
      "total_pgfault": 369303,
      "total_pgmajfault": 66,
      "total_pgpgin": 299838,
      "total_pgpgout": 133938,
      "total_rss": 635019264,
      "total_rss_huge": 0,
      "total_unevictable": 0,
      "total_writeback": 0,
      "unevictable": 0,
      "writeback": 0
    },
    "limit": 9223372036854772000
  },
   ・・・   省略   ・・・

1つ目のfree -m実行結果は1274(userd) / 3854(total) =0.331となりました。メモリ消費率が約33%となっていることがわかります。

2つ目の/sys/fs/cgroup/memory/memory.statの値も712964571(usage) / 2147483648(hierarchical_memory_limit) =0.332で約33%であり、
実際の測定でもメモリ使用率はAWS上のコンソールとほぼ同じ値が得られました。

コンソール上はスケールアウトした後の全コンテナの平均値が表示されています。そのため1つのコンテナ内で取得したメモリ使用率とコンソール上で閲覧できるメモリ使用率に若干の差があります。調査時は4台までスケールアウトしていたため4台全てでメモリ使用率を測定し、コンソール上の平均値とほぼ同値であることを確認しました。

上記変更と調査を行い、実際にYJITでの本番環境運用を開始しました。

運用結果

レスポンス速度

導入前2週間と導入後2週間でのレスポンス速度を計測し、以下の通りになりました。

1枚目のグラフがp90(90th percentile)で、2枚目がp50(50th percentile)です。どちらも濃く着色された折れ線が導入後の数値になります。

導入前 導入後
p90 98.2(ms) 81.5(ms)
p50 18.8(ms) 16.5(ms)

p90の平均値は、導入前が平均98.2(ms)だったのに対して、導入後は81.5(ms)となりました。
割合にして約20%近くレスポンス速度が向上しています。また、全日で導入後の方がレスポンス速度が早く、顕著に差が出ました。

p50の平均値は導入前が18.8(ms)で、導入後が16.5(ms)となりました。こちらも約13%程レスポンス速度が向上しています。

実際に、ホーム画面のレスポンス速度は下記グラフの通りに変化しました。

6月18日のYJIT導入を境にレスポンス速度が改善されていることがわかります。

YJITカバー率

メモリ使用量100MBほどで、YJITのカバー率は目標としていた99%に到達していました。
導入前の試算から乖離することなく、狙い通りの挙動になりました。

I, [2025-06-18T17:17:03.636129 #8]  INFO -- : [YJIT]: code_region_size: 31096832, yjit_alloc_size: 52412181, ratio_in_yjit: 98.34052255946375
I, [2025-06-18T17:17:03.637203 #8]  INFO -- : [YJIT]: total memory usage: 79.64040088653564 MB
I, [2025-06-18T17:34:11.245573 #8]  INFO -- : [YJIT]: code_region_size: 33193984, yjit_alloc_size: 55700037, ratio_in_yjit: 98.54103535795394
I, [2025-06-18T17:34:11.245777 #8]  INFO -- : [YJIT]: total memory usage: 84.77594470977783 MB
I, [2025-06-18T19:19:13.931748 #8]  INFO -- : [YJIT]: code_region_size: 37072896, yjit_alloc_size: 62140809, ratio_in_yjit: 99.08002440102673
I, [2025-06-18T19:19:13.931854 #8]  INFO -- : [YJIT]: total memory usage: 94.61756229400635 MB
I, [2025-06-19T03:00:10.293765 #8]  INFO -- : [YJIT]: code_region_size: 38858752, yjit_alloc_size: 64812521, ratio_in_yjit: 99.1960748272391
I, [2025-06-19T03:00:10.293938 #8]  INFO -- : [YJIT]: total memory usage: 98.86863040924072 MB
I, [2025-06-19T08:30:29.143663 #8]  INFO -- : [YJIT]: code_region_size: 39399424, yjit_alloc_size: 65636670, ratio_in_yjit: 99.21875472489396
I, [2025-06-19T08:30:29.143763 #8]  INFO -- : [YJIT]: total memory usage: 100.17022514343262 MB

メモリ使用率

6月18日の17時に変更を入れ、メモリ消費率は7~8%増加しました。導入前は30%台前半だったメモリ消費率が、導入後は40%前後で推移しています。

19日~22日で一時的にメモリ消費率が急増していますが、該当日にアプリケーション内で非常に重い処理が繰り返し実行された履歴をNew Relic上で確認しています。このメモリ消費率の急増はYJITの導入起因ではありませんでした。

YJIT導入によるメモリ消費率の増加は、概ね事前の見積もりに準じた結果となりました。

おわりに

調査段階ではメモリ消費量の急増など、いくつか懸念点があったためかなり慎重に作業を進めました。
良い結果となって非常に満足しています。Rubyのメンテナー、コミッターの方々に感謝を忘れず、引き続きRubyの最新動向をキャッチアップおよび実践していきたいと思います。

参考文献

https://tech.timee.co.jp/entry/2025/03/26/132435
https://techblog.zozo.com/entry/effects-and-considerations-of-ruby3-yjit-on-wear
https://tech.smarthr.jp/entry/2025/03/28/123020
https://zenn.dev/dely_jp/articles/094f3084e9fa2c
https://zenn.dev/pharmax/articles/add57693adf021
https://tech.medpeer.co.jp/entry/2024/07/05/165000
https://k0kubun.hatenablog.com/entry/ruby-3-3-yjit
https://docs.ruby-lang.org/en/3.4/yjit/yjit_md.html#:~:text=--yjit-mem-size%3DN%3A soft limit on YJIT memory usage in MiB (default%3A 128). Tries to limit code_region_size %2B yjit_alloc_size

LRM Tech Blog

Discussion