Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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に適用したものです。

技術的詳細

ゴルーチンブロッキングプロファイリングは、プログラム実行中にゴルーチンがブロックされたイベントをサンプリングし、そのブロッキングが発生したコールスタックとブロッキング時間を記録します。

  1. サンプリングレートの設定: runtime.SetBlockProfileRate(rate int)関数を使用して、ブロッキングイベントのサンプリングレートを設定します。rateはナノ秒単位で、プロファイラは平均してrateナノ秒ごとに1つのブロッキングイベントをサンプリングすることを目指します。rate=1を設定すると、すべてのブロッキングイベントが記録されます。rate <= 0でプロファイリングを無効にできます。
  2. ブロッキングイベントの検出: Goランタイム内のチャネル操作 (chan.c) やセマフォ操作 (sema.goc) など、ゴルーチンがブロックされる可能性のある箇所にフックが追加されます。ゴルーチンがブロック状態に入る直前にタイムスタンプが記録され、ブロック状態から解放されたときに再度タイムスタンプが記録されます。この差分がブロッキング時間となります。
  3. スタックトレースの収集: ブロッキングイベントが発生した際、その時点のゴルーチンのコールスタックが収集されます。
  4. プロファイルデータの集計: 収集されたブロッキング時間とスタックトレースは、runtime/mprof.goc内のプロファイリングバケットに集計されます。同じコールスタックで発生したブロッキングイベントは、その回数 (Count) と合計ブロッキング時間 (Cycles) が加算されます。
  5. プロファイルの取得: runtime.BlockProfile([]BlockProfileRecord) (int, bool)関数を使用して、集計されたブロッキングプロファイルデータを取得できます。BlockProfileRecord構造体には、ブロッキング回数、合計ブロッキング時間、およびスタックトレースが含まれます。
  6. 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を取得します。
    • そのBucketcount(イベント回数)と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(合計ブロッキング時間)でソートします。その後、各BlockProfileRecordCyclesCount、およびスタックトレースを整形して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)です。

参考にした情報源リンク