はじめに
Go 1.26 リリース連載の 2 本目です。
本記事では Go 1.26 で追加された Goroutine Leak Profiles について紹介します。
アップデートの概要
goroutine のリークを検知するためのプロファイルが runtime/pprof に追加されました。
これまでは Uber 社が公開している goleak などを用いてリークを検知するのが一般的なプラクティスでしたが、Go 標準のプロファイル機能を利用して検知ができるようになりました。
リリースノート を参照し、ポイントだけ先にまとめると次の通りです。
runtime/pprofに新しいプロファイルgoroutineleakが追加されたnet/http/pprofのエンドポイントとして/debug/pprof/goroutineleakも追加された- Go 1.26 では
GOEXPERIMENT=goroutineleakprofileを付けてビルドした場合のみ利用可能となる(実験的機能) - Go 1.27 でデフォルト有効化が検討されている
関連する主要な Proposal や PR は次の通りです。
- Proposal
https://github.com/golang/go/issues/74609
https://github.com/golang/go/issues/75280 - Design Document
https://go.googlesource.com/proposal/+/master/design/74609-goroutine-leak-detection-gc.md - PR
https://github.com/golang/go/pull/74622 - Change List
https://go-review.googlesource.com/c/go/+/688335
そもそもの話
本題へ入る前に、初心者向けに goroutine リークと Go のプロファイリング(runtime/pprof)について、軽くおさらいします。
goroutine リークとは
goroutine リーク(goroutine leak)は、ざっくり言うと「本来は不要になった goroutine が、適切に終了されず放置されたまま(実質的に)生き続ける」状態です。
リークした goroutine はスタックや参照しているヒープオブジェクトを保持し続け、長期的にはメモリ使用量や GC 負荷に影響します。通常 goroutine リークは、プログラムがすぐに停止したりエラーになったりしないため、問題に気付きにくいという点でやっかいです。
典型例は次のような3ケースです。
func leak() { |
func leak() { |
func leak() { |
プロファイリング(runtime/pprof)とは
runtime/pprof は、Go ランタイムが持つ各種プロファイルを取得するための標準ライブラリです。
具体的には次のようなプロファイルが用意されています。
| 名前 | 種別 | 説明 |
|---|---|---|
goroutine |
スナップショット型 | 現在存在するすべての goroutine のスタックトレース |
heap |
スナップショット型 | 生存オブジェクトに対するメモリ割り当てのサンプリング |
allocs |
スナップショット型 | 過去に行われたすべてのメモリ割り当てのサンプリング |
block |
スナップショット型 | channel やロックなどの同期処理で待ち状態になったスタックトレース |
mutex |
スナップショット型 | 競合しているミューテックスを保持している側のスタックトレース |
threadcreate |
スナップショット型 | OS スレッド生成時のスタックトレース |
profile |
期間収集型 | 一定期間にわたって収集された CPU プロファイル |
trace |
期間収集型 | 一定期間の実行トレース(スケジューラ/ネットワーク等) |
スナップショット型のプロファイルは取得時点におけるプログラムの状態を切り取って記録するのに対し、期間収集型のプロファイルは一定期間にわたる実行状況を収集・記録します。
各プロファイルを取得する方法はいくつかありますが、net/http/pprof を使うと、これらを HTTP のエンドポイント経由で公開・取得できます。
import ( |
各種プロファイルは go tool pprof コマンドで HTTP エンドポイント(/debug/pprof/${プロファイル名})を指定することで確認できます。
たとえば heap プロファイルを確認するコマンドは次の通りです。
$ go tool pprof http://localhost:6060/debug/pprof/heap |
このコマンドを実行すると、指定した HTTP エンドポイントからプロファイルを取得し、解析用の対話モード(pprof シェル)が起動します。
対話モードでは、top や list などのコマンドを使ってプロファイルの内容を確認できます。
(pprof) top |
なお go tool pprof を利用せずとも、debug=1 や debug=2 を指定すれば、ブラウザや curl からプロファイルをテキスト形式で確認できます。
$ curl http://localhost:6060/debug/pprof/heap?debug=1 |
実際に試してみる
前提となる goroutine リークやプロファイルが理解できたところで、実際に goroutineleak プロファイルを試してみましょう。
先ほどリークの例として挙げた「close されない channel を使う」コードを利用します。
package main |
Go 1.26 では実験的な機能(Experimental)となるため、まず GOEXPERIMENT=goroutineleakprofile を付けてビルド後、起動します。
まずは debug=1 を付与して簡易的なテキスト形式で確認してみましょう。
$ curl http://localhost:6060/debug/pprof/goroutineleak?debug=1 |
1つの goroutine がリークしていることがわかります。
debug=2 を付与して確認すると、現在存在する goroutine の完全なスタックトレースの一覧を確認できます。
リークを確認するには (leaked) が付いている goroutine を見つけることが重要です。goroutine 21 [chan receive (leaked)]: という出力のとおり、channel の受信でブロックが発生し、リークしていることがわかります。
curl http://localhost:6060/debug/pprof/goroutineleak?debug=2 |
それでは次に、先ほどのコードにクローズ処理を入れることでリークを解消して試してみます。
package main |
プロファイルからも goroutine リークが発生していないことが確認できます。
$ curl http://localhost:6060/debug/pprof/goroutineleak?debug=1 |
リーク検知の背後にある思想やしくみ
リーク検知の設計思想などをつかむために Proposal(#74609)や Design Doc を見てみます。
リークを検知するためのアプローチのざっくりとした理解としては、通常の GC とは異なるリーク検知用の GC サイクルを実行し、到達可能な(実行される可能性のある)goroutine をマークすることで、どこからも到達できない goroutine を検出するようなイメージになります。
リーク判定のために GC サイクルを走らせるため、本番環境における利用は原則さけ、ローカルや本番に近しい環境での調査に使うのがよさそうです。
なお、このアプローチは「検知されたものは確実にリークである(偽陽性が出ない)」ことを重視しており、逆に「リークしているけど検知できない(偽陰性)」ケースはあり得るとされています。
The advantage of this approach over other goroutine leak detection techniques is that it can be leveraged, with a minimal performance cost, in regular Go systems, e.g., production services. It is also theoretically sound, i.e., there are no false positives. Its primary limitation is that its effectiveness is reduced the more heap resources are over-exposed in memory, i.e., pair-wise reachable.
ここまで見るとテストの実行前後で goroutine スタックを比較することでリークを検知する goleak のアプローチとは根本的に異なることがわかります。
その意味ではどちらかがどちらかに取って代わるものではなく、もしかしたら相互補完的な関係性にあると言えるかもしれません(例. ユニットテストや CI では goleak を利用し、より本番に近い環境での診断には goroutineleak プロファイルを使用するなど)。
リーク検知されないケース
先ほど説明した通り goroutineleak プロファイルによる検知は「偽陽性を出さない」方向に設計されており、リークしているけど検知できないケースがあります。
たとえばグローバル変数として channel を保持する場合は、到達可能(実行される可能性がある)と判断されます。
package main |
次のとおりこのコードはリークとして検知されません。
$ curl http://localhost:6060/debug/pprof/goroutineleak?debug=1 |
おわりに
ここまで見てきたとおり、Go 1.26 で追加された goroutineleak プロファイルは、なかなか追いづらかった goroutine リークを検知できる標準機能です。
Go 1.26 時点では実験的機能という位置付けですが、Go 1.27 でデフォルトで有効になるのが待ち遠しいですね。