GC到達可能性解析でゴルーチンリークを検出する goroutineleak プロファイルが正式機能になった
Goroutine leak profile
概要
Go 1.26で実験的機能として導入されていたゴルーチンリーク検出用のプロファイルタイプが、正式機能として一般提供されるようになった。新しいプロファイルタイプ goroutineleak は runtime/pprof パッケージでサポートされ、net/http/pprof 経由でも /debug/pprof/goroutineleak エンドポイントとして利用できる。
リークしたゴルーチンとは、チャネルや sync.Mutex、sync.Cond などの並行処理プリミティブでブロックされ、二度とブロック解除されえない状態のゴルーチンを指す。ランタイムはガベージコレクタを利用してこれを検出する。あるゴルーチンGが並行処理プリミティブPでブロックされており、Pが実行可能な(あるいは実行可能なゴルーチンからブロック解除されうる)どのゴルーチンからも到達不可能である場合、Pは二度とブロック解除されえないため、GはリークしたゴルーチンだとGCは判定する。この手法はすべての永久ブロックを検出できるわけではないが、広いクラスのリークを検出できる。
この手法は到達可能性に基づくため、グローバル変数や実行可能なゴルーチンのローカル変数を経由してのみ到達可能な並行処理プリミティブでのブロックによるリークは検出できないことがある。
例は Go 1.26 リリースノート を参照。
この機能の一般提供に伴い、goroutineleakprofile GOEXPERIMENT 設定は削除された。
導入経緯
本Itemの元になったproposal(Uber社のVlad Saioc氏による提案)は、GCのメモリ到達可能性解析を利用してゴルーチンリーク(学術的には部分デッドロックとも呼ばれる)を検出する仕組みを runtime/pprof に追加するというもの。設計は事前に design document にまとめられ、69件のマイクロベンチマークや、Uber社内の3111テストスイート、本番サービスでの検証(24時間で252件のリーク報告)によって効果が裏付けられていた。
提案当初は検出に加えてリークしたゴルーチンのメモリを強制回収する案もあったが、ネットワーク接続やファイル、Cメモリなど非メモリリソースまでは回収できないという懸念から、GCの到達可能性上は引き続き「実行可能」として扱い、検出のみを行う設計に変更された。実装は先行して GOEXPERIMENT=goroutineleakprofile の下でランディングされ(#75280)、Datadogなど外部ユーザーからのフィードバックも得た上で、本proposalとして正式レビューにかけられた。
議論タイムライン
- 2026-03-11: 週次プロポーザルレビューミーティングで議題に追加され、
activeに移行。mknyszekがユーザー可視の変更点(トレースバックの[leaked]表示、runtime/pprof.Lookup/Profilesの挙動、net/http/pprofエンドポイント追加)を整理したコメントを投稿。 - 2026-04-08: 週次プロポーザルレビューミーティングで
likely_acceptに移行。 - 2026-04-15: 「コンセンサスに変更なし」として
acceptedに確定。
議論のハイライト
- 偽陽性ゼロが本proposal最大の強み。GCが「到達不可能」と判定したゴルーチンのみを報告するため誤検知が発生しない。
- 当初検討されたリークしたゴルーチンのメモリ強制回収は見送られ、ファイルディスクリプタやネットワーク接続など非メモリリソースの扱いが難しいことから、まずは検出のみに専念する設計となった。
select{}で永久ブロックするmainゴルーチンは、バックグラウンド処理のみで動くサービスの正当な実装パターンであるため、mainゴルーチンがselect{}でブロックしている場合に限りプロファイルから除外する方向で合意された。- Uber社での実証データ(3111テストスイートで180〜357件のリーク検出、本番サービスで24時間に252件のリーク報告)が信頼性の裏付けとして重視された。
GOEXPERIMENT=goroutineleakprofileによる段階的な先行リリース戦略を採用し、外部フィードバックを集めた上で正式なproposalレビューに進んだ。
使用例
Before
package main
import (
"os"
"runtime/pprof"
)
func main() {
// goroutine プロファイル全体を取得し、
// リークしているものは手作業で見分ける必要があった
p := pprof.Lookup("goroutine")
p.WriteTo(os.Stdout, 1)
}
After
package main
import (
"os"
"runtime/pprof"
)
func main() {
// goroutineleak プロファイルはリーク検出用のGCサイクルを
// 自動的にトリガーし、リークしたゴルーチンのみを報告する
p := pprof.Lookup("goroutineleak")
if p != nil {
p.WriteTo(os.Stdout, 1)
}
}
移行時の注意
ビルド環境で明示的に GOEXPERIMENT=goroutineleakprofile を設定していた場合、この設定自体が削除されたため、該当の環境変数指定を取り除く必要がある。機能自体は常時有効になっており、追加の設定なしで goroutineleak プロファイルを利用できる。
実装解説
runtime/pprof 側では、goroutineleak は他の組み込みプロファイルと同様に Profile 値として登録されている(pprof.go;l=193 で定義し、pprof.go;l=265 で profiles.m マップに登録)。WriteTo の実装は、プロファイルを書き出す前に runtime_goroutineLeakGC を呼び出してリーク検出用のGCサイクルを明示的にトリガーしてから結果を書き出す(pprof.go;l=807)。
ランタイム側の goroutineLeakGC(mgc.go;l=597)は work.goroutineLeak.pending フラグを立てたうえで、実際にリーク検出を実施するGCサイクルが完了するまで GC() をスピンさせる。マーク終端処理の中で呼ばれる findGoroutineLeaks(mgc.go;l=1278)は、実行可能なゴルーチンから到達可能なスタックルート(nMaybeRunnableStackRoots)のみをマーク済みとして扱い、残りのゴルーチンを _Gleaked 状態に遷移させる。_Gleaked は通常のGCからは実行可能ゴルーチンと同様に到達可能なメモリとして扱われる新設の goroutine 状態で、runtime2.go;l=95 に定義がある(runtime2.go;l=95)。トレースバック出力では _Gleaked は "leaked" という文字列にマッピングされ(traceback.go;l=1264)、リークしたゴルーチンはスタックダンプ上で [leaked] と表示される。
net/http/pprof 側は、goroutineleak を profileSupportsDelta および profileDescriptions に追加しているだけで(pprof.go;l=356, pprof.go;l=374)、/debug/pprof/ のインデックスページは pprof.Profiles() を列挙するだけの実装のため(pprof.go;l=400)、runtime/pprof 側で登録されたプロファイルは自動的にHTTPエンドポイント一覧にも反映される。