行動すれば次の現実

ほどよくモダンなシステム開発を目指しています。メインテーマは生産性、Ruby、Javascriptです。

HerokuのR14エラーをScout APMで対策する方法 | Rails

Herokuではメモリ使用量がプランの上限を超えるとR14エラーが発生します。 R14はスワップメモリが発生していることを意味しており、発生するとアプリケーションの処理速度が著しく低下してしまいます。

このままメモリ使用量が200%を超えてしまうと、R15エラーが発生してdynoが強制再起動されてしまいます。 このような自体にならないためにもR14エラーが発生した場合は、早急に原因を特定して改善する必要があります。

Scout APMでRailsのパフォーマンスチューニング

スワップメモリを発生させないためにはメモリ使用状態をモニタリングする必要があります。 メトリクス監視サービスとしてNewRelicなどが有名所ですが、筆者はScout APMをオススメします。

NewRelicの有料プランを使用していたのですが、以下の点が不便であると感じていました。

  • 多言語をサポートしているためフレームワーク固有の情報が取得できない
  • 多機能すぎて使いこなすのが難しい
  • サーバー単位でのメトリクスは詳細に把握できるが、どこのURLのアクションが問題を起こしているのかが把握できない

その点、Scout APMはRailsに特化しているので上記の問題点がすべて解消されます。 さらに、Githubと連携することでコード単位での分析も可能となり、ピンポイントに原因を特定することが可能になります。

以下に、R14エラーが発生した際の原因特定からパフォーマンスチューニングまでの流れを説明します。 R14エラーに悩まされている方はぜひ参考にしてみてください。

STEP1. メモリ使用量が大きいアクションを特定する

R14エラーを検知したらScout APMのDashboardから原因のアクションを特定します。

下記画像(Scout APM Dashboard)のようにMEMORYチャートを表示させて、発生した時間帯にシークバーを設定します。 設定すると「Largest Memory Increase」というウィンドウにメモリ使用量の大きい順でアクション情報が出力されます。

StoreLayouts::PrintsController#indexが41MBのメモリを使用していることが確認できます。 このアクションが原因であることが確認できます。

f:id:furu07yu:20211002164610j:plain
Scout APM Dashboard

STEP2. トレース情報から問題のあるコードを特定する

StoreLayouts::PrintsController#indexの箇所をクリックすると、より詳細なトレース情報が表示されます。 下記画像(Transaction Traces)を参照。

f:id:furu07yu:20211002165901j:plain
Transaction Traces

Transaction Tracesの該当アクションをクリックすると、実行された処理がタイムライン形式で表示されます。 下記画像(Memory Allocation)を参照。

「Memory Allocation Breakdown」タブを選択するとメモリーアロケーションの回数などが表示されます。 また、N+1が発生した場合はその発生回数が表示されますので、問題のありそうなコードがおおよそ把握できます。

f:id:furu07yu:20211002193641j:plain
Memory Allocation Breakdown

backtraceというラベルをクリックするとGithub上のコードが表示されます。 下記画像(backtrace)を参照。

f:id:furu07yu:20211002171018j:plain
backtrace

N+1が多く発生している箇所などを中心に問題のある箇所がないかをチェックしていきます。

R14エラーの場合、N+1以外にも、ActiveRecordのfind時に無駄にincludesしていることで徐々にメモリリークになるケースも多いので、そのような箇所もチェックしていきます。

Scout APMで調査できることはここまでです。 後は上記の調査結果からソースコードの当たりをつけてパフォーマンスチューニングを実際に行っていきます。

STEP3. 実際にパフォーマンスチューニングを行う

ソースコードの当たりがついたらメモリを測定します。

RubyにはObjectSpace.memsize_of_allというメソッドがありますので、それを使用します。 application_controller.rbなどの共通参照できる箇所に以下のようなメソッドを定義しておきます。

def log_memory(label)
    puts '--------'
    puts "--------#{label}"
    puts "#{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"
    puts '--------'
    puts '--------'
  end

実行するとMB単位でメモリ使用量がコンソール出力されますので、メモリを多く使用していると思われる処理の前後に仕込んでおき、メモリ使用量を測定します。

log_memory('before method_a')
method_a
log_memory('after method_a')

メモリ使用量過多の箇所が特定できれば、後は適宜実装を改善した上で改めてメモリ使用量を測定します。

メモリの使用量が改善されればパフォーマンスチューニングは成功です。