Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goroutine Leak Profiler を使ってみよう

Avatar for rerorero rerorero
March 14, 2026
130

Goroutine Leak Profiler を使ってみよう

Avatar for rerorero

rerorero

March 14, 2026
Tweet

Transcript

  1. 4 © LayerX Inc. goroutine リークのインパクト 💣 出典: "Unveiling and

    Vanquishing Goroutine Leaks" (Uber, arXiv:2312.12002) 数⽇のアップタイムでリークしていたメモリ(Uber社の例) リーク修正前 リーク修正後 改善 サービスX 43.5 GB / instance 3 GB / instance 92%削減 サービスY 30 GB / instance 6.5 GB / isntance 78%削減 デプロイのたびにリセットされるので気づきにくい 知らないうちにコストがn倍.. なんてことに 😱
  2. © LayerX Inc. 5 runtime.NumGoroutine  Goroutineの数のみ 従来の⼿法 pprof/goroutine  goroutineのスナップショット uber-go/goleak

     テスト終了時に余分なgoroutineがいないか検出。テストでしかリーク検出できない。
  3. © LayerX Inc. 6 goroutine leak profiler 推しポイント ☑ 本番環境でリーク検出できる

    ☑ false positiveがない ☑ どこでリークが起きているか分かる
  4. © LayerX Inc. 7 ガベージコレクションの仕組みを利⽤ 通常のGC  全ての groutine のスタックからポインタを巡っていき、参照されていないオブジェクトを⾒つける goroutine

    leak profiler  GCのフローを利⽤して、runnableなgoroutineのスタックから到達できない同期プリミティブ(チャ ンネル)を待っている waitingな goroutineをリークとみなす
  5. © LayerX Inc. 8 func causeLeak() { ch := make(chan

    int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } スタックにいる
  6. © LayerX Inc. 9 func causeLeak() { ch := make(chan

    int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } Waiting
  7. © LayerX Inc. 10 func causeLeak() { ch := make(chan

    int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } リターン
  8. © LayerX Inc. 11 func causeLeak() { ch := make(chan

    int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } もう巡れない Waiting
  9. © LayerX Inc. 12 func causeLeak() { ch := make(chan

    int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } もう巡れない Waiting ↓ Leaked
  10. © LayerX Inc. 13 ガベージコレクションとほぼ同じコストしかかからない goroutine leak profiler • GoのGCは軽量

    • 何もしなくても2分に⼀回強制的にGCが⾛っている https://github.com/golang/go/blob/827564191b9796a764e970175cecd51c2030530e/src/runtime/proc.go#L6504-L6509 • 数分に⼀回動かしてもきっと⼤丈夫! // forcegcperiod is the maximum time in nanoseconds between garbage // collections. If we go this long without a garbage collection, one // is forced to run. // // This is a variable for testing purposes. It normally doesn't change. var forcegcperiod int64 = 2 * 60 * 1e9
  11. © LayerX Inc. 15 Step1: フラグをつけてビルド GOEXPERIMENT=goroutineleakprofile go run main.go

    Step2: アプリケーションで定期的にプロファイリング pprof.Lookup("goroutineleak").WriteTo(os.stdout, 1) 1.26では GOEXPERIMENT フラグでビルドが必要だが、 リリースノートには The implementation is production-ready,と⽰されている。 1.27ではデフォルトでON、フラグ不要になる予定
  12. © LayerX Inc. 17 mysqlを使うサーバーアプリケーションで試してみた すると早速検出!     goroutineleak profile:

    total 1 1 @ ... database/sql.(*DB).connectionOpener ⾊々調べた結果、起動時に実⾏するスキーママイグレーションのところで DB接続の Close() を忘れている問題に気づく。
  13. © LayerX Inc. 18 簡略化した問題のコード(リーク検出する) func main() { ctx, stop

    := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // db接続を作り、マイグレーション実行 db, _ := sql.Open("mysql", "root:pass@tcp(127.0.0.1:3306)/mysqldb") doMigration(db) // サービスメイン(DBへのアクセスは別の接続を利用) go doMain() <-ctx.Done() // signalを待つ }
  14. © LayerX Inc. 19 簡略化した問題のコード(リーク検出しない) func main() { ctx, stop

    := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // db接続を作り、マイグレーション実行 db, _ := sql.Open("mysql", "root:pass@tcp(127.0.0.1:3306)/mysqldb") doMigration(db) // サービスメイン(DBへのアクセスは別の接続を利用) go doMain() <-ctx.Done() // signalを待つ db.Close() } リーク検出しなくなった!
  15. © LayerX Inc. 20 簡略化した問題のコード(リーク検出しない) func main() { ctx, stop

    := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() // db接続を作り、マイグレーション実行 db, _ := sql.Open("mysql", "root:pass@tcp(127.0.0.1:3306)/mysqldb") doMigration(db) // サービスメイン(DBへのアクセスは別の接続を利用) go doMain() <-ctx.Done() // signalを待つ println(db) } Closeしなくても リーク検出しなくなる
  16. © LayerX Inc. 21 リーク検出箇所 sql.Open() └─ OpenDB() ├─ openerCh

    = make(chan struct{}, 1000000) └─ go connectionOpener() ← goroutine起動、openerCh 待機へ
  17. 22 © LayerX Inc. liveness analysis • ある特定の場所でGCにどのオブジェクトが有効かを教える仕組み • ”セーフポイント”で利⽤されないとコンパイラが判断したスタック上のポイ

    ンタ変数は、GCは到達不可と判定する - https://go.dev/src/cmd/compile/README#7-generating-machine-code • セーフポイントはGCが安全にメモリの位置を特定できるポイント - Go 1.13(⾮協調的プリエンプション)までは関数呼び出しだけだったが、1.14以降はunsafeでない場合⼤体 セーフポイントになった - https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption.md • ビルドフラグ gcflags=”-live” をつけるとセーフポイントの情報が表⽰できる - https://github.com/golang/go/blob/master/src/cmd/compile/internal/liveness/plive.go Read Me
  18. 23 © LayerX Inc. go build gcflags=”-live” ./main.go Read Me

    ./main.go:19:35: live at call to NotifyContext: .autotmp_16 ./main.go:19:35: live at call to newobject: .autotmp_16 ./main.go:22:19: live at call to Open: .autotmp_16 ctx.data ./main.go:25:2: live at call to newproc: .autotmp_16 ctx.data db ./main.go:30:2: live at call to chanrecv1: .autotmp_16 db ./main.go:30:12: live at indirect call: .autotmp_16 db ./main.go:32:1: live at indirect call: .autotmp_16 chanrecv1が <- ctx.Done で待つところ そのセーフポイントでlive なシンボルが⼀覧で表⽰さ れる
  19. 24 © LayerX Inc. go build gcflags=”-live” ./main.go Read Me

    ./main.go:19:35: live at call to NotifyContext: .autotmp_16 ./main.go:19:35: live at call to newobject: .autotmp_16 ./main.go:22:19: live at call to Open: .autotmp_16 ctx.data ./main.go:25:2: live at call to newproc: .autotmp_16 ctx.data db ./main.go:30:2: live at call to chanrecv1: .autotmp_16 db ./main.go:30:12: live at indirect call: .autotmp_16 db ./main.go:32:1: live at indirect call: .autotmp_16 ./main.go:18:35: live at call to NotifyContext: .autotmp_16 ./main.go:18:35: live at call to newobject: .autotmp_16 ./main.go:21:19: live at call to Open: .autotmp_16 ctx.data ./main.go:24:2: live at call to newproc: .autotmp_16 ctx.data ./main.go:29:2: live at call to chanrecv1: .autotmp_16 ./main.go:29:12: live at indirect call: .autotmp_16 ./main.go:31:1: live at indirect call: .autotmp_16 println(db) // println(db) dbが liveでなく なってる
  20. © LayerX Inc. 25 db.Close()がないとリーク判定される理由 • 送信chを含むdbの参照が liveness analysis により到達不可と判定

    • ch も未使⽤とみなされる • 受信側の Waiting な sql connectionOpener の goroutine がリークと判断された おそらく起きていたこと 今回は実際のリークであったが、 liveness analysisの影響によりリーク検出しないケースが存在する
  21. © LayerX Inc. 26 • liveness analysisの影響でリーク検出されなくなるケースがあるという学び • それでも検出されたリークは100%本当のリーク •

    本番環境で安全にリークを検出できるので使っていきましょう! • dbのClose()は忘れずに まとめ