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

[インデックス 18454] ファイルの概要

このコミットは、GoランタイムにおけるWindows環境でのプロファイリングの挙動に関する修正です。具体的には、ネットワークI/Oのポーリング(netpoll)において、GetQueuedCompletionStatus (GQCS) 関数がブロックされている際に、プロファイラがそのスレッドを不適切に「ブロックされている」と記録し、プロファイルデータが歪む問題を解決します。このブロックは実際にはリソースを消費しない待機状態であるため、プロファイルから除外することで、より正確なパフォーマンス分析を可能にします。

コミット

commit e5a4211b36ca776189730de6f1ab4403dde46f46
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Feb 11 13:41:46 2014 +0400

    runtime: do not profile blocked netpoll on windows
    There is frequently a thread hanging on GQCS,
    currently it skews profiles towards netpoll,
    but it is not bad and is not consuming any resources.
    
    R=alex.brainman
    CC=golang-codereviews
    https://golang.org/cl/61560043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e5a4211b36ca776189730de6f1ab4403dde46f46

元コミット内容

--- a/src/pkg/runtime/netpoll_windows.c
+++ b/src/pkg/runtime/netpoll_windows.c
@@ -94,13 +94,17 @@ retry:
 		n = nelem(entries) / runtime·gomaxprocs;
 		if(n < 8)
 			n = 8;
+		if(block)
+			m->blocked = true;
 		if(runtime·stdcall(runtime·GetQueuedCompletionStatusEx, 6, iocphandle, entries, (uintptr)n, &n, (uintptr)wait, (uintptr)0) == 0) {
+			m->blocked = false;
 			errno = runtime·getlasterror();
 			if(!block && errno == WAIT_TIMEOUT)
 				return nil;
 			runtime·printf("netpoll: GetQueuedCompletionStatusEx failed (errno=%d)\n", errno);
 			runtime·throw("netpoll: GetQueuedCompletionStatusEx failed");
 		}
+		m->blocked = false;
 		for(i = 0; i < n; i++) {
 			op = entries[i].op;
 			errno = 0;
@@ -113,7 +117,10 @@ retry:
 		op = nil;
 		errno = 0;
 		qty = 0;
+		if(block)
+			m->blocked = true;
 		if(runtime·stdcall(runtime·GetQueuedCompletionStatus, 5, iocphandle, &qty, &key, &op, (uintptr)wait) == 0) {
+			m->blocked = false;
 			errno = runtime·getlasterror();
 			if(!block && errno == WAIT_TIMEOUT)
 				return nil;
@@ -123,6 +130,7 @@ retry:
 			}
 			// dequeued failed IO packet, so report that
 		}
+		m->blocked = false;
 		handlecompletion(&gp, op, errno, qty);
 	}
 	if(block && gp == nil)

変更の背景

このコミットの背景には、GoランタイムのプロファイリングにおけるWindows固有の課題がありました。Windows環境では、Goのネットワークポーリング(netpoll)メカニズムがI/O Completion Ports (IOCP) を利用しており、その主要なAPIであるGetQueuedCompletionStatus (GQCS) または GetQueuedCompletionStatusEx を呼び出します。

これらの関数は、完了したI/O操作を待機するためにブロックする可能性があります。Goのプロファイラは、OSスレッド(GoランタイムのM (Machine) に対応)がブロックされている時間を計測し、プロファイルデータに含めます。しかし、GQCSがブロックしている状態は、実際にはCPUリソースを消費しているわけではなく、単にI/Oイベントの発生を効率的に待機している状態です。

コミットメッセージにあるように、「頻繁にGQCSでハングしているスレッドがある」にもかかわらず、「それは悪くなく、リソースを消費していない」という状況でした。この「ハング」は、プロファイラにとってはスレッドが長時間ブロックされているように見え、結果としてプロファイルデータにおいてnetpollが不当に大きな割合を占めることになり、実際のパフォーマンスボトルネックを特定する上で誤解を招く可能性がありました。

この変更は、このような誤解を招くプロファイリングデータを排除し、Go開発者がより正確なパフォーマンス分析を行えるようにすることを目的としています。つまり、リソースを消費しない待機状態をプロファイルから除外することで、真のCPU使用率やブロッキング時間を反映したプロファイルを提供します。

前提知識の解説

Goランタイムの基本要素 (M, P, G)

Goの並行処理モデルは、M (Machine)、P (Processor)、G (Goroutine) という3つの主要な要素によって支えられています。

  • G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。Goの関数呼び出しがゴルーチンとして実行されます。
  • P (Processor): 論理プロセッサを表します。Goスケジューラは、GをPに割り当て、PはMに割り当てられます。Pの数は通常、GOMAXPROCS環境変数によって制御され、デフォルトではCPUのコア数に設定されます。Pは、Gを実行するためのコンテキストとリソースを提供します。
  • M (Machine): OSスレッドを表します。Goランタイムは、Gを実行するためにOSスレッドを使用します。MはPからGを受け取り、それを実行します。Mがシステムコールなどでブロックされると、Goランタイムは別のMを起動するか、既存のMを再利用して、他のPに割り当てられたGの実行を継続しようとします。

