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

[インデックス 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スレッドにピン留めされるようなシナリオで、意図しないスレッドの増加が発生することがありました。このような問題が発生した場合、どのコードパスがスレッド作成を引き起こしているのかを特定することが困難でした。

ヒーププロファイリングがメモリリークの原因を特定するのに役立つのと同様に、スレッド作成プロファイリングは、スレッドの過剰な生成の原因を特定し、デバッグを容易にすることを目的としています。これにより、開発者はスレッドのライフサイクルをより詳細に理解し、リソースの効率的な利用を促進できるようになります。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムは、Goランタイム上で動作します。ランタイムは、ガベージコレクション、スケジューリング(GoルーチンとOSスレッドのマッピング)、メモリ管理、システムコールなど、プログラムの実行に必要な多くの低レベルなタスクを処理します。Goの並行性モデルはGoルーチン(軽量スレッド)に基づいていますが、これらのGoルーチンは最終的にOSスレッド上で実行されます。ランタイムは、GoルーチンをOSスレッドに多重化(M:Nスケジューリング)し、必要に応じて新しいOSスレッドを生成・破棄します。

  2. pprof (プロファイリングツール): pprofは、Goプログラムのパフォーマンスを分析するためのツールです。CPU使用率、メモリ割り当て(ヒープ)、ブロック操作、ミューテックス競合など、様々な種類のプロファイルを収集し、可視化することができます。pprofは、プログラムの実行中に特定のイベント(例: 関数呼び出し、メモリ割り当て)が発生した時点のコールスタックをサンプリングし、そのデータを集計してレポートを生成します。これにより、パフォーマンスのボトルネックやリソースリークの原因を特定するのに役立ちます。 pprofは、Goプログラムが提供するHTTPエンドポイント(例: /debug/pprof/heap, /debug/pprof/profile)からプロファイルデータを取得し、それを解析してグラフやテキスト形式で表示します。

  3. コールスタック (Call Stack): プログラムが実行される際、関数呼び出しのシーケンスはコールスタックに記録されます。ある関数が別の関数を呼び出すと、呼び出された関数の情報(引数、ローカル変数、戻りアドレスなど)がスタックにプッシュされます。関数が終了すると、その情報がスタックからポップされます。プロファイリングでは、特定のイベント(例: スレッド作成)が発生した時点のコールスタックを記録することで、そのイベントがどのコードパスから引き起こされたかを特定できます。

  4. OSスレッド (Operating System Thread): OSスレッドは、オペレーティングシステムによって管理される実行の単位です。各スレッドは独自の実行コンテキスト(プログラムカウンタ、レジスタ、スタックなど)を持ち、CPUによってスケジューリングされます。Goランタイムは、Goルーチンを実行するためにこれらのOSスレッドを利用します。GoルーチンはOSスレッドよりも軽量であり、GoランタイムがGoルーチンをOSスレッドに効率的にマッピングすることで、高い並行性を実現しています。

  5. runtimeパッケージ: Goの標準ライブラリの一部であり、Goランタイムとのインターフェースを提供します。ガベージコレクションの制御、Goルーチンの管理、プロファイリングデータの収集など、低レベルなランタイム機能へのアクセスを提供します。

  6. net/http/pprofパッケージ: GoのHTTPサーバーにpprofのプロファイリングエンドポイントを公開するためのパッケージです。これにより、実行中のGoアプリケーションからHTTP経由でプロファイルデータを取得できるようになります。

技術的詳細

このコミットは、Goランタイムとpprofツールに「スレッド作成プロファイリング」機能を追加します。これは、ヒーププロファイリングと同様のメカニズムで動作し、新しいOSスレッドが作成されるたびに、その作成をトリガーしたコールスタックを記録します。

主要なメカニズム:

  1. スタックトレースの記録: src/pkg/runtime/runtime.hM構造体(OSスレッドを表すランタイム内部の構造体)にcreatestack [32]uintptrというフィールドが追加されました。これは、スレッドが作成された時点のコールスタックを最大32フレームまで記録するための配列です。 src/pkg/runtime/proc.cmcommoninit関数(新しいMが初期化される際に呼び出される)内で、runtime·callers(1, m->createstack, nelem(m->createstack));が呼び出され、現在のコールスタックがm->createstackに保存されます。これにより、各OSスレッドがどのGoコードパスから生成されたかを追跡できるようになります。

  2. プロファイルデータの収集: src/pkg/runtime/debug.goThreadProfileRecord構造体とThreadProfile関数が追加されました。

    • ThreadProfileRecord: スレッド作成時のスタックトレースを保持する構造体です。
    • ThreadProfile(p []ThreadProfileRecord) (n int, ok bool): 現在のスレッドプロファイルデータを取得するためのランタイム関数です。pに十分な容量があれば、スレッド作成レコードをコピーし、コピーされたレコードの数ntrueを返します。容量が不足していれば、必要なレコード数nfalseを返します。これにより、呼び出し元は必要なバッファサイズを事前に知ることができます。
  3. pprof形式での出力: src/pkg/runtime/pprof/pprof.goWriteThreadProfile(w io.Writer) error関数が追加されました。この関数は、runtime.ThreadProfileからスレッド作成プロファイルデータを取得し、それをpprofツールが解析できるテキスト形式でio.Writerに書き出します。出力形式は、各スレッド作成イベントを@ <PC1> <PC2> ... <PCn>のように、スタックトレースのプログラムカウンタ(PC)のリストとして表現します。

  4. 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のようなコマンドでプロファイルデータをリモートから取得できるようになります。

  5. pprofツールの更新: Perlスクリプトであるsrc/cmd/prof/pprofが更新され、新しい/pprof/threadエンドポイントを認識し、スレッドプロファイルデータを解析・表示できるようになりました。これには、THREAD_PAGE定数の追加、ParseProfileURLでの認識、Units関数での「threads」単位の追加、そしてReadThreadProfileサブルーチンの実装が含まれます。ReadThreadProfileは、pprof形式のテキストデータを読み込み、スタックトレースを解析してプロファイルグラフを構築します。

メモリ使用量: コミットメッセージにあるように、この機能はOSスレッドごとに256バイト(uintptrが8バイトの場合、32 * 8 = 256バイト)のメモリしか消費しません。これは、スタックトレースを保存するための固定サイズの配列によるもので、非常に効率的です。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/cmd/dist/build.c: cmd/profのビルドプロセスに、pprofツールをツールディレクトリにコピーする処理が追加されました。

  2. src/cmd/prof/pprof:

    • my $THREAD_PAGE = "/pprof/thread"; の追加: 新しいプロファイルエンドポイントを定義。
    • ParseProfileURL関数の正規表現の更新: /pprof/threadパスを認識するように変更。
    • Units関数の更新: プロファイルタイプがthreadの場合に「threads」という単位を返すように変更。
    • ReadThreadProfileサブルーチンの追加: スレッドプロファイルデータを読み込み、解析するロジックを実装。これは、@ <PC1> <PC2> ...形式のデータをパースし、プロファイル構造体に変換します。
    • ReadProfile関数の更新: ヘッダーがthread creation profile:で始まる場合にReadThreadProfileを呼び出すように変更。
  3. 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の使用例を追加。
  4. src/pkg/runtime/Makefile: ビルドシステムがMake.distを使用するように変更され、ランタイムのビルドプロセスが簡素化されました。これは直接的な機能追加ではなく、ビルドプロセスのクリーンアップです。

  5. src/pkg/runtime/debug.go:

    • ThreadProfileRecord構造体の追加: スレッド作成時のスタックトレースを保持。
    • ThreadProfile(p []ThreadProfileRecord) (n int, ok bool)関数の追加: ランタイムからスレッド作成プロファイルデータを取得するためのGoインターフェース。
  6. src/pkg/runtime/mprof.goc:

    • ThreadProfile関数の実装: debug.goで宣言されたThreadProfileのCGo実装。ランタイム内部のM構造体からcreatestack情報を収集し、GoのThreadProfileRecordスライスにコピーします。
  7. src/pkg/runtime/pprof/pprof.go:

    • WriteThreadProfile(w io.Writer) error関数の追加: runtime.ThreadProfileからデータを取得し、pprofツールが解析できるテキスト形式で出力するロジックを実装。
  8. src/pkg/runtime/proc.c:

    • mcommoninit関数の変更: 新しいOSスレッド(M)が初期化される際に、runtime·callers(1, m->createstack, nelem(m->createstack));を呼び出して、そのスレッドを作成した時点のコールスタックをm->createstackに記録するように変更。
    • m->alllinkへの追加処理の移動: createstackの記録後にm->alllinkへの追加が行われるように順序が変更されました。
  9. 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.hM構造体にcreatestack [32]uintptrが追加されたことで、各OSスレッド(M)が自身の生成時のスタックトレースを保持できるようになりました。これは、スレッドの「誕生」の瞬間を記録する「DNA」のようなものです。
  • src/pkg/runtime/proc.cmcommoninit関数は、新しいOSスレッドが初期化されるたびに呼び出されます。この関数内でruntime·callersが呼び出され、現在の実行コンテキストのコールスタックがm->createstackにコピーされます。これにより、どのGoルーチンが、どの関数呼び出しパスを経て新しいOSスレッドを必要としたのかが記録されます。
  • src/pkg/runtime/debug.goThreadProfileRecordThreadProfile関数は、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.goWriteThreadProfile関数は、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の議論内容
  • Go言語のpprofツールのドキュメント(一般的な使用法とプロファイルの種類について)
  • GoランタイムのM:Nスケジューリングモデルに関する情報
  • Goのソースコード(特にruntimeパッケージとcmd/profディレクトリ)