Goプログラムの動作を `go tool trace` で解明する
Emily Parker
Product Engineer · Leapcell

はじめに
並行プログラミングの世界では、ゴルーチンと実行フローの複雑なダンスを理解することは、 formidable な課題となり得ます。Goは軽量なゴルーチンとチャネルで並行処理を簡素化しますが、パフォーマンスの問題や予期しないレイテンシの診断は、しばしば、水面下で何が起こっているのかをより深く理解する必要があります。pprof
のような従来のプロファイリングツールは、CPUやメモリのホットスポットを特定するのに優れていますが、並行操作、スケジューラ、そしてガーベージコレクション の影響を真に理解するために必要な、きめ細やかなタイムラインベースのビューを欠いていることがよくあります。まさにここで go tool trace
が真価を発揮します。これはGoプログラムの実行に関する比類のない視覚化を提供し、開発者がゴルーチンの相互作用を観察し、スケジューリングの遅延を特定し、レイテンシの根本原因を特定することを可能にします。この記事では、go tool trace
の機能について詳しく説明し、その使用法をガイドして、Goアプリケーションの並行動作を解明し、そのパフォーマンスを最適化します。
Goトレースの理解
実例に入る前に、go tool trace
に関連するコアコンセプトについての共通理解を確立しましょう。
ゴルーチン (Goroutine): Go の軽量な並行実行ユニットで、少数のオペレーティングシステムスレッドに多重化されています。これらは、並行Goプログラムにおける主要なアクターです。
スケジューラ (Scheduler): Goランタイムのコンポーネントで、利用可能なOSスレッドにゴルーチンを管理および配布する責任があります。どのゴルーチンがいつ、どこで実行されるかを決定します。
イベント (Event): Goプログラムの実行中に発生した重要な出来事の記録。go tool trace
は、ゴルーチンの作成/破棄、ゴルーチンブロック/アンブロック、システムコール、ガーベージコレクションサイクル、ネットワーク操作など、さまざまな種類のイベントを収集します。
トレースファイル (Trace File): runtime/trace
パッケージによって生成されるバイナリファイルで、収集されたすべてのイベントのシーケンシャルログが含まれています。このファイルは、その後 go tool trace
によって処理され、視覚化が生成されます。
トレーシング (Tracing): プログラムの実行中にイベントを記録するようにインストゥルメント化するプロセス。Goでは、通常、runtime/trace
パッケージをインポートし、trace.Start
と trace.Stop
を呼び出すことで行われます。
go tool trace
ユーティリティは、基本的にトレースファイルを入力として受け取り、それをインタラクティブなWebベースの視覚化にレンダリングします。この視覚化はイベントのタイムラインを提供し、ゴルーチンの状態、OSスレッドのアクティビティ、およびガーベージコレクタの影響を示します。これらのイベントを観察することで、開発者は以下を行うことができます。
- ゴルーチンライフサイクルの可視化: ゴルーチンがいつ作成され、実行を開始し、ブロックし、最終的に終了するかを確認します。
- スケジューリング遅延の特定: ゴルーチンが実行可能であるにもかかわらず、利用可能なOSスレッドを待っているインスタンスを観察し、潜在的な競合や過負荷のスケジューラを示唆します。
- レイテンシソースの診断: 遅延に寄与している特定の操作(例: システムコール、ネットワークI/O、チャネル操作、ミューテックス競合)を特定します。
- ガーベージコレクションの影響の理解: GCサイクルがいつ発生し、どのくらいの時間がかかり、ゴルーチン実行にどのような影響を与えるかを確認します。
- ゴルーチンブロックパターンの分析: ゴルーチンがブロックされている理由(例: チャネル、ミューテックス、ネットワークI/Oでの待機)を判断します。
トレースファイルの生成
トレースファイルを生成するには、runtime/trace
パッケージをインポートし、トレースしたいコードを trace.Start
と trace.Stop
でラップする必要があります。
I/O操作やチャネル通信を含む、並行処理のシミュレーションを行う簡単なプログラムを考えてみましょう。
package main import ( "fmt" "io/ioutil" "log" "net/http" "os" runtime "runtime" "runtime/trace" "sync" time "time" ) func fetchURL(url string, result chan<- string, wg *sync.WaitGroup) { defer wg.Done() log.Printf("Fetching %s", url) resp, err := http.Get(url) if err != nil { result <- fmt.Sprintf("Error fetching %s: %v", url, err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { result <- fmt.Sprintf("Error reading body from %s: %v", url, err) return } log.Printf("Finished fetching %s, size: %d bytes", url, len(body)) result <- fmt.Sprintf("Fetched %s: %d bytes", url, len(body)) } func heavyComputation(id int) { log.Printf("Goroutine %d starting heavy computation", id) sum := 0 for i := 0; i < 1_000_000_000; i++ { sum += i } log.Printf("Goroutine %d finished heavy computation. Sum: %d", id, sum) } func main() { // トレースファイルを作成 f, err := os.Create("trace.out") if err != nil { log.Fatalf("failed to create trace file: %v", err) } defer func() { if err := f.Close(); err != nil { log.Fatalf("failed to close trace file: %v", err) } }() // トレーシングを開始 if err := trace.Start(f); err != nil { log.Fatalf("failed to start tracing: %v", err) } defer trace.Stop() fmt.Println("Tracing started...") var wg sync.WaitGroup resultChan := make(chan string, 3) urls := []string{ "https://www.google.com", "https://www.example.com", "https://go.dev/", } for _, url := range urls { wg.Add(1) go fetchURL(url, resultChan, &wg) } // CPUバウンドなゴルーチンを開始 goroutineCount := runtime.GOMAXPROCS(-1) if goroutineCount == 0 { goroutineCount = 1 // 少なくとも1つのCPUバウンドゴルーチンを確保 } for i := 0; i < goroutineCount; i++ { wg.Add(1) go func(id int) { defer wg.Done() heavyComputation(id) }(i) } // 短いメイン処理をシミュレート time.Sleep(100 * time.Millisecond) go func() { wg.Wait() close(resultChan) }() for res := range resultChan { fmt.Println(res) } fmt.Println("Tracing finished.") }
このプログラムを実行してトレースファイルを生成するには:
go run your_program.go
これにより、現在のディレクトリにtrace.out
ファイルが作成されます。
トレースの可視化
trace.out
ファイルができたら、go tool trace
を使用して可視化できます。
go tool trace trace.out
このコマンドは、トレースの視覚化を表示するWebブラウザを起動します。メインページにはいくつかのリンクがあり、それぞれがトレースデータに異なるビューを提供します。最も一般的で洞察に富むビューは「View trace」であり、インタラクティブなタイムラインが表示されます。
トレースUIの主要セクション
「View trace」ページに入ると、複雑ですが情報量の多いインターフェースが表示されます。重要な部分を分解しましょう。
- 概要タイムライン: 一番上に、この圧縮されたビューはトレース全体の期間を示しています。ドラッグして領域を選択するとズームインできます。
- ゴルーチン (Goroutines): このセクションは、トレース期間中にアクティブなすべてのゴルーチンをリストします。各行はゴルーチンを表し、そのタイムラインは状態変化(実行中、実行可能、ブロック、syscall、GCアシスト)を示します。
- 実行中 (Running): ゴルーチンはP(Processor、論理Goスケジューラコンテキスト)で積極的に実行されています。
- 実行可能 (Runnable): ゴルーチンは実行準備ができていますが、Pを待っています。
- ブロック (Blocked): ゴルーチンは何らかのイベント(例: チャネル操作、ミューテックス、ネットワークI/O、システムコール)を待っています。
- システムコール (Syscall): ゴルーチンはシステムコールを実行しています。
- GCアシスト: ゴルーチンはガーベージコレクタをアシストしています。
- OSスレッド (Procs): ゴルーチンの下に、Goランタイムがゴルーチンを実行するために使用するOSスレッド(P0、P1など)が表示されます。このビューは、どのゴルーチンがいつどのPで実行されているかを示します。
- ヒープとGC (Heap and GC): このセクションは、ヒープサイズとガーベージコレクションイベントのタイムラインを提供します。メジャーおよびマイナーGCサイクルとその期間を観察できます。
- イベント (Events): 特定のイベントの詳細なログ。通常、特定の領域にズームインするか、「Heap」または「Network」ステータスペネルをチェックすることで見つかります。
サンプルトレースの分析
go tool trace trace.out
を実行し、「View trace」を選択した後、興味のある領域にズームインします。以下が観察できることです。
- URL取得ゴルーチン: ゴルーチンが「実行中」、「システムコール」(ネットワークI/O用)、および「ブロック」(ネットワーク応答またはチャネル書き込みを待機)の状態間を遷移するのを確認できます。
fetchURL
ゴルーチンは「システムコール」状態でかなりの時間を費やします。 - 重い計算ゴルーチン:
heavyComputation
を実行するゴルーチンは、主に「実行中」状態にあり、OSスレッド(P)を占有します。GOMAXPROCS
が重い計算ゴルーチンの数より少ない場合、これらのゴルーチンの一部が短時間「実行可能」になり、Pが空くのを待っているのが見えるかもしれません。これはCPUバウンドな作業を示唆します。 - メインゴルーチン: メインゴルーチンが
resultChan
でブロックし、fetchURL
ゴルーチンからの結果を待っているのがわかります。 - チャネル通信: ゴルーチンがチャネルで送受信する際の短い「ブロック」状態を観察します。
- ガーベージコレクション: 定期的に、「GCアシスト」イベントがゴルーチンで発生し、「ヒープ」セクションでアクティビティが表示され、ガーベージコレクタがいつ実行されるかがわかります。GCサイクルに時間がかかりすぎると、全体的なレイテンシに寄与する可能性があります。
個々のゴルーチンまたはイベントをクリックすると、ゴルーチンがブロックされたときのスタックトレースや、実行されている特定のシステムコールなど、詳細情報を取得できます。この詳細情報は、パフォーマンスの問題を診断するのに非常に役立ちます。たとえば、ゴルーチンが頻繁に「ブロック」状態にある場合、何で待っているかを調査できます。頻繁に「実行可能」である場合、アプリケーションはCPUバウンドであるか、スケジューラ競合を経験している可能性があります。
context
と trace.WithRegion
による高度なトレーシング
特に複雑なアプリケーションで、より構造化されたトレーシングのために、context.Context
と trace.WithRegion
を使用できます。これにより、コードの論理領域を定義し、トレースでの実行の内訳を確認できます。
package main // ... (前の例からのインポート) ... func main() { // ... (トレースファイル設定) ctx, task := trace.NewTask(context.Background(), "MainTask") defer task.End() fmt.Println("Tracing started...") var wg sync.WaitGroup resultChan := make(chan string, 3) urls := []string{ "https://www.google.com", "https://www.example.com", "https://go.dev/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() trace.WithRegion(ctx, "FetchRegion for " + url, func() { fetchURL(url, resultChan, &wg) // 希望に応じてwg.Done()をWithRegionの外に配置 }) }(url) } goroutineCount := runtime.GOMAXPROCS(-1) if goroutineCount == 0 { goroutineCount = 1 } for i := 0; i < goroutineCount; i++ { wg.Add(1) go func(id int) { defer wg.Done() trace.WithRegion(ctx, fmt.Sprintf("HeavyCompRegion %d", id), func() { heavyComputation(id) }) }(i) } trace.WithRegion(ctx, "MainWorkRegion", func() { time.Sleep(100 * time.Millisecond) }) go func() { wg.Wait() close(resultChan) }() trace.WithRegion(ctx, "ResultConsumptionRegion", func() { for res := range resultChan { fmt.Println(res) } }) fmt.Println("Tracing finished.") }
trace.NewTask
と trace.WithRegion
を使用すると、トレースUIで明確にラベル付けされた領域が表示され、トレースイベントとアプリケーションの特定のコード部分を関連付けることが容易になります。これは、どの論理セクションがレイテンシに最も貢献しているかを特定するのに非常に役立ちます。
結論
go tool trace
は、並行アプリケーションのパフォーマンスを理解し最適化しようとするGo開発者にとって不可欠なツールです。ゴルーチンスケジューリング、システムコール、ガーベージコレクション、およびカスタムイベントの詳細なインタラクティブな視覚化を提供することで、並行実行の不透明な世界を透過的なタイムラインに変換します。開発者がボトルネックを迅速に特定し、トリッキーな並行問題を診断し、最終的により効率的で応答性の高いGoプログラムを構築できるようにします。go tool trace
をマスターすれば、Goアプリケーションのランタイム動作の中心部へのX線のような視界を獲得できます。