[インデックス 14252] ファイルの概要
このコミットは、Go言語のネットワークパッケージにおけるWindows環境での接続リセット問題(特に、別のゴルーチンが読み込みでブロックされている間に接続が閉じられた場合に発生する問題)を修正するためのものです。CancelIo
とCancelIoEx
というWindows API関数を導入し、IO操作のキャンセル処理を改善することで、接続リセットの発生を抑制し、関連する複数のバグを修正しています。
コミット
commit fa3e4fc4290aaa901c4f4de2bb7cdd71d4e586a3
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Oct 31 10:24:37 2012 +1100
net: fix connection resets when closed on windows
It is common to close network connection while another goroutine is
blocked reading on another goroutine. This sequence corresponds to
windows calls to WSARecv to start io, followed by GetQueuedCompletionStatus
that blocks until io completes, and, finally, closesocket called from
another thread. We were expecting that closesocket would unblock
GetQueuedCompletionStatus, and it does, but not always
(http://code.google.com/p/go/issues/detail?id=4170#c5). Also that sequence
results in connection is being reset.
This CL inserts CancelIo between GetQueuedCompletionStatus and closesocket,
and waits for both WSARecv and GetQueuedCompletionStatus to complete before
proceeding to closesocket. This seems to fix both connection resets and
issue 4170. It also makes windows code behave similar to unix version.
Unfortunately, CancelIo needs to be called on the same thread as WSARecv.
So we have to employ strategy we use for connections with deadlines to
every connection now. It means, there are 2 unavoidable thread switches
for every io. Some newer versions of windows have new CancelIoEx api that
doesn't have these drawbacks, and this CL uses this capability when available.
As time goes by, we should have less of CancelIo and more of CancelIoEx
systems. Computers with CancelIoEx are also not affected by issue 4195 anymore.
Fixes #3710
Fixes #3746
Fixes #4170
Partial fix for issue 4195
R=golang-dev, mikioh.mikioh, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6604072
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fa3e4fc4290aaa901c4f4de2bb7cdd71d4e586a3
元コミット内容
Go言語のネットワークパッケージにおいて、Windows環境でネットワーク接続が閉じられた際に発生する接続リセットの問題を修正します。特に、あるゴルーチンがネットワーク読み込みでブロックされている間に、別のゴルーチンがその接続を閉じようとした場合に問題が発生していました。
この問題は、Windows APIのWSARecv
でIOを開始し、GetQueuedCompletionStatus
でIO完了を待機し、別のスレッドからclosesocket
を呼び出すという一連の操作で顕著でした。closesocket
がGetQueuedCompletionStatus
のブロックを解除することを期待していましたが、常にそうなるわけではなく、結果として接続リセットが発生していました(Go issue #4170のコメント5を参照)。
この変更では、GetQueuedCompletionStatus
とclosesocket
の間にCancelIo
を挿入し、WSARecv
とGetQueuedCompletionStatus
の両方が完了するまで待機してからclosesocket
に進むようにします。これにより、接続リセットとissue #4170の両方が修正され、Windows版のコードがUnix版の動作に近づきます。
ただし、CancelIo
はWSARecv
と同じスレッドから呼び出す必要があるという制約があります。このため、デッドラインを持つ接続で使用していた戦略をすべてのIOに適用する必要があり、結果としてIOごとに2回のスレッド切り替えが発生します。新しいバージョンのWindowsには、この欠点がないCancelIoEx
APIがあり、この変更では利用可能な場合にCancelIoEx
を使用します。CancelIoEx
を使用できるシステムでは、issue #4195の影響も受けません。
このコミットは、以下のGo issueを修正または部分的に修正します。
- Fixes #3710
- Fixes #3746
- Fixes #4170
- Partial fix for issue #4195
変更の背景
Go言語のネットワークパッケージは、クロスプラットフォームでの一貫した動作を目指していますが、OS固有のAPIや動作の違いにより、プラットフォーム間で差異が生じることがあります。特にWindows環境では、非同期I/O(Overlapped I/O)のキャンセル処理が複雑であり、複数のゴルーチンが同じソケットに対して操作を行う場合に競合状態や予期せぬ動作が発生する可能性がありました。
このコミットの背景には、以下の具体的な問題がありました。
- 接続リセット問題: ネットワーク接続が閉じられる際に、まだ保留中のI/O操作がある場合、WindowsがTCP接続を「リセット」してしまう現象が発生していました。これは、アプリケーションが予期しないエラーを受け取ったり、通信が途中で切断されたりする原因となります。
GetQueuedCompletionStatus
のブロック解除の不確実性: WindowsのI/O完了ポート(IOCP)モデルでは、GetQueuedCompletionStatus
関数がI/O操作の完了を待機します。しかし、別のスレッドからclosesocket
が呼び出された際に、この待機が常に適切に解除されるわけではないという問題が報告されていました(Go issue #4170)。これにより、ゴルーチンが無限にブロックされる可能性がありました。CancelIo
の制約: 既存のCancelIo
APIは、I/O操作を開始したスレッドと同じスレッドから呼び出す必要があるという制約がありました。Goの並行処理モデルでは、異なるゴルーチンがI/O操作を開始し、別のゴルーチンがそれをキャンセルしようとすることが一般的であるため、この制約は実装上の課題となっていました。CancelIoEx
の登場: Windows Vista以降で導入されたCancelIoEx
APIは、CancelIo
の制約を解消し、任意のスレッドからI/O操作をキャンセルできるようになりました。この新しいAPIを活用することで、より効率的で堅牢なI/Oキャンセル処理を実装できる可能性がありました。
これらの問題を解決し、GoのネットワークパッケージがWindows環境でもより安定して動作するようにするために、このコミットが作成されました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
- Go言語のゴルーチンとチャネル:
- ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
- チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルを通じてゴルーチンは同期し、安全にデータを共有できます。
- Windows APIとネットワークプログラミング:
- Winsock (Windows Sockets): WindowsにおけるネットワークプログラミングのためのAPIです。TCP/IP通信などを扱う際に使用されます。
- 非同期I/O (Overlapped I/O): WindowsにおけるI/O処理のモデルの一つで、I/O操作が完了するのを待たずに、すぐに制御を呼び出し元に返すことができます。I/O操作の完了は、I/O完了ポート(IOCP)などのメカニズムを通じて通知されます。
- I/O完了ポート (IOCP - I/O Completion Port): Windowsにおける高性能な非同期I/O処理のためのメカニズムです。複数のI/O操作の完了を効率的に処理し、スレッドプールと組み合わせてスケーラブルなサーバーアプリケーションを構築するのに適しています。
WSARecv
: Winsockの関数で、非同期にデータを受信するために使用されます。OVERLAPPED
構造体を使用して非同期I/O操作を開始します。GetQueuedCompletionStatus
: IOCPに関連する関数で、I/O完了ポートにキューイングされた完了パケット(完了したI/O操作の情報)を取得するために使用されます。この関数は、完了パケットが利用可能になるまでブロックすることができます。closesocket
: Winsockの関数で、ソケットを閉じるために使用されます。CancelIo
: 指定されたファイルハンドルに対して発行された、呼び出し元のスレッドによって発行された保留中のI/O操作をキャンセルするWindows API関数です。重要なのは「呼び出し元のスレッドによって発行された」という点です。CancelIoEx
: Windows Vista以降で導入された新しいAPIで、指定されたファイルハンドルに対して発行された、任意のスレッドによって発行された保留中のI/O操作をキャンセルできます。CancelIo
の制約を解消します。ERROR_OPERATION_ABORTED
: I/O操作がキャンセルされた場合に返されるWindowsのエラーコードです。ERROR_NOT_FOUND
:CancelIoEx
が、キャンセルしようとしたI/O操作が既に完了している場合に返す可能性のあるエラーコードです。
- Goの
net
パッケージとsyscall
パッケージ:net
パッケージ: Go言語の標準ライブラリで、ネットワークI/O機能を提供します。TCP/UDP接続、HTTPクライアント/サーバーなどが含まれます。syscall
パッケージ: OS固有のシステムコールへの低レベルなインターフェースを提供します。このコミットでは、Windows API関数を直接呼び出すために使用されています。netFD
:net
パッケージ内部で使用されるネットワークファイルディスクリプタを表す構造体です。ソケットハンドルやI/O操作の状態を管理します。anOp
: 非同期I/O操作の基本となる構造体で、OVERLAPPED
構造体を内包し、I/O完了ポートからの結果を受け取るためのチャネルなどを含みます。ioSrv
: I/O操作のサブミットとキャンセルを処理するサービスです。特にCancelIo
の制約がある場合に、専用のゴルーチンでI/O操作を処理します。
これらの知識を前提として、コミットの技術的詳細を掘り下げていきます。
技術的詳細
このコミットの主要な技術的変更点は、Windowsにおける非同期I/Oのキャンセル処理の改善にあります。
-
CancelIoEx
の優先的な利用:- コミットは、まずシステムが
CancelIoEx
APIをサポートしているかどうかをsyscall.LoadCancelIoEx()
で確認します。 canCancelIO
というグローバル変数が導入され、CancelIoEx
が利用可能かどうかを示します。CancelIoEx
が利用可能な場合、I/O操作のキャンセルはよりシンプルになり、任意のスレッドからsyscall.CancelIoEx(syscall.Handle(o.Op().fd.sysfd), &o.o)
を呼び出すことで行われます。これにより、CancelIo
が持つ「同じスレッドから呼び出す必要がある」という制約が解消され、スレッド切り替えのオーバーヘッドが削減されます。CancelIoEx
がERROR_NOT_FOUND
を返す場合(I/Oが既に完了している場合)はエラーとして扱わず、それ以外のエラーはパニックを引き起こすようにしています。
- コミットは、まずシステムが
-
CancelIo
フォールバック時の専用ゴルーチン:CancelIoEx
が利用できない(canCancelIO
がfalse
)場合、Goは従来のCancelIo
を使用します。CancelIo
はI/O操作を開始したスレッドと同じスレッドから呼び出す必要があるため、ioSrv.ProcessRemoteIO()
という専用のゴルーチンが導入されます。このゴルーチンはruntime.LockOSThread()
によって特定のOSスレッドにロックされ、I/O操作のサブミットとキャンセルをこの単一のスレッドで行います。- これにより、他のゴルーチンからのI/Oリクエストは
ioSrv.submchan
チャネルを通じてこの専用ゴルーチンに送信され、キャンセルリクエストはioSrv.canchan
チャネルを通じて送信されます。このアプローチは、デッドラインを持つ接続で以前から使用されていたものと同様です。 - この方式では、I/O操作ごとに2回のスレッド切り替え(リクエストの送信と結果の受信)が避けられないオーバーヘッドとなります。
-
netFD.Close()
の改善:netFD.Close()
メソッドが大幅に修正され、保留中のI/O操作を適切にキャンセルし、完了を待機するようになりました。fd.closec
という新しいチャネルが導入され、Close()
が呼び出された際にこのチャネルが閉じられます。これにより、ExecIO
内でI/O操作がブロックされているゴルーチンがfd.closec
からの通知を受け取り、I/O操作をキャンセルするトリガーとなります。Close()
は、読み込みと書き込みのI/O操作が完了するまで(fd.rio.Lock()
とfd.wio.Lock()
を使用して)待機するようになりました。これにより、ソケットが閉じられる前にすべての保留中のI/Oが適切に処理されることが保証されます。- 以前の
closesocket
の呼び出しは、fd.closing && fd.sysref == 0 && fd.sysfd != syscall.InvalidHandle
という条件が満たされた場合にのみ行われるようになりました。これは、ソケットへの参照がすべてなくなり、かつ閉じられる準備ができた場合にのみ実際のソケットクローズが行われることを意味します。
-
I/O操作のロック順序の変更:
Read
,ReadFrom
,Write
,WriteTo
などのI/O操作メソッドにおいて、fd.incref()
とfd.decref()
の呼び出しが、対応するI/Oロック(fd.rio.Lock()
やfd.wio.Lock()
)の外側に移動されました。- これにより、参照カウントの操作がI/Oロックの競合を避けるように改善されています。
-
テストケースの追加:
net/net_test.go
にTestTCPClose
が追加され、別のゴルーチンが読み込みでブロックされている間にTCP接続が閉じられた場合の動作をテストします。net/timeout_test.go
にTestReadWriteDeadline
が追加され、読み書きのデッドラインが設定されたI/O操作が適切にタイムアウトし、キャンセルされることをテストします。このテストはcanCancelIO
がtrue
の場合にのみ実行されます。
これらの変更により、Windows環境におけるGoのネットワークI/Oの堅牢性と信頼性が大幅に向上しました。特に、CancelIoEx
の利用は、パフォーマンスと実装の複雑さの両面で大きな改善をもたらしています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にsrc/pkg/net/fd_windows.go
とsrc/pkg/syscall/syscall_windows.go
、および関連するテストファイルです。
-
src/pkg/net/fd_windows.go
:canCancelIO
変数の導入とinit()
関数での初期化(syscall.LoadCancelIoEx()
の呼び出し)。anOp
、bufOp
、resultSrv
、ioSrv
などの構造体やメソッドのコメントが「io」から「IO」に変更され、一貫性が向上。ioSrv.ExecIO
メソッドのロジックが大幅に変更され、canCancelIO
の値に基づいてCancelIoEx
を使用するか、ProcessRemoteIO
経由でCancelIo
を使用するかが分岐。ExecIO
内のキャンセル処理で、fd.closec
チャネルからの通知を待機するselect
文が追加。netFD
構造体にclosec chan bool
が追加され、allocFD
で初期化。netFD.Close()
メソッドが大幅に修正され、fd.closec
を閉じ、読み書きのI/Oロックを待機し、保留中のI/Oをキャンセルするロジックが追加。Read
,ReadFrom
,Write
,WriteTo
メソッド内のincref
/decref
とロックの順序が変更。
-
src/pkg/syscall/syscall_windows.go
:CancelIoEx
のシステムコール定義が追加。LoadCancelIoEx()
関数が追加され、procCancelIoEx.Find()
を呼び出してCancelIoEx
が利用可能かを確認。ERROR_NOT_FOUND
エラーコードがconst
に追加。
-
src/pkg/net/net_test.go
:TestTCPClose
関数が追加され、TCP接続のクローズ時の動作をテスト。
-
src/pkg/net/timeout_test.go
:TestReadWriteDeadline
関数が追加され、読み書きのデッドラインが設定されたI/O操作のタイムアウトとキャンセルをテスト。canCancelIO
がtrue
の場合にのみ実行される。
-
src/pkg/syscall/zsyscall_windows_386.go
およびsrc/pkg/syscall/zsyscall_windows_amd64.go
:procCancelIoEx
の定義と、CancelIoEx
関数の実装が追加。
これらの変更は、GoのネットワークスタックのWindows固有の部分に深く関わっており、非同期I/Oのライフサイクル管理とエラー処理を改善しています。
コアとなるコードの解説
ここでは、特に重要な変更点であるnet/fd_windows.go
のExecIO
メソッドとnetFD.Close
メソッドを中心に解説します。
ioSrv.ExecIO
メソッドの変更
func (s *ioSrv) ExecIO(oi anOpIface, deadline int64) (int, error) {
var err error
o := oi.Op()
if canCancelIO { // CancelIoEx が利用可能か
err = oi.Submit() // 直接 I/O をサブミット
} else {
// CancelIo しか利用できない場合、専用スレッドにリクエストを送信
s.submchan <- oi
err = <-o.errnoc
}
// ... (エラーハンドリング部分は省略) ...
// デッドラインが設定されている場合、タイマーを設定
var timer <-chan time.Time
if deadline != 0 {
dt := deadline - time.Now().UnixNano()
if dt < 1 {
dt = 1
}
t := time.NewTimer(time.Duration(dt) * time.Nanosecond)
defer t.Stop()
timer = t.C
}
// I/O 完了、タイマー、またはクローズ通知を待機
var r ioResult
var cancelled bool
select {
case r = <-o.resultc: // I/O 完了
case <-timer: // デッドラインによるタイムアウト
cancelled = true
case <-o.fd.closec: // 接続クローズによるキャンセル
cancelled = true
}
if cancelled {
// I/O をキャンセル
if canCancelIO {
err := syscall.CancelIoEx(syscall.Handle(o.Op().fd.sysfd), &o.o)
// ERROR_NOT_FOUND は I/O が既に完了していることを意味するため、エラーではない
if err != nil && err != syscall.ERROR_NOT_FOUND {
panic(err) // その他のエラーはパニック
}
} else {
// CancelIo の場合、専用スレッドにキャンセルリクエストを送信
s.canchan <- oi
<-o.errnoc // キャンセル操作の完了を待機
}
// I/O がキャンセルされるか、成功するまで待機
r = <-o.resultc
if r.err == syscall.ERROR_OPERATION_ABORTED { // I/O がキャンセルされた場合
r.err = syscall.EWOULDBLOCK // EWOULDBLOCK に変換
}
}
// ... (結果の処理部分は省略) ...
}
canCancelIO
による分岐:ExecIO
の冒頭でcanCancelIO
(CancelIoEx
が利用可能か)によってI/Oのサブミット方法が分岐します。CancelIoEx
が利用可能なら直接oi.Submit()
を呼び出し、そうでなければioSrv.ProcessRemoteIO
ゴルーチンにチャネル経由でリクエストを送信します。select
文による待機: I/O操作の完了を待つselect
文が強化されました。o.resultc
からのI/O完了通知。timer
チャネルからのデッドラインタイムアウト通知。- 新しく追加された
o.fd.closec
からの接続クローズ通知。 これが、別のゴルーチンからのClose()
呼び出しによってI/O操作を早期に中断させるための重要なメカニズムです。
- キャンセル処理:
cancelled
フラグがtrue
の場合、I/O操作のキャンセルが試みられます。ここでもcanCancelIO
によってCancelIoEx
とCancelIo
のどちらを使用するかが分岐します。CancelIoEx
の場合、syscall.CancelIoEx
を呼び出します。ERROR_NOT_FOUND
はI/Oが既に完了していることを意味するため、エラーとして扱われません。CancelIo
の場合、s.canchan
を通じて専用ゴルーチンにキャンセルリクエストを送信し、その完了を待ちます。- キャンセル後、最終的なI/O結果を
o.resultc
から受け取ります。ERROR_OPERATION_ABORTED
(キャンセルされたことを示す)が返された場合、syscall.EWOULDBLOCK
に変換されます。
netFD.Close()
メソッドの変更
func (fd *netFD) Close() error {
// ... (incref の処理は省略) ...
defer fd.decref()
// 保留中の読み書き操作をアンブロック
close(fd.closec) // ここで fd.closec チャネルを閉じる
// 読み書きゴルーチンが終了するまで待機
fd.rio.Lock() // 読み込み I/O のロックを取得
defer fd.rio.Unlock()
fd.wio.Lock() // 書き込み I/O のロックを取得
defer fd.wio.Unlock()
// ... (実際の closesocket 呼び出しは decref 内の条件付きロジックに移動) ...
return nil
}
close(fd.closec)
: これがClose()
メソッドの最も重要な変更点です。fd.closec
チャネルを閉じることで、ExecIO
内で<-o.fd.closec
を待機しているゴルーチンが即座にブロック解除され、I/O操作のキャンセル処理に進むことができます。これにより、closesocket
が呼び出される前に保留中のI/O操作が適切に処理されるようになります。- I/Oロックの待機:
fd.rio.Lock()
とfd.wio.Lock()
をdefer
付きで呼び出すことで、Close()
が、現在進行中の読み書きI/O操作が完了するまで待機することを保証します。これにより、ソケットが閉じられる前にすべてのI/Oがクリーンアップされるため、競合状態や接続リセットのリスクが低減されます。
これらの変更により、GoのネットワークパッケージはWindows環境でより堅牢になり、特に並行処理下での接続クローズ時の問題が大幅に改善されました。CancelIoEx
の利用は、パフォーマンスと実装のシンプルさの両面で大きな進歩です。
関連リンク
- Go issue #3710: https://code.google.com/p/go/issues/detail?id=3710
- Go issue #3746: https://code.google.com/p/go/issues/detail?id=3746
- Go issue #4170: https://code.google.com/p/go/issues/detail?id=4170
- Go issue #4195: https://code.google.com/p/go/issues/detail?id=4195
- Go CL 6604072: https://golang.org/cl/6604072
参考にした情報源リンク
CancelIo
function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-cancelioCancelIoEx
function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-cancelioex- I/O Completion Ports (Windows): https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
- Go Concurrency Patterns: Pipelines and Cancellation: https://go.dev/blog/pipelines (チャネルによるキャンセルパターンに関する一般的なGoのプラクティス)
- Go issue #4170のコメント5: コミットメッセージで参照されている具体的な問題の議論。
- Go言語の
net
パッケージとsyscall
パッケージのソースコード。 - Go言語のドキュメントとブログ記事。
- Winsockに関する一般的な情報。
- TCP接続のリセットに関する一般的なネットワーク知識。