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

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

このコミットは、Go言語の標準ライブラリ net パッケージにおいて、Windows環境でのネットワーク接続確立処理を改善するものです。具体的には、DialTimeout 関数が内部的に利用する接続メカニズムを、従来の非効率な「goroutine-racing」方式から、Windows固有の高性能なWinsock APIである ConnectEx を利用するように変更しています。これにより、特にWindows環境におけるネットワーク接続のパフォーマンス向上とリソース効率の改善が期待されます。

コミット

commit 810e439859afe4c2b526c4baed9d01fd72d499ed
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Jan 11 12:42:09 2013 +1100

    net: use windows ConnectEx to dial (when possible)
    
    Update #2631.
    Update #3097.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7061060

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

https://github.com/golang/go/commit/810e439859afe4c2b526c4baed9d01fd72d499ed

元コミット内容

このコミットの元の内容は、Go言語の net パッケージにおいて、Windows環境でのネットワーク接続処理を最適化するために、ConnectEx というWindows APIを使用するように変更することです。これにより、既存の非効率な DialTimeout の実装(goroutine-racing)を改善し、パフォーマンスとリソース管理を向上させることが目的です。コミットメッセージには、関連するGoのIssue番号 #2631#3097 が記載されており、これらの問題の解決に貢献することが示唆されています。

変更の背景

この変更の背景には、Go言語の net パッケージがWindows環境でネットワーク接続を確立する際に抱えていたパフォーマンスとリソース効率の問題があります。

  1. 既存の DialTimeout の非効率性: コミット前のGoの net パッケージでは、DialTimeout 関数がWindowsおよびPlan 9環境において「goroutine-racing」という比較的非効率な実装を使用していました。これは、接続試行とタイムアウト処理を別々のgoroutineで並行して実行し、先に完了した方を採用するという方式です。このアプローチは、デッドラインをポーリングサーバーにプッシュダウンする機能が未実装であったために採用されていましたが、リソース消費やパフォーマンスの点で最適ではありませんでした。特に、多数の同時接続を扱うようなシナリオでは、この非効率性が顕著になる可能性がありました。

  2. Windows固有の高性能APIの活用: Windowsオペレーティングシステムには、Winsock APIの一部として ConnectEx という高度な接続関数が提供されています。ConnectEx は、非同期I/O (Overlapped I/O) と組み合わせることで、非常に効率的なソケット接続確立を可能にします。従来の connect 関数と比較して、ConnectEx は接続確立と同時にデータの送受信を開始できるなど、いくつかの最適化が施されています。Goの net パッケージがWindows上で動作する以上、このようなプラットフォーム固有の高性能APIを活用しない手はありませんでした。

  3. 関連Issueの解決: コミットメッセージに記載されているIssue #2631#3097 は、おそらくWindows環境でのネットワーク関連のパフォーマンス問題や、DialTimeout の実装に関する懸念を扱っていたと考えられます。このコミットは、ConnectEx の導入によってこれらのIssueを解決または改善することを目的としています。

これらの背景から、Go開発チームはWindows環境でのネットワーク処理のボトルネックを解消し、より堅牢で高性能なネットワークスタックを提供するために、ConnectEx の導入を決定したと考えられます。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

1. Winsock API (Windows Sockets API)

Winsockは、Windowsオペレーティングシステムにおけるネットワークプログラミングインターフェースです。Unix系OSにおけるBerkeley Sockets APIに相当し、TCP/IPプロトコルを用いたネットワーク通信を行うための関数群を提供します。Go言語の net パッケージがWindows上で動作する際には、内部的にこのWinsock APIを呼び出しています。

2. ConnectEx 関数

