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

Goroutine Leak Profiler を使ってみよう

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for rerorero rerorero
March 14, 2026
180

Goroutine Leak Profiler を使ってみよう

Avatar for rerorero

rerorero

March 14, 2026

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()は忘れずに まとめ