Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Akatsuki Games Internship 2022のRuby on Rails / AWS コースに参加しました!

はじめまして!2022年の9/1~9/22の3週間、アカツキゲームスのサーバーサイドインターンに参加させていただいた伊藤といいます。この記事では、僕が取り組んだタスクや学んだことについて書かせていただきます!

自己紹介

豊田高専情報工学科4年の伊藤大輝です。普段は学校でコンピュータについて勉強していたり、コンテストやインターンなどで開発したりしています。

参加動機

普段できないような大規模開発をしたいと思ったのと、パフォーマンス改善についてのお仕事をさせていただけるというお話を事前に聞いていたので参加したいと思いました。

インターン中に取り組んだこと

インターンでやらせていただいたタスクは大きく分けて二つです。

  1. Ruby3.1 & Rails7.0.2へのアップデート
  2. YJITを有効化してのパフォーマンス調査

Ruby3.1 & Rails7.0.2へのアップデート

今回はRuby2.7系、Rails6.1系からの更新だったのでかなり変更点が多く、テストを全て通すことを目標として始めました。

Ruby3.1 で出会った変更点

Ruby3.0の変更点ですがキーワード引数と位置引数の分離というのがあります。

def foo(k: 1)
  p k
end

foo(k: 2) - > OK
foo({k: 40}) - > NG

Ruby3.0以前ではNGコードのようにキーワード引数を定義されたメソッドでもハッシュを引数に渡せば暗黙的にキーワード引数に変換されていましたが、それがなくなりました。このように明確に位置引数とキーワード引数を分ける変更がなされました。

Rails7.0.2で出会った変更点

Rails7.0.2にアップデートする上で出会った変更点はかなり多く全てを解説するのは時間が足りないのでリストアップするとともに一番詰まった点を紹介します。

  • MemcacheStoreの :raw value読み込みnilになる問題
  • ActionController::Parameter, CacheStore への保存できない問題
  • notice,alert Action Not Found Error (flash)
  • アプリケーションの initializer
  • 各種非推奨項目
  • redis-object
  • unicode-emoji
  • kosi
  • pry関連
  • mysql2
  • newrelic_rpm
  • oj

この中で僕が一番詰まったのは MemcacheStore の変更点です。

Rails.cache.increment(key, 1)
Rails.cache.read(key) - > 1

Rails.cache.increment(key, 1)
Rails.cache.read(key) - > nil

Rails6系ではMemcacheStoreに対する :raw value の書き込みでオプション raw: trueで読み込みしなくても値は返ってきたのですが、Rails7からは :raw valueの書き込みにはオプション raw: trueで読み込まないと nil が返ってくるという変更点がありました。しかしここまでの変更点についてCHANGELOGやReleae Noteで見当たらずかなり苦労しました。PRやDiscussionを探す中で見つかり修正に至ることが出来ました。

YJITを有効化してのパフォーマンス調査

Ruby3.1 から実験的な機能としてYJITが導入されました。Shopifyのベンチマークを見ると、本番Rails環境でもRubyインタプリタよりも約6%速くなっていることがあるとされています。ただし本番環境では必ずしも速くなるということはないとあり、実際のところどれくらい上がるのかということで負荷試験調査を行うことになりました。

YJITって何?

YJIT 説明 雑な図ですみません

今までのRubyの実行環境ではRubyのソースコードをRubyVMのためのバイトコードに翻訳して、それをRubyVM上のインタプリタで実行していました。YJITではバイトコードを生成した後、インタプリタで実行しつつ必要な時(Just In Time)にそれからさらに機械語を生成してコンピュータが直接実行するようになっています。

負荷試験結果

not YJIT

YJIT

画像はレスポンスタイムにおけるレスポンス数の分布図になります。YJITを無効化している方が低いレスポンスタイムを出していることがわかります。ただ有効化するだけでは高いパフォーマンスを出すことはおろか無効化している時よりも性能が劣化してしまいました。ここで僕はドキュメントを読み込み、--yjit-exec-mem-sizeが小さいのではと思いました。--yjit-exec-mem-size はYJITが生成した機械語を格納しておく場所のサイズを指定するためのオプションです。YJITには生成された機械語に対してのGCがありません。つまり今回の問題はサーバを実行している間に --yjit-exec-mem-size の値が足りずコード生成が止まってしまうためだと考えました。そして --yjit-exec-mem-size の値をデフォルトよりも大きくして実行しました。すると、さらなる性能劣化が見られました。

パラメータ別実行結果

上記の結果を受けて他のパラメータで実行することにしました。

128MiB

512MiB

900MiB

上記の図から--yjit-exec-mem-sizeの値が大きいほど性能劣化が確認できるかと思います。僕の仮説は間違っていました。そもそも上記の仮説を踏むと性能劣化する原因自体を特定することは出来ません。機械語生成が止まれば通常のインタプリタでの実行に移るだけなので性能劣化することはないからです。そしてここでさらにドキュメントを読み込むとこの現象のヒントとなる情報に出会いました。「YJIT のコード生成は特定の処理を10回実行すると行われるが、予期しないコードやサポートしていないコードにあうと、インタプリンタに戻り通常のインタプリタより遅くなる」この情報を正しいと仮定すると、上記の各パラメータごとの性能評価を説明することができると考えます。なぜ --yjit-exec-mem-size の値がより大きいとより性能劣化が見られるのかというと、あくまで予想ですが、より値が大きい方が生成できる機械語が多くなり、上記の実行が遅くなる問題に直面し続けるからだと思います。

なぜYJITで性能劣化が見られたのか考察

上記の結果を踏まえると性能劣化の原因は、プロジェクト内のYJITにとって予期しないあるいはサポートしないコードにあると考えます。そのコードを起因としてYJITインタプリタ開始終了問題に直面して性能劣化が見られるのではないかと思いました。

やり残したこと

考察の裏付けまでやり切ることが出来ませんでした。YJITのデバック機能を使い最後の原因の特定まで行いたかったです。

学んだこと

  • 詰まったら自分の中に溜め込みすぎず、他の人に聞いてみる
  • 説明しやすい文を作ることは難しい
  • ドキュメントを読み正しい情報の理解を得ることは難しい
  • times chは素晴らしい

感想

今回は個人や小さいプロジェクトでは中々関わることのない大規模ソフトウェアの言語アップデートと負荷試験調査というタスクをさせていただきました。ドキュメントを読みながら落ちているテストのエラーと照らし合わせたり、負荷試験を回してnew relicと睨めっこしたりと新鮮なことばかりでとても楽しかったです!またyasu-sanをはじめとしたサーバチームの方々には詰まった際にサポートしていただきました。そのおかげでスムーズに作業が進んだと思います。ありがとうございました!