ConnectEx は、Winsock 2.0で導入された拡張関数の一つで、非同期I/O (Overlapped I/O) をサポートするソケット接続関数です。従来の connect 関数と比較して、以下の特徴があります。

  • 非同期操作: ConnectEx は非同期で動作し、呼び出し元スレッドをブロックせずに接続処理をバックグラウンドで実行できます。これは、I/O完了ポート (IOCP) と組み合わせて使用することで、多数の同時接続を効率的に処理するサーバーアプリケーションなどで特に有効です。
  • 事前バインドの要件: ConnectEx を呼び出す前に、ソケットをローカルアドレスにバインド(bind 関数)しておく必要があります。これは通常の connect とは異なる点です。
  • データ送信の統合: 接続確立と同時に最初のデータを送信する機能(lpSendBuffer 引数)も持っています。これにより、ネットワークラウンドトリップを削減し、パフォーマンスを向上させることができます。
  • SO_UPDATE_CONNECT_CONTEXT: ConnectEx で接続が確立された後、ソケットのプロパティを更新するために setsockopt 関数で SO_UPDATE_CONNECT_CONTEXT オプションを使用する必要があります。これにより、ソケットが他のWinsock関数(getsockname, getpeername など)で正しく機能するようになります。

3. Go言語のネットワークI/Oモデル

Go言語の net パッケージは、プラットフォームに依存しない統一されたネットワークI/Oインターフェースを提供しますが、その内部では各OSのネイティブなI/Oメカニズムを利用しています。

  • 非同期I/Oとポーリング: Goのランタイムは、効率的なネットワークI/Oのために、OSが提供する非同期I/Oメカニズム(Linuxのepoll、macOS/BSDのkqueue、WindowsのI/O完了ポートなど)を利用しています。これにより、多数のネットワーク接続を少数のゴルーチンで効率的に処理できます。
  • netFD 構造体: net パッケージでは、ネットワークファイルディスクリプタ(ソケット)を抽象化するために netFD 構造体を使用します。この構造体は、OS固有のソケットハンドルや、I/O操作の状態を管理します。
  • syscall パッケージ: Goの syscall パッケージは、OSのシステムコールやCライブラリ関数への低レベルなインターフェースを提供します。ConnectEx のようなWindows APIをGoから呼び出す際には、この syscall パッケージを介して行われます。

4. DialTimeout の「goroutine-racing」実装

コミット前のWindowsおよびPlan 9における DialTimeout の実装は、以下のような特徴を持っていました。

  • タイムアウト処理: 接続試行とタイムアウトタイマーをそれぞれ別のgoroutineで開始します。
  • 競合: 接続が確立されるか、タイムアウトタイマーが先に発火するかの「競合」状態になります。
  • 非効率性: この方式は、OSのI/Oポーリングメカニズムにタイムアウト情報を直接プッシュダウンするのではなく、Goランタイムレベルでタイムアウトを管理するため、リソース消費が大きく、多数の接続を扱う場合にはオーバーヘッドが大きくなる傾向がありました。コミットメッセージの TODO: remove this once those are implemented. というコメントは、この非効率な実装を将来的に改善する意図があったことを示しています。

これらの知識を前提として、コミットがどのように ConnectEx を統合し、Goのネットワークスタックを改善したかを詳細に見ていきます。

技術的詳細

