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

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

このコミットは、Go言語のネットワークパッケージにおけるWindows環境での接続リセット問題(特に、別のゴルーチンが読み込みでブロックされている間に接続が閉じられた場合に発生する問題)を修正するためのものです。CancelIoCancelIoExという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を呼び出すという一連の操作で顕著でした。closesocketGetQueuedCompletionStatusのブロックを解除することを期待していましたが、常にそうなるわけではなく、結果として接続リセットが発生していました(Go issue #4170のコメント5を参照)。

この変更では、GetQueuedCompletionStatusclosesocketの間にCancelIoを挿入し、WSARecvGetQueuedCompletionStatusの両方が完了するまで待機してからclosesocketに進むようにします。これにより、接続リセットとissue #4170の両方が修正され、Windows版のコードがUnix版の動作に近づきます。

ただし、CancelIoWSARecvと同じスレッドから呼び出す必要があるという制約があります。このため、デッドラインを持つ接続で使用していた戦略をすべての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)のキャンセル処理が複雑であり、複数のゴルーチンが同じソケットに対して操作を行う場合に競合状態や予期せぬ動作が発生する可能性がありました。

このコミットの背景には、以下の具体的な問題がありました。

  1. 接続リセット問題: ネットワーク接続が閉じられる際に、まだ保留中のI/O操作がある場合、WindowsがTCP接続を「リセット」してしまう現象が発生していました。これは、アプリケーションが予期しないエラーを受け取ったり、通信が途中で切断されたりする原因となります。
  2. GetQueuedCompletionStatusのブロック解除の不確実性: WindowsのI/O完了ポート(IOCP)モデルでは、GetQueuedCompletionStatus関数がI/O操作の完了を待機します。しかし、別のスレッドからclosesocketが呼び出された際に、この待機が常に適切に解除されるわけではないという問題が報告されていました(Go issue #4170)。これにより、ゴルーチンが無限にブロックされる可能性がありました。
  3. CancelIoの制約: 既存のCancelIo APIは、I/O操作を開始したスレッドと同じスレッドから呼び出す必要があるという制約がありました。Goの並行処理モデルでは、異なるゴルーチンがI/O操作を開始し、別のゴルーチンがそれをキャンセルしようとすることが一般的であるため、この制約は実装上の課題となっていました。
  4. CancelIoExの登場: Windows Vista以降で導入されたCancelIoEx APIは、CancelIoの制約を解消し、任意のスレッドからI/O操作をキャンセルできるようになりました。この新しいAPIを活用することで、より効率的で堅牢なI/Oキャンセル処理を実装できる可能性がありました。

これらの問題を解決し、GoのネットワークパッケージがWindows環境でもより安定して動作するようにするために、このコミットが作成されました。

前提知識の解説

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

  1. Go言語のゴルーチンとチャネル:
    • ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
    • チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルを通じてゴルーチンは同期し、安全にデータを共有できます。
  2. 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操作が既に完了している場合に返す可能性のあるエラーコードです。
  3. 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のキャンセル処理の改善にあります。

  1. CancelIoExの優先的な利用:

    • コミットは、まずシステムがCancelIoEx APIをサポートしているかどうかをsyscall.LoadCancelIoEx()で確認します。
    • canCancelIOというグローバル変数が導入され、CancelIoExが利用可能かどうかを示します。
    • CancelIoExが利用可能な場合、I/O操作のキャンセルはよりシンプルになり、任意のスレッドからsyscall.CancelIoEx(syscall.Handle(o.Op().fd.sysfd), &o.o)を呼び出すことで行われます。これにより、CancelIoが持つ「同じスレッドから呼び出す必要がある」という制約が解消され、スレッド切り替えのオーバーヘッドが削減されます。
    • CancelIoExERROR_NOT_FOUNDを返す場合(I/Oが既に完了している場合)はエラーとして扱わず、それ以外のエラーはパニックを引き起こすようにしています。
  2. CancelIoフォールバック時の専用ゴルーチン:

    • CancelIoExが利用できない(canCancelIOfalse)場合、Goは従来のCancelIoを使用します。
    • CancelIoはI/O操作を開始したスレッドと同じスレッドから呼び出す必要があるため、ioSrv.ProcessRemoteIO()という専用のゴルーチンが導入されます。このゴルーチンはruntime.LockOSThread()によって特定のOSスレッドにロックされ、I/O操作のサブミットとキャンセルをこの単一のスレッドで行います。
    • これにより、他のゴルーチンからのI/OリクエストはioSrv.submchanチャネルを通じてこの専用ゴルーチンに送信され、キャンセルリクエストはioSrv.canchanチャネルを通じて送信されます。このアプローチは、デッドラインを持つ接続で以前から使用されていたものと同様です。
    • この方式では、I/O操作ごとに2回のスレッド切り替え(リクエストの送信と結果の受信)が避けられないオーバーヘッドとなります。
  3. 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という条件が満たされた場合にのみ行われるようになりました。これは、ソケットへの参照がすべてなくなり、かつ閉じられる準備ができた場合にのみ実際のソケットクローズが行われることを意味します。
  4. I/O操作のロック順序の変更:

    • Read, ReadFrom, Write, WriteToなどのI/O操作メソッドにおいて、fd.incref()fd.decref()の呼び出しが、対応するI/Oロック(fd.rio.Lock()fd.wio.Lock())の外側に移動されました。
    • これにより、参照カウントの操作がI/Oロックの競合を避けるように改善されています。
  5. テストケースの追加:

    • net/net_test.goTestTCPCloseが追加され、別のゴルーチンが読み込みでブロックされている間にTCP接続が閉じられた場合の動作をテストします。
    • net/timeout_test.goTestReadWriteDeadlineが追加され、読み書きのデッドラインが設定されたI/O操作が適切にタイムアウトし、キャンセルされることをテストします。このテストはcanCancelIOtrueの場合にのみ実行されます。

これらの変更により、Windows環境におけるGoのネットワークI/Oの堅牢性と信頼性が大幅に向上しました。特に、CancelIoExの利用は、パフォーマンスと実装の複雑さの両面で大きな改善をもたらしています。

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

このコミットにおけるコアとなるコードの変更箇所は、主にsrc/pkg/net/fd_windows.gosrc/pkg/syscall/syscall_windows.go、および関連するテストファイルです。

  1. src/pkg/net/fd_windows.go:

    • canCancelIO変数の導入とinit()関数での初期化(syscall.LoadCancelIoEx()の呼び出し)。
    • anOpbufOpresultSrvioSrvなどの構造体やメソッドのコメントが「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とロックの順序が変更。
  2. src/pkg/syscall/syscall_windows.go:

    • CancelIoExのシステムコール定義が追加。
    • LoadCancelIoEx()関数が追加され、procCancelIoEx.Find()を呼び出してCancelIoExが利用可能かを確認。
    • ERROR_NOT_FOUNDエラーコードがconstに追加。
  3. src/pkg/net/net_test.go:

    • TestTCPClose関数が追加され、TCP接続のクローズ時の動作をテスト。
  4. src/pkg/net/timeout_test.go:

    • TestReadWriteDeadline関数が追加され、読み書きのデッドラインが設定されたI/O操作のタイムアウトとキャンセルをテスト。canCancelIOtrueの場合にのみ実行される。
  5. src/pkg/syscall/zsyscall_windows_386.go および src/pkg/syscall/zsyscall_windows_amd64.go:

    • procCancelIoExの定義と、CancelIoEx関数の実装が追加。

これらの変更は、GoのネットワークスタックのWindows固有の部分に深く関わっており、非同期I/Oのライフサイクル管理とエラー処理を改善しています。

コアとなるコードの解説

ここでは、特に重要な変更点であるnet/fd_windows.goExecIOメソッドと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の冒頭でcanCancelIOCancelIoExが利用可能か)によって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によってCancelIoExCancelIoのどちらを使用するかが分岐します。
    • 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の利用は、パフォーマンスと実装のシンプルさの両面で大きな進歩です。

関連リンク

参考にした情報源リンク