[インデックス 14034] ファイルの概要
このコミットは、Go言語のプロファイリングツールであるpprof
に、ゴルーチンブロッキングプロファイリング機能を追加するものです。これにより、プログラム内でゴルーチンが同期プリミティブ(チャネル、ミューテックスなど)によってブロックされている時間を計測し、その原因となっているコールスタックを特定できるようになります。これは、Google Perf Toolsの同様の機能に触発されたもので、Goアプリケーションのパフォーマンスボトルネック、特に並行処理における競合状態やデッドロックの特定に役立ちます。
コミット
commit 4cc7bf326a26d3cc18f049424729784812fe16b6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sat Oct 6 12:56:04 2012 +0400
pprof: add goroutine blocking profiling
The profiler collects goroutine blocking information similar to Google Perf Tools.
You may see an example of the profile (converted to svg) attached to
http://code.google.com/p/go/issues/detail?id=3946
The public API changes are:
+pkg runtime, func BlockProfile([]BlockProfileRecord) (int, bool)
+pkg runtime, func SetBlockProfileRate(int)
+pkg runtime, method (*BlockProfileRecord) Stack() []uintptr
+pkg runtime, type BlockProfileRecord struct
+pkg runtime, type BlockProfileRecord struct, Count int64
+pkg runtime, type BlockProfileRecord struct, Cycles int64
+pkg runtime, type BlockProfileRecord struct, embedded StackRecord
R=rsc, dave, minux.ma, r
CC=gobot, golang-dev, r, remyoudompheng
https://golang.org/cl/6443115
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4cc7bf326a26d3cc18f049424729784812fe16b6
元コミット内容
pprof: add goroutine blocking profiling
The profiler collects goroutine blocking information similar to Google Perf Tools.
You may see an example of the profile (converted to svg) attached to
http://code.google.com/p/go/issues/detail?id=3946
The public API changes are:
+pkg runtime, func BlockProfile([]BlockProfileRecord) (int, bool)
+pkg runtime, func SetBlockProfileRate(int)
+pkg runtime, method (*BlockProfileRecord) Stack() []uintptr
+pkg runtime, type BlockProfileRecord struct
+pkg runtime, type BlockProfileRecord struct, Count int64
+pkg runtime, type BlockProfileRecord struct, Cycles int64
+pkg runtime, type BlockProfileRecord struct, embedded StackRecord
変更の背景
Go言語は並行処理を容易にするゴルーチンとチャネルを提供しますが、並行処理のコードにはデッドロックや競合状態といったパフォーマンスボトルネックが潜むことがあります。特に、ゴルーチンがチャネル操作やミューテックスのロック取得などでブロックされる時間は、アプリケーション全体の応答性やスループットに大きな影響を与えます。
このコミット以前のGoのプロファイリングツール(pprof
)は、CPU使用率やメモリ割り当てのプロファイリングは可能でしたが、ゴルーチンのブロッキングに関する詳細な情報を提供していませんでした。開発者は、どの同期プリミティブがボトルネックになっているのか、どのコードパスでゴルーチンが長時間ブロックされているのかを特定するのが困難でした。
この課題を解決するため、Google内部で使用されているGoogle Perf Toolsのブロッキングプロファイリング機能に触発され、Goにも同様の機能が導入されました。これにより、開発者はGoアプリケーションの並行処理におけるパフォーマンス問題をより効果的に診断し、最適化できるようになります。
前提知識の解説
ゴルーチン (Goroutine)
Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに少ないメモリで作成でき、数百万のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによってスケジューリングされ、OSスレッドに多重化されて実行されます。
ブロッキング (Blocking)
ゴルーチンが何らかの操作(例: チャネルからの読み込み、ミューテックスのロック取得、I/O操作など)が完了するまで待機する状態を指します。ブロッキング自体は並行処理において正常な動作ですが、不必要に長いブロッキングはパフォーマンスの低下を招きます。
プロファイリング (Profiling)
プログラムの実行中にその動作を監視し、パフォーマンス特性(例: CPU使用率、メモリ使用量、関数呼び出し回数、ブロッキング時間など)に関するデータを収集するプロセスです。収集されたデータは、プログラムのボトルネックを特定し、最適化の対象を見つけるために使用されます。
pprof
Go言語に標準で組み込まれているプロファイリングツールです。CPUプロファイル、メモリプロファイル、ゴルーチンプロファイル、スレッド作成プロファイルなど、様々な種類のプロファイルデータを収集・可視化できます。これらのプロファイルは、go tool pprof
コマンドを使って解析し、グラフやテキスト形式で表示できます。
Google Perf Tools
Googleが開発したパフォーマンスプロファイリングツールのスイートです。CPU、ヒープ、スレッド、そしてブロッキングなど、多岐にわたるプロファイリング機能を提供します。このコミットで追加されたゴルーチンブロッキングプロファイリングは、Google Perf Toolsのブロッキングプロファイリングの概念をGoに適用したものです。
技術的詳細
ゴルーチンブロッキングプロファイリングは、プログラム実行中にゴルーチンがブロックされたイベントをサンプリングし、そのブロッキングが発生したコールスタックとブロッキング時間を記録します。
- サンプリングレートの設定:
runtime.SetBlockProfileRate(rate int)
関数を使用して、ブロッキングイベントのサンプリングレートを設定します。rate
はナノ秒単位で、プロファイラは平均してrate
ナノ秒ごとに1つのブロッキングイベントをサンプリングすることを目指します。rate=1
を設定すると、すべてのブロッキングイベントが記録されます。rate <= 0
でプロファイリングを無効にできます。 - ブロッキングイベントの検出: Goランタイム内のチャネル操作 (
chan.c
) やセマフォ操作 (sema.goc
) など、ゴルーチンがブロックされる可能性のある箇所にフックが追加されます。ゴルーチンがブロック状態に入る直前にタイムスタンプが記録され、ブロック状態から解放されたときに再度タイムスタンプが記録されます。この差分がブロッキング時間となります。 - スタックトレースの収集: ブロッキングイベントが発生した際、その時点のゴルーチンのコールスタックが収集されます。
- プロファイルデータの集計: 収集されたブロッキング時間とスタックトレースは、
runtime/mprof.goc
内のプロファイリングバケットに集計されます。同じコールスタックで発生したブロッキングイベントは、その回数 (Count
) と合計ブロッキング時間 (Cycles
) が加算されます。 - プロファイルの取得:
runtime.BlockProfile([]BlockProfileRecord) (int, bool)
関数を使用して、集計されたブロッキングプロファイルデータを取得できます。BlockProfileRecord
構造体には、ブロッキング回数、合計ブロッキング時間、およびスタックトレースが含まれます。 pprof
との統合:net/http/pprof
パッケージとruntime/pprof
パッケージが更新され、HTTPエンドポイント/debug/pprof/block
を通じてブロッキングプロファイルにアクセスできるようになります。go tool pprof http://localhost:6060/debug/pprof/block
のようにコマンドを実行することで、ブロッキングプロファイルを解析・可視化できます。
このサンプリングベースのアプローチにより、プロファイリングのオーバーヘッドを最小限に抑えつつ、アプリケーションのブロッキングパターンに関する有用な洞察を得ることができます。
コアとなるコードの変更箇所
このコミットでは、以下の主要なファイルが変更されています。
src/cmd/go/test.go
:go test
コマンドに-test.blockprofile
と-test.blockprofilerate
フラグが追加され、テスト実行時にブロッキングプロファイルを生成できるようになりました。src/cmd/go/testflag.go
: 上記の新しいテストフラグがgo test
のフラグ定義に追加されました。src/pkg/net/http/pprof/pprof.go
: HTTPサーバーを通じてブロッキングプロファイルを提供する/debug/pprof/block
エンドポイントが追加されました。src/pkg/runtime/chan.c
: チャネルの送受信操作 (chansend
,chanrecv
) に、ブロッキング時間の計測とプロファイリングイベントの記録のためのコードが追加されました。SudoG
構造体にreleasetime
フィールドが追加されています。src/pkg/runtime/debug.go
: ユーザーが直接呼び出すための公開APIとして、SetBlockProfileRate
関数とBlockProfile
関数、およびBlockProfileRecord
型が追加されました。src/pkg/runtime/mprof.goc
: メモリプロファイリングとブロッキングプロファイリングの両方を扱うための汎用的なプロファイリングバケット管理ロジックが導入されました。Bucket
構造体にtyp
フィールドが追加され、メモリプロファイル (MProf
) とブロッキングプロファイル (BProf
) を区別できるようになりました。また、runtime·blockevent
関数が追加され、ブロッキングイベントの記録と集計を行います。src/pkg/runtime/pprof/pprof.go
:runtime/pprof
パッケージにblockProfile
が追加され、ブロッキングプロファイルの収集とio.Writer
への書き出しロジックが実装されました。src/pkg/runtime/runtime.c
:runtime·tickspersecond
関数が追加され、CPUティックのレートを計算します。これはブロッキング時間の正規化に使用されます。src/pkg/runtime/runtime.h
: 新しいランタイム関数とグローバル変数 (runtime·tickspersecond
,runtime·blockevent
,runtime·blockprofilerate
) の宣言が追加されました。src/pkg/runtime/sema.goc
: セマフォの取得 (semacquireimpl
) に、ブロッキング時間の計測とプロファイリングイベントの記録のためのコードが追加されました。Sema
構造体にreleasetime
フィールドが追加されています。src/pkg/runtime/signal_linux_arm.c
:runtime·cputicks
の実装が変更され、ブロッキングプロファイラとfastrand1
のシードとしてruntime·nanotime()
とruntime·randomNumber
を使用するように修正されました。src/pkg/testing/testing.go
:testing
パッケージに-test.blockprofile
と-test.blockprofilerate
フラグが追加され、テスト実行後のブロッキングプロファイルのファイル出力がサポートされました。
コアとなるコードの解説
src/pkg/runtime/debug.go
// SetBlockProfileRate controls the fraction of goroutine blocking events
// that are reported in the blocking profile. The profiler aims to sample
// an average of one blocking event per rate nanoseconds spent blocked.
//
// To include every blocking event in the profile, pass rate = 1.
// To turn off profiling entirely, pass rate <= 0.
func SetBlockProfileRate(rate int)
// BlockProfileRecord describes blocking events originated
// at a particular call sequence (stack trace).
type BlockProfileRecord struct {
Count int64
Cycles int64
StackRecord
}
// BlockProfile returns n, the number of records in the current blocking profile.
// If len(p) >= n, BlockProfile copies the profile into p and returns n, true.
// If len(p) < n, BlockProfile does not change p and returns n, false.
//
// Most clients should use the runtime/pprof package or
// the testing package's -test.blockprofile flag instead
// of calling BlockProfile directly.
func BlockProfile(p []BlockProfileRecord) (n int, ok bool)
debug.go
では、ブロッキングプロファイリングを制御するための主要な公開APIが定義されています。
SetBlockProfileRate
: プロファイリングのサンプリングレートを設定します。rate
が小さいほど詳細なプロファイルが得られますが、オーバーヘッドが増加します。BlockProfileRecord
: ブロッキングイベントの情報を保持する構造体です。Count
はブロッキングイベントの発生回数、Cycles
はそのスタックトレースでの合計ブロッキング時間(CPUサイクル単位)、StackRecord
はコールスタック情報を含みます。BlockProfile
: 現在のブロッキングプロファイルデータを取得するための関数です。
src/pkg/runtime/mprof.goc
enum { MProf, BProf }; // profile types
typedef struct Bucket Bucket;
struct Bucket
{
Bucket *next; // next in hash list
Bucket *allnext; // next in list of all mbuckets/bbuckets
int32 typ; // MProf or BProf
union
{
struct // typ == MProf
{
// ... memory profile fields ...
};
struct // typ == BProf
{
int64 count;
int64 cycles;
};
};
uintptr hash;
uintptr nstk;
uintptr stk[1];
};
static Bucket *mbuckets; // memory profile buckets
static Bucket *bbuckets; // blocking profile buckets
static Bucket*
stkbucket(int32 typ, uintptr *stk, int32 nstk, bool alloc)
{
// ... existing logic ...
b->typ = typ; // Set the type of the bucket
if(typ == MProf) {
b->allnext = mbuckets;
mbuckets = b;
} else { // typ == BProf
b->allnext = bbuckets;
bbuckets = b;
}
return b;
}
int64 runtime·blockprofilerate; // in CPU ticks
void
runtime·SetBlockProfileRate(intgo rate)
{
runtime·atomicstore64((uint64*)&runtime·blockprofilerate, rate * runtime·tickspersecond() / (1000*1000*1000));
}
void
runtime·blockevent(int64 cycles, int32 skip)
{
int32 nstk;
int64 rate;
uintptr stk[32];
Bucket *b;
if(cycles <= 0)
return;
rate = runtime·atomicload64((uint64*)&runtime·blockprofilerate);
if(rate <= 0 || (rate > cycles && runtime·fastrand1()%rate > cycles))
return;
nstk = runtime·callers(skip, stk, 32);
runtime·lock(&proflock);
b = stkbucket(BProf, stk, nstk, true); // Get or create a blocking profile bucket
b->count++;
b->cycles += cycles;
runtime·unlock(&proflock);
}
mprof.goc
は、プロファイリングデータのコアな集計ロジックを扱います。
Bucket
構造体が拡張され、メモリプロファイルとブロッキングプロファイルの両方を格納できるようになりました。typ
フィールドでどちらのプロファイルタイプであるかを識別します。stkbucket
関数は、typ
引数を受け取るようになり、適切なプロファイルバケットリスト(mbuckets
またはbbuckets
)に新しいバケットを追加します。runtime·blockprofilerate
は、ブロッキングプロファイリングのサンプリングレートを保持するグローバル変数です。runtime·SetBlockProfileRate
は、GoのSetBlockProfileRate
関数から呼び出され、内部のruntime·blockprofilerate
を設定します。ナノ秒単位のレートをCPUティック単位に変換しています。runtime·blockevent
は、ブロッキングイベントが発生した際に呼び出される主要な関数です。cycles
はブロッキング時間(CPUティック単位)です。- サンプリングレートに基づいて、イベントを記録するかどうかを決定します。
rate > cycles && runtime·fastrand1()%rate > cycles
の条件は、ブロッキング時間が短いイベントを確率的にスキップすることで、プロファイリングのオーバーヘッドを削減するためのサンプリングロジックです。 - イベントが記録される場合、現在のコールスタックを取得し、
stkbucket
を呼び出して対応するBucket
を取得します。 - その
Bucket
のcount
(イベント回数)とcycles
(合計ブロッキング時間)をインクリメントします。
src/pkg/runtime/chan.c
および src/pkg/runtime/sema.goc
これらのファイルでは、チャネル操作やセマフォ操作の際に、ブロッキング時間の計測とruntime·blockevent
の呼び出しが追加されています。
// src/pkg/runtime/chan.c (抜粋)
// chansend, chanrecv 関数内
t0 = 0;
mysg.releasetime = 0;
if(runtime·blockprofilerate > 0) {
t0 = runtime·cputicks(); // ブロッキング開始時刻を記録
mysg.releasetime = -1; // ブロッキング中であることを示す
}
// ...
// ブロッキング解除後
if(mysg.releasetime > 0)
runtime·blockevent(mysg.releasetime - t0, 2); // ブロッキング時間を計算し、イベントを記録
ゴルーチンがブロックされる直前にruntime·cputicks()
で開始時刻t0
を記録し、ブロックが解除された後に再度runtime·cputicks()
を呼び出して終了時刻を取得します。その差分をruntime·blockevent
に渡してプロファイリングデータを更新します。mysg.releasetime
は、SudoG
(チャネル待機中のゴルーチン情報)やSema
(セマフォ待機中のゴルーチン情報)構造体に追加されたフィールドで、ブロッキング解除時刻を保持するために使用されます。
src/pkg/runtime/pprof/pprof.go
var blockProfile = &Profile{
name: "block",
count: countBlock,
write: writeBlock,
}
// countBlock returns the number of records in the blocking profile.
func countBlock() int {
n, _ := runtime.BlockProfile(nil)
return n
}
// writeBlock writes the current blocking profile to w.
func writeBlock(w io.Writer, debug int) error {
// ...
n, ok := runtime.BlockProfile(nil)
for {
p = make([]runtime.BlockProfileRecord, n+50)
n, ok = runtime.BlockProfile(p)
if ok {
p = p[:n]
break
}
}
sort.Sort(byCycles(p)) // ブロッキング時間でソート
// ... プロファイルデータをフォーマットしてwに書き出す ...
for i := range p {
r := &p[i]
fmt.Fprintf(w, "%v %v @", r.Cycles, r.Count) // CyclesとCountを出力
for _, pc := range r.Stack() {
fmt.Fprintf(w, " %#x", pc) // スタックトレースのPC値を出力
}
fmt.Fprint(w, "\n")
if debug > 0 {
printStackRecord(w, r.Stack(), false) // 詳細なスタックトレースを出力
}
}
// ...
}
runtime/pprof
パッケージは、pprof
ツールがプロファイルデータを読み取るためのインターフェースを提供します。
blockProfile
という新しいProfile
インスタンスが追加され、name
が"block"に設定されています。countBlock
関数は、現在のブロッキングプロファイルのレコード数を返します。writeBlock
関数は、runtime.BlockProfile
からデータを取得し、それをCycles
(合計ブロッキング時間)でソートします。その後、各BlockProfileRecord
のCycles
、Count
、およびスタックトレースを整形してio.Writer
に書き出します。これにより、go tool pprof
が解析できる形式のプロファイルデータが生成されます。
関連リンク
- Go Issue 3946: pprof: add goroutine blocking profiling: http://code.google.com/p/go/issues/detail?id=3946 このコミットの背景となったGoのIssueトラッカーのエントリです。SVG形式のプロファイル例が添付されています。
- Go CL 6443115: https://golang.org/cl/6443115 このコミットに対応するGoのコードレビュー(Change List)です。
参考にした情報源リンク
- Go プロファイリングの公式ドキュメント:
https://pkg.go.dev/runtime/pprof
Goの
pprof
パッケージに関する公式ドキュメント。 - Go のプロファイリング (日本語記事): https://zenn.dev/hsaki/articles/go-profiling-basics Goのプロファイリングに関する基本的な概念と使用方法を解説した記事。
- Google Performance Tools (gperftools): https://github.com/gperftools/gperftools Google Perf ToolsのGitHubリポジトリ。このコミットのブロッキングプロファイリング機能のインスピレーション元。