このコミットの技術的詳細は、主に以下の3つの領域にわたります。

  1. ConnectEx APIのGo言語からの呼び出し:

    • src/pkg/syscall/syscall_windows.goConnectEx 関数が追加されました。これは、Windowsの ConnectEx APIをGoから呼び出すためのラッパーです。
    • LoadConnectEx 関数が導入され、ConnectEx 関数のアドレスを動的に取得します。これは、ConnectEx がWinsockの拡張関数であり、すべてのシステムで利用可能とは限らないため、実行時にその存在を確認し、関数ポインタを取得する必要があるためです。WSAIoctlSIO_GET_EXTENSION_FUNCTION_POINTER を使用して、この関数ポインタを取得しています。
    • src/pkg/syscall/ztypes_windows.go には、ConnectEx の関数ポインタを取得するために必要な GUID 構造体と WSAID_CONNECTEX 定数が追加されています。また、SO_UPDATE_CONNECT_CONTEXT という新しいソケットオプションも定義されています。
  2. net パッケージにおける ConnectEx の統合:

    • src/pkg/net/fd_windows.gocanUseConnectEx 関数が追加されました。この関数は、現在のネットワークタイプ(TCP/UDPなど)とシステムが ConnectEx をサポートしているかどうかをチェックします。UDPソケットは ConnectEx でサポートされないため、udp 系のネットワークでは false を返します。
    • dialTimeout 関数がWindows固有の実装として src/pkg/net/fd_windows.go に移動・変更されました。この関数は、canUseConnectEx の結果に基づいて、ConnectEx を使用するか、従来の dialTimeoutRace (goroutine-racing) 実装を使用するかを決定します。
    • netFD 構造体の connect メソッドが大幅に修正されました。
      • ConnectEx を使用する場合、ソケットはまず bind 関数でローカルアドレスにバインドされます。これは ConnectEx の要件です。
      • connectOp という新しい構造体が定義され、ConnectEx 呼び出しのための非同期I/O操作(anOp)をカプセル化します。
      • iosrv.ExecIO を使用して ConnectEx を非同期で実行します。これはGoの内部的なI/Oポーリングメカニズムと統合されています。
      • 接続成功後、syscall.Setsockopt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_UPDATE_CONNECT_CONTEXT, ...) を呼び出してソケットのコンテキストを更新します。これは ConnectEx で確立されたソケットが他のWinsock関数で正しく動作するために必要です。
      • ConnectEx が利用できない場合は、引き続き syscall.Connect を使用するフォールバックロジックも含まれています。
  3. DialTimeout の実装変更とプラットフォームごとの分離:

    • src/pkg/net/dial.go から useDialTimeoutRace 定数と DialTimeout のWindows/Plan 9固有の分岐ロジックが削除されました。
    • 代わりに、dialTimeout 関数が各OS固有のファイル(src/pkg/net/fd_unix.go, src/pkg/net/fd_plan9.go, src/pkg/net/fd_windows.go)に定義され、それぞれのOSに最適な実装が提供されるようになりました。
      • Unix系OS (fd_unix.go) では、従来のデッドラインをポーリングサーバーにプッシュダウンする効率的な dialTimeout 実装が維持されます。
      • Plan 9 (fd_plan9.go) では、引き続き dialTimeoutRace が使用されます。コミットメッセージの TODO: fix this on plan9. は、Plan 9での改善が将来の課題であることを示しています。
      • Windows (fd_windows.go) では、前述の ConnectEx を利用した新しい dialTimeout 実装が導入されました。
    • src/pkg/net/dial_windows_test.go には、ConnectEx を使用した DialTimeout がハンドルリークを起こさないことを検証するための新しいテストが追加されました。numHandles 関数を使ってプロセスが保持するハンドルの数を計測し、接続タイムアウト後にハンドルの数が変化しないことを確認しています。

これらの変更により、Goの net パッケージはWindows環境でより効率的かつ高性能なネットワーク接続確立を実現し、OS固有の機能を最大限に活用できるようになりました。

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

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

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

    • useDialTimeoutRace 定数が削除されました。
    • DialTimeout 関数からWindows/Plan 9固有の dialTimeoutRace への分岐ロジックが削除され、代わりにプラットフォーム固有の dialTimeout 関数を呼び出すように変更されました。
  2. src/pkg/net/dial_windows_test.go:

    • 新規追加されたファイル。ConnectEx を使用した DialTimeout がハンドルリークを起こさないことを検証するテスト TestDialTimeoutHandleLeak が含まれています。
  3. src/pkg/net/fd_plan9.go:

    • dialTimeout 関数が追加され、Plan 9では引き続き dialTimeoutRace を使用するように定義されました。
  4. src/pkg/net/fd_unix.go:

    • dialTimeout 関数が追加され、Unix系OSでは既存の効率的なデッドラインプッシュダウン方式を使用するように定義されました。
  5. src/pkg/net/fd_windows.go:

    • canUseConnectEx 関数が追加され、ConnectEx の利用可能性をチェックします。
    • dialTimeout 関数がWindows固有の実装として追加され、canUseConnectEx に基づいて ConnectEx または dialTimeoutRace を選択します。
    • connectOp 構造体が追加され、ConnectEx 操作をカプセル化します。
    • netFDconnect メソッドが大幅に修正され、ConnectEx を使用した接続確立ロジック(ソケットのバインド、ConnectEx の呼び出し、SO_UPDATE_CONNECT_CONTEXT の設定)が実装されました。
  6. src/pkg/syscall/syscall_windows.go:

    • LoadConnectEx 関数が追加され、ConnectEx APIの関数ポインタを動的にロードします。
    • connectEx および ConnectEx 関数が追加され、GoからWindowsの ConnectEx APIを呼び出すための低レベルなインターフェースを提供します。
  7. src/pkg/syscall/ztypes_windows.go:

    • SO_UPDATE_CONNECT_CONTEXT 定数が追加されました。
    • IOC_OUT, IOC_IN, IOC_INOUT, IOC_WS2, SIO_GET_EXTENSION_FUNCTION_POINTER などのWinsock IOCTL関連定数が追加されました。
    • GUID 構造体と WSAID_CONNECTEX 定数が追加され、ConnectEx の関数ポインタ取得に使用されます。