このコミットで変更されるm->blockedは、このM(OSスレッド)が現在ブロック状態にあるかどうかを示すフラグです。プロファイラは、このフラグの状態を監視して、スレッドがブロックされている時間を計測します。

Goのプロファイリング

Goには、プログラムのパフォーマンス特性を分析するための強力なプロファイリングツールが組み込まれています。主にpprofツールが使用されます。pprofは、CPU使用率、メモリ割り当て、ゴルーチンのブロッキング時間、ミューテックスの競合など、さまざまな種類のプロファイルデータを収集・可視化できます。

  • CPUプロファイリング: プログラムがCPU時間をどこで消費しているかを特定します。
  • ブロッキングプロファイリング: ゴルーチンがシステムコール、チャネル操作、ミューテックスのロックなどでブロックされている時間を特定します。このコミットは、特にこのブロッキングプロファイリングの精度に関わります。

プロファイラは、定期的に(例えば100Hzで)実行中のスレッドの状態をサンプリングし、そのスレッドがどの関数を実行しているか、またはどのような理由でブロックされているかを記録します。

ネットワークポーリング (netpoll)

Goのランタイムは、効率的なネットワークI/Oを実現するために「ネットワークポーラー(netpoll)」と呼ばれるメカニズムを使用しています。これは、複数のネットワーク接続からのI/Oイベント(データの読み書き準備完了など)を単一のスレッドで効率的に監視し、イベントが発生した際に適切なゴルーチンをスケジューラにディスパッチする役割を担います。

netpollは、OSが提供するI/O多重化APIを利用します。

  • Linux: epoll
  • macOS/FreeBSD: kqueue
  • Windows: I/O Completion Ports (IOCP)

これらのAPIは、多数のI/O操作を非同期的に処理し、完了したイベントのみを通知することで、スケーラブルなネットワークアプリケーションを実現します。

Windows I/O Completion Ports (IOCP)

WindowsにおけるI/O Completion Ports (IOCP) は、高性能な非同期I/Oを実現するためのカーネルレベルのメカニズムです。特に、多数の同時接続を処理するサーバーアプリケーションでその真価を発揮します。

IOCPの基本的な動作は以下の通りです。

  1. I/O操作の開始: アプリケーションは非同期I/O操作(例: ReadFile, WriteFile, WSARecv, WSASendなど)を開始し、その操作が完了した際に通知を受け取るためにIOCPに関連付けます。
  2. 完了通知のキューイング: I/O操作が完了すると、その結果(完了バイト数、エラーコードなど)がIOCPのキューにポストされます。
  3. 完了通知の取得: アプリケーションは、GetQueuedCompletionStatus (GQCS) または GetQueuedCompletionStatusEx 関数を呼び出して、IOCPキューから完了通知を取得します。これらの関数は、キューに通知が来るまで(または指定されたタイムアウト期間まで)呼び出し元のスレッドをブロックします。

GQCSがブロックしている間、そのスレッドはCPUを消費していません。カーネル内でI/Oイベントの発生を待機している状態です。これが、Goのプロファイラがこの状態を「ブロック」として誤って解釈し、プロファイルデータを歪ませる原因となっていました。

技術的詳細

このコミットは、GoランタイムのWindows固有のネットワークポーリング実装ファイルである src/pkg/runtime/netpoll_windows.c に変更を加えています。

変更の核心は、GetQueuedCompletionStatusEx および GetQueuedCompletionStatus という2つのWindows API呼び出しの直前と直後に、GoランタイムのM(OSスレッド)の状態を示す m->blocked フラグを適切に設定・解除することです。

Goランタイムのプロファイラは、OSスレッド(M)がブロックされているかどうかを判断するために、この m->blocked フラグを参照します。

  1. GetQueuedCompletionStatusEx の場合:

    • runtime·stdcall(runtime·GetQueuedCompletionStatusEx, ...) の呼び出し直前に m->blocked = true; が追加されます。これは、これからGQCSExがブロックする可能性があるため、Mがブロック状態に入ることをプロファイラに通知します。
    • GQCSExの呼び出しが完了した後(成功または失敗にかかわらず)、m->blocked = false; が追加されます。これにより、Mがブロック状態から解放されたことをプロファイラに通知します。
  2. GetQueuedCompletionStatus の場合:

    • 同様に、runtime·stdcall(runtime·GetQueuedCompletionStatus, ...) の呼び出し直前に m->blocked = true; が追加されます。
    • GQCSの呼び出しが完了した後、m->blocked = false; が追加されます。

