[インデックス 11705] ファイルの概要
このコミットは、Go言語のランタイムとプロファイリングツールであるpprof
に、スレッド作成のプロファイリング機能を追加するものです。これにより、プログラム内でどのようにスレッドが生成されたかを追跡し、多数のスレッドが予期せず実行されるような問題のデバッグを支援します。ヒーププロファイリングと同様の概念で、OSスレッドごとに256バイトという低メモリ使用量で、高い効果が期待されます。
コミット
commit 5b93fc9da67d59159e8c30494136c9761e350c1f
Author: Russ Cox <rsc@golang.org>
Date: Wed Feb 8 10:33:54 2012 -0500
runtime, pprof: add profiling of thread creation
Same idea as heap profile: how did each thread get created?
Low memory (256 bytes per OS thread), high reward for
programs that suddenly have many threads running.
Fixes #1477.
R=golang-dev, r, dvyukov
CC=golang-dev
https://golang.org/cl/5639059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5b93fc9da67d59159e8c30494136c9761e350c1f
元コミット内容
runtime, pprof: add profiling of thread creation
Same idea as heap profile: how did each thread get created?
Low memory (256 bytes per OS thread), high reward for
programs that suddenly have many threads running.
Fixes #1477.
変更の背景
この変更は、Goプログラムが予期せず多数のOSスレッドを生成してしまう問題に対処するために導入されました。コミットメッセージに「Fixes #1477」とあるように、Go issue 1477がこの機能追加の直接的なトリガーとなっています。
Go issue 1477は、「runtime: pprof for thread creation」というタイトルで、スレッド作成のプロファイリング機能の要望が議論されていました。既存のpprof
ツールはCPU使用率、メモリ割り当て(ヒープ)、ブロック操作などのプロファイリングをサポートしていましたが、OSスレッドの作成元を特定する機能は不足していました。
特に、Goのランタイムは必要に応じてOSスレッド(M: Machine)を生成しますが、これが過剰になるとシステムリソースを消費し、パフォーマンスに悪影響を与える可能性があります。例えば、Cgo呼び出しや特定のネットワーク操作、あるいはGoルーチンがOSスレッドにピン留めされるようなシナリオで、意図しないスレッドの増加が発生することがありました。このような問題が発生した場合、どのコードパスがスレッド作成を引き起こしているのかを特定することが困難でした。
ヒーププロファイリングがメモリリークの原因を特定するのに役立つのと同様に、スレッド作成プロファイリングは、スレッドの過剰な生成の原因を特定し、デバッグを容易にすることを目的としています。これにより、開発者はスレッドのライフサイクルをより詳細に理解し、リソースの効率的な利用を促進できるようになります。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムは、Goランタイム上で動作します。ランタイムは、ガベージコレクション、スケジューリング(GoルーチンとOSスレッドのマッピング)、メモリ管理、システムコールなど、プログラムの実行に必要な多くの低レベルなタスクを処理します。Goの並行性モデルはGoルーチン(軽量スレッド)に基づいていますが、これらのGoルーチンは最終的にOSスレッド上で実行されます。ランタイムは、GoルーチンをOSスレッドに多重化(M:Nスケジューリング)し、必要に応じて新しいOSスレッドを生成・破棄します。
-
pprof (プロファイリングツール):
pprof
は、Goプログラムのパフォーマンスを分析するためのツールです。CPU使用率、メモリ割り当て(ヒープ)、ブロック操作、ミューテックス競合など、様々な種類のプロファイルを収集し、可視化することができます。pprof
は、プログラムの実行中に特定のイベント(例: 関数呼び出し、メモリ割り当て)が発生した時点のコールスタックをサンプリングし、そのデータを集計してレポートを生成します。これにより、パフォーマンスのボトルネックやリソースリークの原因を特定するのに役立ちます。pprof
は、Goプログラムが提供するHTTPエンドポイント(例:/debug/pprof/heap
,/debug/pprof/profile
)からプロファイルデータを取得し、それを解析してグラフやテキスト形式で表示します。 -
コールスタック (Call Stack): プログラムが実行される際、関数呼び出しのシーケンスはコールスタックに記録されます。ある関数が別の関数を呼び出すと、呼び出された関数の情報(引数、ローカル変数、戻りアドレスなど)がスタックにプッシュされます。関数が終了すると、その情報がスタックからポップされます。プロファイリングでは、特定のイベント(例: スレッド作成)が発生した時点のコールスタックを記録することで、そのイベントがどのコードパスから引き起こされたかを特定できます。
-
OSスレッド (Operating System Thread): OSスレッドは、オペレーティングシステムによって管理される実行の単位です。各スレッドは独自の実行コンテキスト(プログラムカウンタ、レジスタ、スタックなど)を持ち、CPUによってスケジューリングされます。Goランタイムは、Goルーチンを実行するためにこれらのOSスレッドを利用します。GoルーチンはOSスレッドよりも軽量であり、GoランタイムがGoルーチンをOSスレッドに効率的にマッピングすることで、高い並行性を実現しています。
-
runtime
パッケージ: Goの標準ライブラリの一部であり、Goランタイムとのインターフェースを提供します。ガベージコレクションの制御、Goルーチンの管理、プロファイリングデータの収集など、低レベルなランタイム機能へのアクセスを提供します。 -
net/http/pprof
パッケージ: GoのHTTPサーバーにpprof
のプロファイリングエンドポイントを公開するためのパッケージです。これにより、実行中のGoアプリケーションからHTTP経由でプロファイルデータを取得できるようになります。
技術的詳細
このコミットは、Goランタイムとpprof
ツールに「スレッド作成プロファイリング」機能を追加します。これは、ヒーププロファイリングと同様のメカニズムで動作し、新しいOSスレッドが作成されるたびに、その作成をトリガーしたコールスタックを記録します。
主要なメカニズム:
-
スタックトレースの記録:
src/pkg/runtime/runtime.h
のM
構造体(OSスレッドを表すランタイム内部の構造体)にcreatestack [32]uintptr
というフィールドが追加されました。これは、スレッドが作成された時点のコールスタックを最大32フレームまで記録するための配列です。src/pkg/runtime/proc.c
のmcommoninit
関数(新しいMが初期化される際に呼び出される)内で、runtime·callers(1, m->createstack, nelem(m->createstack));
が呼び出され、現在のコールスタックがm->createstack
に保存されます。これにより、各OSスレッドがどのGoコードパスから生成されたかを追跡できるようになります。 -
プロファイルデータの収集:
src/pkg/runtime/debug.go
にThreadProfileRecord
構造体とThreadProfile
関数が追加されました。ThreadProfileRecord
: スレッド作成時のスタックトレースを保持する構造体です。ThreadProfile(p []ThreadProfileRecord) (n int, ok bool)
: 現在のスレッドプロファイルデータを取得するためのランタイム関数です。p
に十分な容量があれば、スレッド作成レコードをコピーし、コピーされたレコードの数n
とtrue
を返します。容量が不足していれば、必要なレコード数n
とfalse
を返します。これにより、呼び出し元は必要なバッファサイズを事前に知ることができます。
-
pprof形式での出力:
src/pkg/runtime/pprof/pprof.go
にWriteThreadProfile(w io.Writer) error
関数が追加されました。この関数は、runtime.ThreadProfile
からスレッド作成プロファイルデータを取得し、それをpprof
ツールが解析できるテキスト形式でio.Writer
に書き出します。出力形式は、各スレッド作成イベントを@ <PC1> <PC2> ... <PCn>
のように、スタックトレースのプログラムカウンタ(PC)のリストとして表現します。 -
HTTPエンドポイントの追加:
src/pkg/net/http/pprof/pprof.go
に/debug/pprof/thread
という新しいHTTPエンドポイントが追加されました。このエンドポイントは、Thread(w http.ResponseWriter, r *http.Request)
関数によって処理され、pprof.WriteThreadProfile
を呼び出してスレッド作成プロファイルデータをHTTPレスポンスとして提供します。これにより、go tool pprof http://localhost:6060/debug/pprof/thread
のようなコマンドでプロファイルデータをリモートから取得できるようになります。 -
pprof
ツールの更新: Perlスクリプトであるsrc/cmd/prof/pprof
が更新され、新しい/pprof/thread
エンドポイントを認識し、スレッドプロファイルデータを解析・表示できるようになりました。これには、THREAD_PAGE
定数の追加、ParseProfileURL
での認識、Units
関数での「threads」単位の追加、そしてReadThreadProfile
サブルーチンの実装が含まれます。ReadThreadProfile
は、pprof
形式のテキストデータを読み込み、スタックトレースを解析してプロファイルグラフを構築します。
メモリ使用量:
コミットメッセージにあるように、この機能はOSスレッドごとに256バイト(uintptr
が8バイトの場合、32 * 8 = 256バイト)のメモリしか消費しません。これは、スタックトレースを保存するための固定サイズの配列によるもので、非常に効率的です。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/cmd/dist/build.c
:cmd/prof
のビルドプロセスに、pprof
ツールをツールディレクトリにコピーする処理が追加されました。 -
src/cmd/prof/pprof
:my $THREAD_PAGE = "/pprof/thread";
の追加: 新しいプロファイルエンドポイントを定義。ParseProfileURL
関数の正規表現の更新:/pprof/thread
パスを認識するように変更。Units
関数の更新: プロファイルタイプがthread
の場合に「threads」という単位を返すように変更。ReadThreadProfile
サブルーチンの追加: スレッドプロファイルデータを読み込み、解析するロジックを実装。これは、@ <PC1> <PC2> ...
形式のデータをパースし、プロファイル構造体に変換します。ReadProfile
関数の更新: ヘッダーがthread creation profile:
で始まる場合にReadThreadProfile
を呼び出すように変更。
-
src/pkg/net/http/pprof/pprof.go
:init()
関数の更新:/debug/pprof/thread
パスにThread
ハンドラを登録。Thread
関数の追加: HTTPリクエストを受け取り、pprof.WriteThreadProfile
を呼び出してスレッドプロファイルデータをレスポンスとして書き出す。- コメントの更新:
go tool pprof http://localhost:6060/debug/pprof/thread
の使用例を追加。
-
src/pkg/runtime/Makefile
: ビルドシステムがMake.dist
を使用するように変更され、ランタイムのビルドプロセスが簡素化されました。これは直接的な機能追加ではなく、ビルドプロセスのクリーンアップです。 -
src/pkg/runtime/debug.go
:ThreadProfileRecord
構造体の追加: スレッド作成時のスタックトレースを保持。ThreadProfile(p []ThreadProfileRecord) (n int, ok bool)
関数の追加: ランタイムからスレッド作成プロファイルデータを取得するためのGoインターフェース。
-
src/pkg/runtime/mprof.goc
:ThreadProfile
関数の実装:debug.go
で宣言されたThreadProfile
のCGo実装。ランタイム内部のM
構造体からcreatestack
情報を収集し、GoのThreadProfileRecord
スライスにコピーします。
-
src/pkg/runtime/pprof/pprof.go
:WriteThreadProfile(w io.Writer) error
関数の追加:runtime.ThreadProfile
からデータを取得し、pprof
ツールが解析できるテキスト形式で出力するロジックを実装。
-
src/pkg/runtime/proc.c
:mcommoninit
関数の変更: 新しいOSスレッド(M)が初期化される際に、runtime·callers(1, m->createstack, nelem(m->createstack));
を呼び出して、そのスレッドを作成した時点のコールスタックをm->createstack
に記録するように変更。m->alllink
への追加処理の移動:createstack
の記録後にm->alllink
への追加が行われるように順序が変更されました。
-
src/pkg/runtime/runtime.h
:M
構造体へのuintptr createstack[32];
フィールドの追加: 各OSスレッドの作成スタックトレースを保存するための配列。
コアとなるコードの解説
このコミットの核心は、GoランタイムがOSスレッドを生成する際に、その生成元のコールスタックを記録し、それをpprof
ツールで可視化できるようにする点にあります。
ランタイム側の変更 (src/pkg/runtime/proc.c
, src/pkg/runtime/runtime.h
, src/pkg/runtime/debug.go
, src/pkg/runtime/mprof.goc
):
src/pkg/runtime/runtime.h
のM
構造体にcreatestack [32]uintptr
が追加されたことで、各OSスレッド(M
)が自身の生成時のスタックトレースを保持できるようになりました。これは、スレッドの「誕生」の瞬間を記録する「DNA」のようなものです。src/pkg/runtime/proc.c
のmcommoninit
関数は、新しいOSスレッドが初期化されるたびに呼び出されます。この関数内でruntime·callers
が呼び出され、現在の実行コンテキストのコールスタックがm->createstack
にコピーされます。これにより、どのGoルーチンが、どの関数呼び出しパスを経て新しいOSスレッドを必要としたのかが記録されます。src/pkg/runtime/debug.go
のThreadProfileRecord
とThreadProfile
関数は、Goプログラムがランタイムからこのスレッド作成スタックトレースデータにアクセスするための高レベルなインターフェースを提供します。ThreadProfile
は、現在存在するすべてのOSスレッドのcreatestack
情報を集約し、ThreadProfileRecord
のスライスとして返します。
pprofツール側の変更 (src/pkg/net/http/pprof/pprof.go
, src/pkg/runtime/pprof/pprof.go
, src/cmd/prof/pprof
):
src/pkg/runtime/pprof/pprof.go
のWriteThreadProfile
関数は、runtime.ThreadProfile
から生のスレッド作成データを取得し、それをpprof
ツールが理解できる標準的なテキスト形式(@ <PC1> <PC2> ...
)に変換して出力します。この形式は、各行が1つのスレッド作成イベントを表し、それに続く数値がそのイベントを引き起こしたコールスタックのプログラムカウンタ(PC)を示します。src/pkg/net/http/pprof/pprof.go
は、このWriteThreadProfile
関数をHTTPエンドポイント/debug/pprof/thread
として公開します。これにより、開発者は実行中のGoアプリケーションにHTTPリクエストを送るだけで、スレッド作成プロファイルデータを取得できるようになります。src/cmd/prof/pprof
(Perlスクリプト)は、この新しいHTTPエンドポイントからデータを取得し、ReadThreadProfile
サブルーチンで解析し、既存のpprof
の可視化機能(グラフ、テキストレポートなど)を使って、スレッド作成のホットスポットを特定できるようにします。例えば、どの関数が最も多くのスレッドを作成しているか、そのスレッドがどのようなコールスタックで生成されたか、といった情報を視覚的に把握できるようになります。
この一連の変更により、Go開発者は、アプリケーションが予期せず多数のOSスレッドを生成する問題に直面した場合でも、その原因を効率的に特定し、デバッグできるようになりました。これは、Goアプリケーションのパフォーマンスチューニングとリソース管理において非常に重要な機能追加と言えます。
関連リンク
- Go issue 1477: https://github.com/golang/go/issues/1477
- Go CL 5639059: https://golang.org/cl/5639059
参考にした情報源リンク
- Go issue 1477の議論内容
- Go言語の
pprof
ツールのドキュメント(一般的な使用法とプロファイルの種類について) - GoランタイムのM:Nスケジューリングモデルに関する情報
- Goのソースコード(特に
runtime
パッケージとcmd/prof
ディレクトリ)