これらの変更は、GoのネットワークスタックのWindows固有部分を大幅に強化し、パフォーマンスと効率を向上させるためのものです。

コアとなるコードの解説

このコミットの核となる変更は、Windows環境におけるネットワーク接続確立のロジックを、従来の非効率な「goroutine-racing」方式から、Windows固有の高性能な ConnectEx APIを利用する方式へと移行した点にあります。

src/pkg/net/fd_windows.goconnect メソッド

このメソッドは、netFD (ネットワークファイルディスクリプタ、つまりソケット) を使ってリモートアドレスに接続する際の中心的なロジックを含んでいます。

func (fd *netFD) connect(ra syscall.Sockaddr) error {
	if !canUseConnectEx(fd.net) {
		// ConnectExが利用できない場合、またはUDPソケットの場合は従来のsyscall.Connectを使用
		return syscall.Connect(fd.sysfd, ra)
	}
	// ConnectEx windows API requires an unconnected, previously bound socket.
	// ConnectExの要件:未接続で、事前にバインドされたソケットが必要
	var la syscall.Sockaddr
	switch ra.(type) {
	case *syscall.SockaddrInet4:
		la = &syscall.SockaddrInet4{} // IPv4ソケットの場合、空のIPv4アドレスでバインド
	case *syscall.SockaddrInet6:
		la = &syscall.SockaddrInet6{} // IPv6ソケットの場合、空のIPv6アドレスでバインド
	default:
		panic("unexpected type in connect") // 未知のソケットタイプ
	}
	if err := syscall.Bind(fd.sysfd, la); err != nil {
		return err // ソケットのバインドに失敗
	}
	// Call ConnectEx API.
	var o connectOp
	o.Init(fd, 'w') // connectOpを初期化し、書き込み操作として設定
	o.ra = ra       // リモートアドレスを設定
	_, err := iosrv.ExecIO(&o, fd.wdeadline.value()) // ConnectExを非同期で実行
	if err != nil {
		return err // ConnectExの実行に失敗
	}
	// Refresh socket properties.
	// ソケットのプロパティを更新(ConnectExで確立されたソケットの必須手順)
	return syscall.Setsockopt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_UPDATE_CONNECT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))
}

解説:

  1. canUseConnectEx のチェック: まず、現在のネットワークタイプ(fd.net)とシステムが ConnectEx をサポートしているかを確認します。UDPソケットの場合や、ConnectEx がロードできない場合は、従来の syscall.Connect にフォールバックします。
  2. ソケットのバインド: ConnectEx の重要な要件として、呼び出し前にソケットがローカルアドレスにバインドされている必要があります。ここでは、リモートアドレスのタイプ(IPv4またはIPv6)に応じて、対応する空のローカルアドレス(0.0.0.0 または ::)にソケットをバインドしています。
  3. connectOp の利用と非同期実行:
    • connectOp は、ConnectEx 呼び出しに必要な情報(ソケットディスクリプタ、リモートアドレス、Overlapped I/O構造体など)をカプセル化する内部構造体です。
    • o.Init(fd, 'w') で初期化され、この操作が書き込み(接続)であることを示します。
    • iosrv.ExecIO(&o, fd.wdeadline.value()) は、GoランタイムのI/Oポーリングサーバー(WindowsではI/O完了ポート)を利用して、ConnectEx を非同期で実行します。これにより、接続処理がバックグラウンドで行われ、呼び出し元のgoroutineはブロックされません。タイムアウトもこの ExecIO の中で処理されます。
  4. SO_UPDATE_CONNECT_CONTEXT: ConnectEx で接続が成功した後、ソケットの内部状態を更新するために SO_UPDATE_CONNECT_CONTEXT オプションを指定して setsockopt を呼び出す必要があります。これは、ConnectEx がソケットのコンテキストを完全に初期化しないため、他のWinsock関数(例: getsockname, getpeername)が正しく機能するようにするために不可欠なステップです。

