Copilotのメモリリーク問題
(stevenharman.net)ActiveSupport::Notifications に関連するメモリリーク解決プロセスの整理
-
メモリリークが発生した状況
- ある時点から
webDyno のメモリ使用量が異常に増加し始めた - Pager が鳴り始め、メモリリークと思われる状況が発生
- ある時点から
-
即時対応
- Heroku ではメモリリークが疑われる場合、Dyno を再起動することで一時的に解決可能
- 通常の deploy サイクルに合わせて restart するか、メモリ上限に近い Dyno を手動で再起動
-
原因特定のための疑わしいコードの確認
- memory spike 直前にデプロイされたコード変更を確認
- 原因として疑われるいくつかのコードを一つずつデプロイし、メモリリーク発生の有無を確認
- 原因らしいコードが見つからず、tooling の変更もロールバックして確認したが、メモリリークは継続
-
メモリ増加パターンの分析
- リークは
webDyno でのみ発生。Sidekiq、Delayed::Job Dyno は正常 - すべての
webDyno が常にリークするわけではない。数時間正常に稼働した後、1〜2台またはすべての Dyno でリークが始まる - トラフィック量よりも特定のトラフィックによって発生していると推測
- Dyno 内のすべての Puma worker でリークが起きるわけではなく、少数の worker が全メモリの大半を使用
- リークは
-
Heap dump の収集と分析
rbtraceを使ってリーク発生中の Ruby process の heap dump を収集heroku ps:execでリーク中の dyno に ssh 接続psコマンドで最もメモリを使用している Ruby worker process を選択rbtraceで対象 pid に attach し、メモリ割り当て追跡を開始 (ObjectSpace.trace_object_allocations_start)ObjectSpace.dump_allで heap dump を収集。サイズが大きい場合は gzip 圧縮heroku ps:copyで dump ファイルをローカルへ取得
reapを使って heap dump を flamegraph として可視化- 1.9GB のメモリを参照している Thread と、その配下で 32,067 個のオブジェクトを参照する Array を発見
sheapを使って疑わしいオブジェクトを調査- その Thread は Puma の worker thread であることが判明
ActiveSupport::SubscriberQueueRegistryオブジェクトがHashを参照しており、その配下にStringとArrayオブジェクトが存在- 問題の
Arrayには 32,000 個を超えるActiveSupport::Notifications::Eventオブジェクトが積み上がっていた
-
原因の推論
ActiveSupport::NotificationsのEventオブジェクトが#childrenarray に誤って蓄積されていると推測ActiveSupport::Notifications.instrumentblock 内でエラーが発生すると、そのEventが#childrenから除去されず残り、メモリリークが発生すると推定
-
ローカルでの再現
- production で見つかった疑わしい request path と parameter でローカルからリクエストを送信
500 Internal Server ErrorとともにURI::InvalidURIErrorが発生することを確認- そのリクエストを送った production dyno のメモリ使用量が急増することを確認
-
具体的な原因分析
- Rails 7.1 で修正された
ActiveSupport::NotificationsのEvent#childrenに関するバグが存在した - これに加えて、Bugsnag gem が request url を clean する過程で
URI.parse中にURI::InvalidURIErrorを raise するバグが重なり、メモリリークが発生 ActiveSupport::Notifications.subscribeblock 内で raise されたエラーが捕捉されず、そのEventが#childrenarray から除去されないまま蓄積し続けるメモリリークが発生
- Rails 7.1 で修正された
-
解決策
- 短期: Bugsnag gem で
URI::InvalidURIErrorが発生してもエラーを raise しないよう、バージョンを upgrade - 長期:
ActiveSupport::Notificationsのバグが修正された Rails 7.x に upgrade
- 短期: Bugsnag gem で
GN⁺の意見
- 問題を発見し、体系的に原因を特定していくプロセスが印象的。メモリリークが疑われる際に基本として試せる分析手順がよく整理されている
- Ruby の heap dump 収集、可視化、分析のためのさまざまなオープンソースツール(
rbtrace、reap、sheapなど)が活発に開発されているようだ。Ruby に限らず、言語ごとの有用なメモリ分析ツールを把握し、問題に適用できることが重要だと感じる - 実際、メモリリークの原因が利用中の特定ライブラリやフレームワークのバグであることは多いが、そのバグを直接解析して修正し、デプロイできるとは限らない。そのため、回避策をできるだけ早く適用することが重要。バグレポートとあわせて実行可能な代替策を提示するのも良い方法
- 単にメモリリークを解決するだけでなく、問題の root cause を深く掘り下げた点も良い。フレームワーク内部のコードを丁寧に追い、根本原因までたどる分析姿勢は開発者にとって必要だと感じる
- 結局、メモリリークの原因は当初まったく無関係に見えた些細なライブラリのバージョンアップにあった。依存関係管理と変更追跡の重要性を示す事例であり、小さな変更でも影響を慎重に分析し、デプロイ後も監視が必要であることを示唆している
1件のコメント
Hacker Newsの意見
手動メモリ管理は恐れるものではなく、エンジニアリングの訓練で解決できる
メモリリークによる500万ドル損失の事例
メモリリークのデバッグツールと解決策
文章スタイルへの称賛