runtime/pprof,runtime: new goroutine leak profile
要約
概要
runtime/pprof パッケージに新しいプロファイル種別 goroutineleak を追加し、ガベージコレクター(GC)の到達可能性分析を利用してゴルーチンリーク(永久にブロックされたゴルーチン)をオンデマンドで検出できるようにするプロポーザルです。
ステータス変更
active → likely_accept
2026年4月8日のプロポーザルレビューミーティング(@aclements、@adonovan、@griesemer、@ianlancetaylor、@neild 出席)において "likely accept" に分類されました。実装が GOEXPERIMENT=goleakprofiler のフラグ下で先行ランディングされており、UberやDatadogなど実環境での検証が積み重なっていたことが、この段階への進展を後押ししました。最終コメント期間(last call for comments)として公開されています。
技術的背景
現状の問題点
Goでのゴルーチンリークは「部分デッドロック」とも呼ばれる問題であり、チャネルや sync パッケージの同期プリミティブで永久にブロックされたゴルーチンがメモリや他のリソース(ネットワーク接続、ファイルなど)を保持し続けます。
既存の runtime/pprof の goroutine プロファイルでは全ゴルーチンのスタックが取得できますが、どれがリークしているかを自動的に判定する手段がありませんでした。uber-go/goleak のようなサードパーティライブラリは単体テストにしか適用できず、本番サービスでの検出は困難でした。
提案された解決策
GCのマーキングフェーズを活用した新しい検出アルゴリズムです。
- 制約付きルート選択: 通常のGCと異なり、最初は実行可能(runnable)なゴルーチンのみをGCのマークルートとして扱う
- 到達可能性マーキング: このルートセットから到達可能なメモリをマーキングする
- eventually runnable の検出: マーキング済みの同期プリミティブ(チャネルなど)でブロックされている未マークのゴルーチンを「eventually runnable(いずれ実行可能)」として追加ルートに昇格させる
- 固定点まで反復: 新しいルートから再マーキングし、変化がなくなるまで繰り返す
- リーク判定: 最終的にルートとして扱われなかったゴルーチンをリーク(永久ブロック)として報告する
この手法は誤検知ゼロを保証します。アルゴリズムがリークと判定したゴルーチンは、理論的に二度と実行可能にならないことが保証されています。
これによって何ができるようになるか
runtime/pprof.Lookup("goroutineleak") でプロファイルを取得するだけで、ゴルーチンリーク検出用の特別なGCサイクルが実行され、リークしているゴルーチンのスタックトレースが得られます。また net/http/pprof パッケージが自動的に /debug/pprof/goroutineleak エンドポイントを公開するため、本番サービスへの統合が容易です。
Uberの実績では、3,111のテストスイートで180〜357件の異なるゴルーチンリークを発見し、本番サービスでは24時間で252件のリークレポートを生成しました。
コード例
// Before: 既存の goroutine プロファイルでは手動でリークを判断する必要がある
p := pprof.Lookup("goroutine")
p.WriteTo(os.Stdout, 1)
// → 全ゴルーチンのスタックが出力されるが、どれがリークかは不明
// After: goroutineleak プロファイルで自動判定
p := pprof.Lookup("goroutineleak")
if p == nil {
log.Fatal("goroutineleak profile not available")
}
// WriteTo を呼ぶと特別なリーク検出GCサイクルが走る
p.WriteTo(os.Stdout, 1)
// → リークしているゴルーチンのみがスタックトレースとして出力される
// トレースバックの状態表示が [waiting] ではなく [leaked] になる
// net/http/pprof 経由での HTTP アクセス(変更不要で自動利用可能)
// GET /debug/pprof/goroutineleak
議論のハイライト
- 誤検知ゼロの保証: 検出されたリークは理論的に完全に正確であり、これがこのアプローチの最大の強みとして強調された。レースディテクターと対比して「ゴルーチンリークディテクター」と命名することが推奨された。
- リソース回収の見送り: 当初の実装ではリークしたゴルーチンとそのメモリを強制解放する設計があったが、ネットワーク接続やCメモリなど非メモリリソースが解放できないため、新たな問題を引き起こす可能性があるとして却下。まずは「検出」に特化する方針が採用された。
mainゴルーチンのselect{}の扱い:select{}で永久ブロックするmainゴルーチンは正当なユースケース(全ゴルーチンをバックグラウンドで実行させるための慣用句)であるため、select{}でブロックしている場合に限りmainゴルーチンをプロファイルから除外する方向で合意された。- APIデザインの選択: 新機能を
goroutineプロファイルのパラメータとして追加する案も検討されたが、発見しやすさと明確さの観点から独立した新プロファイル種別として公開することが決定された。 - GOEXPERIMENTによる先行ランディング: 実装CLがマージコンフリクトを蓄積し続けていた問題を解消するため、
GOEXPERIMENT=goleakprofilerとして先行ランディングする mini-proposal(#75280)が承認・実装済みであり、これが本プロポーザルの審査加速にも寄与した。