これらの変更により、GQCS関数がI/Oイベントを待機してブロックしている間、m->blocked フラグは true に設定されます。しかし、Goのプロファイラは、この特定の種類のブロック(GQCSによるI/O待機)を、CPUを消費する「真の」ブロッキングとは区別して扱います。つまり、GQCSによる待機は、プロファイルデータにおける「ブロッキング時間」の計算から除外されるようになります。

これは、GQCSがカーネルレベルで効率的な待機メカニズムを提供し、その間スレッドがCPUサイクルを消費しないという特性に基づいています。プロファイラがこの状態を無視することで、開発者はアプリケーションが実際にCPUを消費している場所や、他の理由でゴルーチンがブロックされている場所(例: ロックの競合、チャネルの待機など)に焦点を当てることができます。

この修正は、Windows環境でのGoアプリケーションのプロファイリングの精度を向上させ、誤解を招くデータによって開発者が誤った最適化の判断を下すことを防ぎます。

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

--- a/src/pkg/runtime/netpoll_windows.c
+++ b/src/pkg/runtime/netpoll_windows.c
@@ -94,13 +94,17 @@ retry:
 		n = nelem(entries) / runtime·gomaxprocs;
 		if(n < 8)
 			n = 8;
+		if(block)
+			m->blocked = true;
 		if(runtime·stdcall(runtime·GetQueuedCompletionStatusEx, 6, iocphandle, entries, (uintptr)n, &n, (uintptr)wait, (uintptr)0) == 0) {
+			m->blocked = false;
 			errno = runtime·getlasterror();
 			if(!block && errno == WAIT_TIMEOUT)
 				return nil;
 			runtime·printf("netpoll: GetQueuedCompletionStatusEx failed (errno=%d)\n", errno);
 			runtime·throw("netpoll: GetQueuedCompletionStatusEx failed");
 		}
+		m->blocked = false;
 		for(i = 0; i < n; i++) {
 			op = entries[i].op;
 			errno = 0;
@@ -113,7 +117,10 @@ retry:
 		op = nil;
 		errno = 0;
 		qty = 0;
+		if(block)
+			m->blocked = true;
 		if(runtime·stdcall(runtime·GetQueuedCompletionStatus, 5, iocphandle, &qty, &key, &op, (uintptr)wait) == 0) {
+			m->blocked = false;
 			errno = runtime·getlasterror();
 			if(!block && errno == WAIT_TIMEOUT)
 				return nil;
@@ -123,6 +130,7 @@ retry:
 			}
 			// dequeued failed IO packet, so report that
 		}
+		m->blocked = false;
 		handlecompletion(&gp, op, errno, qty);
 	}
 	if(block && gp == nil)

コアとなるコードの解説

このコミットで追加された主要なコードは、m->blocked = true;m->blocked = false; の行です。これらは、GoランタイムのM(OSスレッド)がブロック状態にあることをプロファイラに通知するためのフラグ操作です。

  1. if(block) 条件: if(block) という条件は、netpoll関数がブロッキングモードで呼び出されている場合にのみ、m->blockedフラグを設定することを示しています。netpollは、I/Oイベントを待機するためにブロックする場合と、単にイベントがあるかチェックする(非ブロッキング)場合の両方で呼び出される可能性があります。プロファイリングの歪みはブロッキング待機時に発生するため、この条件が重要です。

  2. m->blocked = true;: この行は、runtime·stdcall を介して GetQueuedCompletionStatusEx または GetQueuedCompletionStatus が呼び出される直前に挿入されています。これは、GoランタイムのM(OSスレッド)が、これからWindowsのIOCP API呼び出しによってブロックされる可能性があることを示します。このフラグがtrueに設定されることで、プロファイラはスレッドがブロック状態に入ったことを認識します。

  3. m->blocked = false;: この行は、GetQueuedCompletionStatusEx または GetQueuedCompletionStatus の呼び出しが完了した直後(成功またはエラーにかかわらず)に挿入されています。これは、Mがブロック状態から解放され、再び実行可能になったことをプロファイラに通知します。

これらの変更により、Goのプロファイラは、GetQueuedCompletionStatus系の関数によるI/O待機中のスレッドを、プロファイルデータにおける「ブロッキング時間」の計算から除外するようになります。これにより、プロファイルはCPUを実際に消費している部分や、他のGoランタイムレベルでのブロッキング(例: ゴルーチン間のチャネル通信待機、ミューテックスロック待機など)をより正確に反映するようになります。結果として、開発者はアプリケーションの真のパフォーマンスボトルネックを特定しやすくなります。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメントおよびソースコード
  • Microsoft LearnのI/O Completion Portsに関するドキュメント
  • Goランタイムのスケジューラに関する一般的な解説記事
  • Goのプロファイリングに関する技術ブログや記事