Share
Embed
Goroutine Leak Profiler を使ってみよう
rerorero
March 14, 2026
Transcript
27 slides
-
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倍.. なんてことに 😱 -
© LayerX Inc. 5 runtime.NumGoroutine Goroutineの数のみ 従来の⼿法 pprof/goroutine goroutineのスナップショット uber-go/goleak
テスト終了時に余分な Goroutine がいないか検出。テストでしかリーク検出できない。 -
© LayerX Inc. 6 goroutine leak profiler 推しポイント ☑ 本番環境でリーク検出できる
☑ false positiveがない ☑ どこでリークが起きているか分かる -
© LayerX Inc. 7 ガベージコレクションの仕組みを利⽤ 通常のGC 全ての groutine のスタックからポインタを巡っていき、参照されていないオブジェクトを⾒つける goroutine
leak profiler GCのフローを利⽤して、runnableなgoroutineのスタックから到達できない同期プリミティブ(チャ ンネル)を待っている waitingな goroutineをリークとみなす -
© LayerX Inc. 8 func causeLeak() { ch := make(chan
int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } スタックにいる -
© LayerX Inc. 9 func causeLeak() { ch := make(chan
int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } Waiting -
© LayerX Inc. 10 func causeLeak() { ch := make(chan
int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } リターン -
© LayerX Inc. 11 func causeLeak() { ch := make(chan
int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } もう巡れない Waiting -
© LayerX Inc. 12 func causeLeak() { ch := make(chan
int) go func() { ch <- 42 fmt.Println("This will never be printed") }() } もう巡れない Waiting ↓ Leaked -
© 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 -
© 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、フラグ不要になる予定 -
© LayerX Inc. 16 mysqlを使うサーバーアプリケーションで試してみた すると早速検出! goroutineleak profile:
total 1 1 @ ... database/sql.(*DB).connectionOpener -
© LayerX Inc. 17 mysqlを使うサーバーアプリケーションで試してみた すると早速検出! goroutineleak profile:
total 1 1 @ ... database/sql.(*DB).connectionOpener ⾊々調べた結果、起動時に実⾏するスキーママイグレーションのところで DB接続の Close() を忘れている問題に気づく。 -
© 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を待つ } -
© 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() } リーク検出しなくなった! -
© 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しなくても リーク検出しなくなる -
© LayerX Inc. 21 リーク検出箇所 sql.Open() └─ OpenDB() ├─ openerCh
= make(chan struct{}, 1000000) └─ go connectionOpener() ← goroutine起動、openerCh 待機へ -
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 -
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 なシンボルが⼀覧で表⽰さ れる -
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でなく なってる -
© LayerX Inc. 25 db.Close()がないとリーク判定される理由 • 送信chを含むdbの参照が liveness analysis により到達不可と判定
• ch も未使⽤とみなされる • 受信側の Waiting な sql connectionOpener の goroutine がリークと判断された おそらく起きていたこと 今回は実際のリークであったが、 liveness analysisの影響によりリーク検出しないケースが存在する -
© LayerX Inc. 26 • liveness analysisの影響でリーク検出されなくなるケースがあるという学び • それでも検出されたリークは100%本当のリーク •
本番環境で安全にリークを検出できるので使っていきましょう! • dbのClose()は忘れずに まとめ