src/pkg/syscall/syscall_windows.goConnectEx 関連関数

このファイルでは、Windows APIの ConnectEx をGoから直接呼び出すための低レベルなラッパーが提供されています。

// LoadConnectEx は ConnectEx 関数のアドレスを動的にロードします。
var connectExFunc struct {
	once sync.Once
	addr uintptr
	err  error
}

func LoadConnectEx() error {
	connectExFunc.once.Do(func() {
		var s Handle
		s, connectExFunc.err = Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) // 一時的なソケットを作成
		if connectExFunc.err != nil {
			return
		}
		defer CloseHandle(s) // 関数終了時にソケットを閉じる
		var n uint32
		// WSAIoctl を使って ConnectEx の関数ポインタを取得
		connectExFunc.err = WSAIoctl(s,
			SIO_GET_EXTENSION_FUNCTION_POINTER,
			(*byte)(unsafe.Pointer(&WSAID_CONNECTEX)),
			uint32(unsafe.Sizeof(WSAID_CONNECTEX)),
			(*byte)(unsafe.Pointer(&connectExFunc.addr)),
			uint32(unsafe.Sizeof(connectExFunc.addr)),
			&n, nil, 0)
	})
	return connectExFunc.err
}

// connectEx は ConnectEx APIの低レベルなSyscall呼び出しです。
func connectEx(s Handle, name uintptr, namelen int32, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *Overlapped) (err error) {
	r1, _, e1 := Syscall9(connectExFunc.addr, 7, uintptr(s), uintptr(name), uintptr(namelen), uintptr(unsafe.Pointer(sendBuf)), uintptr(sendDataLen), uintptr(unsafe.Pointer(bytesSent)), uintptr(unsafe.Pointer(overlapped)), 0, 0)
	if r1 == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = EINVAL // エラーコードがない場合はEINVAL
		}
	}
	return
}

// ConnectEx は Go から呼び出すための ConnectEx ラッパー関数です。
func ConnectEx(fd Handle, sa Sockaddr, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *Overlapped) error {
	err := LoadConnectEx() // ConnectEx の関数ポインタをロード
	if err != nil {
		return errorspkg.New("failed to find ConnectEx: " + err.Error())
	}
	ptr, n, err := sa.sockaddr() // Sockaddr を C のポインタと長さに変換
	if err != nil {
		return err
	}
	return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped)
}

解説:

  1. LoadConnectEx: この関数は、sync.Once を使用して ConnectEx APIの関数ポインタを一度だけロードします。WSAIoctl 関数と SIO_GET_EXTENSION_FUNCTION_POINTER IOCTLコードを使って、動的に ConnectEx のアドレスを取得します。これは、ConnectEx がすべてのWindowsバージョンで利用可能とは限らないため、実行時にその存在を確認し、関数ポインタを取得する必要があるためです。
  2. connectEx: これは、Goの Syscall9 関数を使って、ロードされた ConnectEx の関数ポインタを直接呼び出す低レベルなラッパーです。Syscall9 は、最大9つの引数を持つシステムコールを呼び出すためのGoの組み込み関数です。
  3. ConnectEx (高レベルラッパー): この関数は、LoadConnectEx を呼び出して関数ポインタが利用可能であることを確認し、Sockaddr 構造体を ConnectEx が期待する形式(ポインタと長さ)に変換してから、低レベルの connectEx を呼び出します。

これらの変更により、Goの net パッケージはWindowsのネイティブな非同期I/O機能を活用し、より効率的でスケーラブルなネットワーク接続処理を実現できるようになりました。特に、多数の同時接続を扱うアプリケーションにおいて、この最適化は大きなパフォーマンス向上をもたらす可能性があります。

関連リンク

参考にした情報源リンク