こんにちは。サーバーグループ エンジニアの山田です。
サーバーグループの仕事の一つにアプリケーションのパフォーマンス改善があります。
今回は普段行っているRailsアプリケーションのパフォーマンス改善の流れについて紹介します。
遅い処理を見つける
前提として遅い処理、遅くなった処理を知る必要があるので、APMなどを使って確認します。
弊社のRailsアプリケーションではNew Relicを使用しているためその画面で説明していきます。APIのレスポンスタイムの改善を行う場合はまず以下を確認することが多いです。
- Transactions > Most time consuming
- Transactions > Slowest average response time
Transactions > Most time consuming
リクエスト数上位のAPI(コントローラのアクション)。この上位を改善できると効果が大きい。
Transactions > Slowest average response time
平均レスポンスが遅い上位API。一概には言えないがレスポンスに時間がかかっているためユーザー体験が悪くなっている可能性が高い。
改善対象のAPIを決める
上に挙げた二つを中心に見て、効果が大きくユーザー体験も改善されるであろうAPIを対象にしていきます。
いつ確認しているか
現状はチームでのNew Relic確認は週次のスプリントイベント内で見るようにしています。ただし毎回必ず見ているわけではなく、以下のような状況の時に特に重点的に確認しています。
- 突発的なアクセス数の増加(例えばコロナの影響)やパフォーマンスに影響を与えそうな修正を入れた時など
- パフォーマンスに関わる問題発生が多くなっている時
以前は必ず毎回のスプリントイベントの中で見るようにしていました。しかしパフォーマンス面の変化が起こることは少なかっため、全員で見る頻度は少なくしました。
ボトルネックとなっている処理の調査
ここからは最近行った改善を例に説明していきます。 Studyplusアプリの中心である勉強を記録するAPIが、想定よりも時間がかかっていることがわかったためその改善を行っていきました。
New Relicの Transaction trace > Trace details で詳細を確認すると
MySQL StudyRecord find
からMySQL StudyRecord create
の間で2秒以上かかる場合があるとわかりました。ここを中心に調査していきます。
ローカル環境で確認する
パフォーマンス問題はデータ量に起因している場合が多いため、本番以外の環境で再現できない場合は多いです。しかし何からしら解決の手掛かりがないかローカルの環境で確認します。
何をどうやって確認するかはその時によって様々で、ソースコードを見てすぐに原因がわかる時もあればそうでない時もあると思います。
今回は rack-lineprof というGemを使って調査を行いました。 時間がかかっているであろう処理の周辺で確認をしていきます。
10.4ms 2 | 135 record.study_unit = bookshelf_entry.study_unit | 136 end | 137 9.2ms 1 | 138 record.save! 84.4ms 1 | 139 record.create_event! | 140 .......
結果としてNew Relicで測定される結果ほど顕著に時間がかかっている箇所は見つけられなかったため、より詳細を確認できるように本番にログを仕込む方向にしました。
ログを仕込む
New RelicのMethod tracers を使えば簡単にメソッドのトランザクション内の時間を計測して、New Relicで表示することができるのでそれを使っていきます。
今回の例では、リクエスの中で呼ばれているStudyRecordというモデルの以下のメソッドを計測するようにしました。
- study という独自に作成したクラスメソッド
- save! というActiveRecrdのモデルオブジェクトをDBに保存にするインスタンスメソッド
+ require 'new_relic/agent/method_tracer' class StudyRecord < ApplicationRecord + include ::NewRelic::Agent::MethodTracer # save! メソッドをトレースする。 New Relic上は Custom/study_record_save という名前で表示する + add_method_tracer :save!, 'Custom/study_record_save' # study というクラスメソッドをトレースする。 New Relic上は Custom/study_record_study という名前で表示する class << self + include ::NewRelic::Agent::MethodTracer + add_method_tracer :study, 'Custom/study_record_study' end end
ログを仕込んだ結果
先ほどのTrace detailsよりもどのメソッドで時間がかかっているかが詳細に見れるようになりました。StudyRecord#save!
を実行してcreateのSQLの実行が完了するまでに時間がかかっていることが明確になりました。
また、SQL実行時間を確認するとcreateのSQL(insert)は時間がかかっていませんでした。そのため直前の処理が原因であるとわかります。
原因特定と修正
save!
の内で重そうな処理は、バリデーション以外にはなさそうだったためそこを中心に確認しました。
本番と同等のデータで確認した結果、あるバリデーションの処理に時間がかかっていることがわかりました。
そのバリデーションは特定の条件で実行すればよいが、必要ない場合も無条件で実行するようになっていました。
そのため条件を満たす場合に実行するという1行の修正を加えました。
効果測定
修正した後に本当に速くなっているか、どれぐらい速くなっているかの測定を行います。
今回の修正では対象のAPIの全てのリクエストが改善できたわけではないため、修正前後の1週間分の平均レスポンスタイムで確認しました。
平均レスポンスタイム | |
---|---|
修正前 | 0.88 sec |
修正後 | 0.33 sec |
無事にレスポンスタイムが短くなっていることを確認できました🎉
最後に
例を交えてRailsアプリケーションのパフォーマンス改善の流れについて紹介しました。 普段どのような流れで行っているかが伝われば幸いです。
個人的にパフォーマンス改善の仕事は、可能性を絞って原因を特定していく感じが問題を解くような楽しさがあり好きな仕事の一つです。 今後も継続的に改善を行っていきたいです。