メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#74609accepted

runtime/pprof,runtime: new goroutine leak profile

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

ガベージコレクタ(GC)のメモリ到達可能性解析を利用して、永久にブロックされたゴルーチン(ゴルーチンリーク)を検出する新しいプロファイルタイプ goroutineleakruntime/pprof パッケージに追加するproposalです。偽陽性ゼロという高い信頼性が特徴であり、Uber社での実運用による実績に裏付けられています。

ステータス変更

likely_acceptaccepted
2026年4月15日に開催されたProposal Review Meetingにおいて「コンセンサスに変更なし」として正式に承認されました。2026年4月8日に likely_accept となって以来、反対意見や追加の懸念が提起されなかったことから、最終承認に至りました。事前に GOEXPERIMENT=goroutineleakprofile として実験的フラグの背後でリリースされ、Datadog等の外部ユーザーからの実績報告が積み重なったことが承認を後押しする要因となったと考えられます。

技術的背景

現状の問題点

Goプログラムにおいて、チャネルや sync パッケージの同期プリミティブの誤用により、ゴルーチンが永久にブロックされる「ゴルーチンリーク」が発生することがあります。既存の runtime/pprof の goroutine プロファイルは全ゴルーチンのスタックトレースを出力しますが、そこからリークしているゴルーチンを特定するには人手による分析が必要でした。
また、uber-go/goleak のような外部ツールはユニットテスト終了時の検出に限定されており、長時間稼働するサービスへの適用が困難でした。

// 問題のあるコード: チャネルに送信者がいないため永久にブロック
func leakExample() {
    ch := make(chan int)
    go func() {
        val := <-ch  // このゴルーチンは永久にブロックされる
        fmt.Println(val)
    }()
    // ch への送信を忘れた場合、ゴルーチンはリーク
}

提案された解決策

GCのマーキングフェーズを活用した以下のアルゴリズムで「到達不可能なゴルーチン」を検出します。

  1. 通常のGCではすべてのゴルーチンをルートとしてマークするが、リーク検出用の特殊GCサイクルでは「実行可能なゴルーチン」のみをルートとして使用する
  2. そこから到達可能なメモリを辿り、到達可能な同期プリミティブ(チャネルなど)でブロックしているゴルーチンを「最終的に実行可能」と判断する
  3. 不動点(これ以上実行可能なゴルーチンが見つからない状態)に達するまで繰り返す
  4. 最終的にルートとして扱われないゴルーチンが「リーク」と判定される
    この手法はゴルーチンリークが発生している場合に限り検出するため、偽陽性ゼロが保証されます。

これによって何ができるようになるか

ゴルーチンリーク検出をオンデマンドで実行し、リークしたゴルーチンのスタックトレースをプロファイルとして取得できるようになります。

  • 長時間稼働するマイクロサービスにおいて、定期的にリーク検出を実行してメモリ増加の原因を特定できる
  • net/http/pprof 経由で /debug/pprof/goroutineleak エンドポイントが自動的に公開され、既存のプロファイリングインフラに統合できる
  • 将来的には go test へのフラグ追加も検討されており、テスト実行時のリーク自動検出が見込まれる

コード例

// Before: 従来のワークアラウンド(goroutineプロファイルから手動で特定)
import "runtime/pprof"
// 全ゴルーチンの一覧を取得するが、どれがリークしているかは不明
p := pprof.Lookup("goroutine")
p.WriteTo(os.Stdout, 1)
// After: 新しいgoroutineleakプロファイルを使用
import "runtime/pprof"
// リーク検出用GCサイクルを自動トリガーし、リークしたゴルーチンのみ返す
p := pprof.Lookup("goroutineleak")
if p != nil {
    p.WriteTo(os.Stdout, 1)  // リークしたゴルーチンのスタックトレースが出力される
}
// HTTP経由でも自動的に利用可能(net/http/pprof を import するだけ)
// GET /debug/pprof/goroutineleak

リークしたゴルーチンはトレースバック出力において [waiting] ではなく [leaked] と表示されるようになります。

議論のハイライト

  • 偽陽性ゼロの保証: 学術論文(ACM Digital Library掲載)に裏付けられたアルゴリズムを採用しており、GCが「到達不可能」と判定したゴルーチンのみを報告するため誤検知が発生しない。これがproposalを強力に支持する最大の根拠となった
  • メモリ回収機能は見送り: 当初の実装ではリークしたゴルーチンのメモリを強制回収する案もあったが、ファイルディスクリプタやネットワーク接続など非メモリリソースの回収が困難であり、誤操作のリスクもあることから、検出のみを行う安全な設計に変更された
  • main ゴルーチンの扱い: select{} で永久ブロックする main ゴルーチンは意図的な利用パターン(バックグラウンド処理のみで動くサービス)として存在するため、main ゴルーチンが select{} でブロックしている場合に限りプロファイルから除外する方向で最終合意した
  • Uber社での実績: 3111のテストスイートで検証し、180〜357件の構文的に異なるリークを検出(uber-go/goleak との比較)、本番サービスでは24時間で252件のリーク報告を達成したデータが、proposalの信頼性を大きく高めた
  • GOEXPERIMENT による段階的展開: 本提案はまず GOEXPERIMENT=goroutineleakprofile として実験的フラグで先行リリース(#75280)され、外部からのフィードバックを収集した後に本格的なproposalレビューに臨む戦略を取った

関連リンク