[インデックス 17553] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージにおけるTCP接続確立の挙動を改善し、いわゆる「Happy Eyeballs」アルゴリズムに似た高速フェイルオーバー機能を追加するものです。これにより、IPv4とIPv6の両方のアドレスを持つホストへの接続時に、より迅速に利用可能なプロトコルで接続を確立できるようになります。
コミット
commit 89b26760d7dfe4ae8ee65e4b2c21fec8a15f449b
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Wed Sep 11 10:48:53 2013 -0400
net: implement TCP connection setup with fast failover
This CL adds minimal support of Happy Eyeballs-like TCP connection
setup to Dialer API. Happy Eyeballs and derivation techniques are
described in the following:
- Happy Eyeballs: Success with Dual-Stack Hosts
http://tools.ietf.org/html/rfc6555
- Analysing Dual Stack Behaviour and IPv6 Quality
http://www.potaroo.net/presentations/2012-04-17-dual-stack-quality.pdf
Usually, the techniques consist of three components below.
- DNS query racers, that run A and AAAA queries in parallel or series
- A short list of destination addresses
- TCP SYN racers, that run IPv4 and IPv6 transport in parallel or series
This CL implements only the latter two. The existing DNS query
component gathers together A and AAAA records in series, so we don't
touch it here. This CL just uses extended resolveInternetAddr and makes
it possible to run multiple Dial racers in parallel.
For example, when the given destination is a DNS name and the name has
multiple address family A and AAAA records, and it happens on the TCP
wildcard network "tcp" with DualStack=true like the following:
(&net.Dialer{DualStack: true}).Dial("tcp", "www.example.com:80")
The function will return a first established connection either TCP over
IPv4 or TCP over IPv6, and close the other connection internally.
Fixes #3610.
Fixes #5267.
Benchmark results on freebsd/amd64 virtual machine, tip vs. tip+12416043:
benchmark old ns/op new ns/op delta
BenchmarkTCP4OneShot 50696 52141 +2.85%
BenchmarkTCP4OneShotTimeout 65775 66426 +0.99%
BenchmarkTCP4Persistent 10986 10457 -4.82%
BenchmarkTCP4PersistentTimeout 11207 10445 -6.80%
BenchmarkTCP6OneShot 62009 63718 +2.76%
BenchmarkTCP6OneShotTimeout 78351 79138 +1.00%
BenchmarkTCP6Persistent 14695 14659 -0.24%
BenchmarkTCP6PersistentTimeout 15032 14646 -2.57%
BenchmarkTCP4ConcurrentReadWrite 7215 6217 -13.83%
BenchmarkTCP6ConcurrentReadWrite 7528 7493 -0.46%
benchmark old allocs new allocs delta
BenchmarkTCP4OneShot 36 36 0.00%
BenchmarkTCP4OneShotTimeout 36 36 0.00%
BenchmarkTCP4Persistent 0 0 n/a%
BenchmarkTCP4PersistentTimeout 0 0 n/a%
BenchmarkTCP6OneShot 37 37 0.00%
BenchmarkTCP6OneShotTimeout 37 37 0.00%
BenchmarkTCP6Persistent 0 0 n/a%
BenchmarkTCP6PersistentTimeout 0 0 n/a%
BenchmarkTCP4ConcurrentReadWrite 0 0 n/a%
BenchmarkTCP6ConcurrentReadWrite 0 0 n/a%
benchmark old bytes new bytes delta
BenchmarkTCP4OneShot 2500 2503 0.12%
BenchmarkTCP4OneShotTimeout 2508 2505 -0.12%
BenchmarkTCP4Persistent 0 0 n/a%
BenchmarkTCP4PersistentTimeout 0 0 n/a%
BenchmarkTCP6OneShot 2713 2707 -0.22%
BenchmarkTCP6OneShotTimeout 2722 2720 -0.07%
BenchmarkTCP6Persistent 0 0 n/a%
BenchmarkTCP6PersistentTimeout 0 0 n/a%
BenchmarkTCP4ConcurrentReadWrite 0 0 n/a%
BenchmarkTCP6ConcurrentReadWrite 0 0 n/a%
R=golang-dev, bradfitz, nightlyone, rsc
CC=golang-dev
https://golang.org/cl/12416043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/89b26760d7dfe4ae8ee65e4b2c21fec8a15f449b
元コミット内容
このコミットは、Go言語のnet
パッケージにTCP接続の高速フェイルオーバー機能、具体的にはHappy Eyeballsアルゴリズムの要素を導入します。これにより、net.Dialer
がIPv4とIPv6の両方のアドレスを持つホスト名に対してDualStack: true
を設定してtcp
ネットワークで接続を試みる際、最初に確立された接続(IPv4またはIPv6)を返し、他の接続試行は内部的に閉じられるようになります。
Happy Eyeballsは通常、以下の3つの要素で構成されます。
- DNSクエリの並行実行(AレコードとAAAAレコード)
- 宛先アドレスの短いリスト
- TCP SYNの並行実行(IPv4とIPv6)
このコミットでは、後者の2つの要素(宛先アドレスリストとTCP SYNの並行実行)を実装しています。既存のDNSクエリコンポーネントはAレコードとAAAAレコードを直列に収集するため、この部分は変更されていません。この変更は、resolveInternetAddr
の拡張を利用し、複数のDial
試行を並行して実行できるようにします。
ベンチマーク結果は、一部のシナリオでパフォーマンスのわずかな変動(増加または減少)を示していますが、全体的なアロケーションやバイト数には大きな変化はありません。
変更の背景
この変更の背景には、デュアルスタック(IPv4とIPv6の両方をサポートする)環境におけるネットワーク接続のユーザーエクスペリエンスの向上が挙げられます。従来の接続確立プロセスでは、DNSルックアップがIPv6アドレスを先に返し、そのIPv6接続が何らかの理由(ネットワークの問題、ファイアウォール、設定ミスなど)で失敗した場合、タイムアウトするまで待機し、その後IPv4アドレスでの接続を試みるという遅延が発生する可能性がありました。これはユーザーにとって不快な遅延となり、アプリケーションの応答性を低下させます。
Happy Eyeballsアルゴリズムは、このような問題を解決するために考案されました。複数のアドレスファミリー(IPv4とIPv6)に対して同時に接続を試みることで、最初に成功した接続を利用し、失敗した接続試行を迅速に中止することで、ユーザーが感じる遅延を最小限に抑えることを目的としています。
このコミットは、Goのnet
パッケージがこの重要なアルゴリズムをサポートし、デュアルスタック環境での接続の信頼性と速度を向上させることを目指しています。コミットメッセージに記載されているFixes #3610
とFixes #5267
は、この機能が解決しようとしている具体的な問題や要望を示唆しています。
前提知識の解説
1. IPv4とIPv6
- IPv4 (Internet Protocol version 4): 現在広く使われているインターネットプロトコルのバージョン。32ビットのアドレス空間を持ち、約43億個のユニークなアドレスを割り当てることができます。アドレス枯渇の問題が顕在化しています。例:
192.168.1.1
- IPv6 (Internet Protocol version 6): IPv4の後継として開発されたインターネットプロトコル。128ビットのアドレス空間を持ち、ほぼ無限のアドレスを提供します。IPv4アドレス枯渇問題の解決策として期待されており、より効率的なルーティングやセキュリティ機能も強化されています。例:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
2. デュアルスタック (Dual-Stack)
デュアルスタックとは、ネットワークデバイスやホストがIPv4とIPv6の両方のプロトコルスタックを同時にサポートし、両方のプロトコルで通信できる状態を指します。これにより、IPv4のみのネットワークとIPv6のみのネットワークの両方に接続し、通信することが可能になります。
3. Happy Eyeballs (RFC 6555)
Happy Eyeballsは、デュアルスタック環境において、クライアントがIPv4とIPv6の両方のアドレスを持つサーバーに接続する際に発生する可能性のある遅延を軽減するためのアルゴリズムです。主な目的は、どちらかのプロトコルでの接続が遅延したり失敗したりした場合でも、ユーザーが体感する接続確立までの時間を最小限に抑えることです。
このアルゴリズムの基本的な考え方は以下の通りです。
- 並行接続試行: クライアントは、サーバーのIPv4アドレスとIPv6アドレスの両方に対して、ほぼ同時に接続を試みます。
- 最初の成功を優先: 最初に接続が確立されたプロトコル(IPv4またはIPv6)の接続を採用し、もう一方の接続試行は中止または破棄します。
- タイムアウトと再試行: もし一方のプロトコルでの接続試行が一定時間内に成功しない場合、もう一方のプロトコルでの接続試行を優先したり、再試行したりするメカニズムも含まれることがあります。
これにより、例えばIPv6ネットワークの経路が不安定な場合でも、IPv4での接続が迅速に確立され、ユーザーはスムーズにサービスを利用できます。
4. net.Dialer
Go言語のnet
パッケージにおけるDialer
は、ネットワーク接続を確立するための設定をカプセル化する構造体です。Dialer
を使用することで、タイムアウト、ローカルアドレスの指定、そしてこのコミットで追加されたDualStack
オプションなど、接続の挙動を細かく制御できます。
5. OpError
Go言語のnet
パッケージで発生するネットワーク操作のエラーを表す構造体です。OpError
は、エラーが発生した操作(Op
)、ネットワークタイプ(Net
)、アドレス(Addr
)、および元のエラー(Err
)などの詳細情報を含みます。これにより、エラーの原因をより具体的に特定し、適切なエラーハンドリングを行うことができます。
技術的詳細
このコミットの技術的な核心は、net.Dialer
にDualStack
フィールドを追加し、Dial
メソッドの内部ロジックを拡張してHappy Eyeballsのような並行接続試行を可能にした点にあります。
-
Dialer
構造体の拡張:net.Dialer
構造体にDualStack bool
フィールドが追加されました。このフィールドがtrue
に設定され、かつネットワークが"tcp"
であり、宛先が複数のアドレスファミリー(IPv4とIPv6)のDNSレコードを持つホスト名である場合に、Happy Eyeballsの挙動が有効になります。 -
Dial
メソッドの変更:Dial
メソッドは、まずresolveAddr
を呼び出して宛先アドレスを解決します。解決されたアドレスがaddrList
(複数のアドレスを含むリスト)であり、DualStack
がtrue
、かつネットワークが"tcp"
の場合、新しいdialMulti
関数が接続確立のために使用されます。それ以外の場合は、既存のdialSingle
関数(単一の接続試行)が使用されます。 -
dialMulti
関数の導入: これがHappy Eyeballsの中核をなす関数です。dialMulti
は、解決された複数の宛先アドレス(addrList
)を受け取ります。- 各アドレスに対して、個別のゴルーチン内で
dialSingle
を呼び出し、並行して接続を試みます。 racer
という内部構造体と、sig
(シグナル)およびlane
(結果チャネル)というチャネルを使用して、並行実行される接続試行を管理します。sig
チャネルは、各ゴルーチンが接続試行を開始する許可を与えるためのトークンとして機能します。これにより、同時に開始される接続試行の数を制御できます。lane
チャネルは、各ゴルーチンからの接続結果(成功した接続、またはエラー)を受け取ります。dialMulti
は、lane
チャネルから最初に成功した接続を受け取ると、その接続を返し、他の進行中の接続試行は内部的に閉じられます(リソースリークを防ぐため)。- すべての接続試行が失敗した場合、最後に発生したエラーが返されます。
-
dialSingle
関数の導入: 既存の単一接続試行ロジックがdialSingle
として分離されました。これは、dialMulti
から呼び出されることで、個々の接続試行を担当します。 -
プラットフォーム固有の
dial
関数の抽象化:src/pkg/net/fd_plan9.go
、src/pkg/net/fd_unix.go
、src/pkg/net/fd_windows.go
内のdial
関数(以前はresolveAndDial
)のシグネチャが変更され、dialer func(time.Time) (Conn, error)
という関数を受け取るようになりました。これにより、プラットフォーム固有の低レベルな接続確立メカニズム(例: WindowsのConnectEx
、またはPlan 9や古いWindowsでのゴルーチンベースのdialChannel
)が、新しいdialMulti
やdialSingle
のロジックと統合されやすくなりました。 -
テストの追加:
TestDialMultiFDLeak
:dialMulti
がファイルディスクリプタを適切にクリーンアップすることを確認するためのテスト。lsof
コマンドを使用してオープンされているTCPセッション数を検証します。TestDialDualStackLocalhost
:localhost
に対してデュアルスタック接続が正しく機能するかを検証します。TestDialGoogle
: 実際の外部ホスト(www.google.com
)に対してデュアルスタック接続が機能するかを検証します。mockserver_test.go
という新しいテストヘルパーファイルが追加され、デュアルスタック環境でのテストを容易にするためのモックサーバー機能が提供されます。
これらの変更により、Goのnet
パッケージは、デュアルスタック環境におけるネットワーク接続の堅牢性とパフォーマンスを大幅に向上させています。
コアとなるコードの変更箇所
このコミットの主要な変更は、src/pkg/net/dial.go
ファイルに集中しています。
-
Dialer
構造体へのDualStack
フィールドの追加:type Dialer struct { // ... 既存のフィールド ... DualStack bool }
-
Dialer.Dial
メソッドの変更:Dial
メソッドの内部で、DualStack
フラグとネットワークタイプ、アドレス解決の結果に基づいて、dialMulti
またはdialSingle
が呼び出されるようにロジックが分岐します。func (d *Dialer) Dial(network, address string) (Conn, error) { ra, err := resolveAddr("dial", network, address, d.deadline()) if err != nil { return nil, &OpError{Op: "dial", Net: network, Addr: nil, Err: err} } dialer := func(deadline time.Time) (Conn, error) { return dialSingle(network, address, d.LocalAddr, ra.toAddr(), deadline) } if ras, ok := ra.(addrList); ok && d.DualStack && network == "tcp" { dialer = func(deadline time.Time) (Conn, error) { return dialMulti(network, address, d.LocalAddr, ras, deadline) } } return dial(network, ra.toAddr(), dialer, d.deadline()) }
-
dialMulti
関数の新規追加: Happy Eyeballsの並行接続ロジックを実装しています。func dialMulti(net, addr string, la Addr, ras addrList, deadline time.Time) (Conn, error) { type racer struct { Conn Addr error } sig := make(chan bool, 1) // 接続試行のフロー制御 lane := make(chan racer, 1) // 接続結果のチャネル for _, ra := range ras { go func(ra Addr) { c, err := dialSingle(net, addr, la, ra, deadline) if _, ok := <-sig; ok { // トークンを受け取ったら結果を送信 lane <- racer{c, ra, err} } else if err == nil { // トークンを受け取れず、かつ接続成功した場合は閉じる c.Close() } }(ra.toAddr()) } defer close(sig) // 全てのゴルーチンが終了したらシグナルチャネルを閉じる var failAddr Addr lastErr := errTimeout nracers := len(ras) for nracers > 0 { sig <- true // 接続試行のトークンを送信 select { case racer := <-lane: // 接続結果を受信 if racer.error == nil { return racer.Conn, nil // 成功した接続を返す } failAddr = racer.Addr lastErr = racer.error nracers-- } } return nil, &OpError{Op: "dial", Net: net, Addr: failAddr, Err: lastErr} // 全て失敗した場合 }
-
dialSingle
関数の新規追加: 単一の接続試行ロジックをカプセル化します。func dialSingle(net, addr string, la, ra Addr, deadline time.Time) (Conn, error) { // ... 既存の単一接続ロジック ... }
これらの変更により、Goのネットワーク接続確立プロセスが、デュアルスタック環境においてよりインテリジェントかつ効率的になりました。
コアとなるコードの解説
Dialer
構造体とDualStack
フィールド
Dialer
構造体は、Goアプリケーションがネットワーク接続を確立する際の設定を保持します。新しく追加されたDualStack bool
フィールドは、このDialer
がHappy Eyeballsのようなデュアルスタック接続試行を行うべきかどうかを制御するフラグです。ユーザーがこのフィールドをtrue
に設定することで、GoランタイムはIPv4とIPv6の両方のアドレスを持つホストへの接続時に、より高速な接続確立を試みます。
Dialer.Dial
メソッドのロジック分岐
Dialer.Dial
メソッドは、Goアプリケーションがネットワーク接続を開始する主要なエントリポイントです。このコミットでは、このメソッドの内部で、接続先の情報とDualStack
フラグに基づいて、接続確立の戦略を動的に選択するようになりました。
- アドレス解決: まず、
resolveAddr
関数が呼び出され、指定されたホスト名(例:www.example.com
)に対応するIPアドレスが解決されます。この解決プロセスは、IPv4 (Aレコード) とIPv6 (AAAAレコード) の両方のアドレスを取得する可能性があります。 dialer
関数の選択:- 解決されたアドレスが単一の場合、または
DualStack
がfalse
の場合、あるいはネットワークが"tcp"
以外の場合(例:"udp"
など)、従来の単一接続試行を行うdialSingle
関数がdialer
変数に設定されます。 - 一方、解決されたアドレスが複数のアドレス(
addrList
)を含み、DualStack
がtrue
、かつネットワークが"tcp"
である場合、Happy Eyeballsロジックを実装したdialMulti
関数がdialer
変数に設定されます。
- 解決されたアドレスが単一の場合、または
- 最終的な
dial
呼び出し: 最終的に、選択されたdialer
関数(dialSingle
またはdialMulti
)が、共通のdial
ヘルパー関数を通じて実行されます。この抽象化により、プラットフォーム固有の低レベルな接続処理(fd_unix.go
,fd_windows.go
,fd_plan9.go
など)が、高レベルのHappy Eyeballsロジックとシームレスに連携できるようになっています。
dialMulti
関数による並行接続試行
dialMulti
関数は、Happy Eyeballsアルゴリズムの「TCP SYN racers」部分を実装しています。
- ゴルーチンによる並行処理:
dialMulti
は、解決された各IPアドレス(IPv4とIPv6)に対して、それぞれ新しいゴルーチンを起動し、そのゴルーチン内でdialSingle
を呼び出して接続を試みます。これにより、複数の接続試行が同時に進行します。 sig
チャネルによるフロー制御:sig
チャネルはバッファ付きチャネル(容量1)として定義されており、各ゴルーチンが接続試行の結果をlane
チャネルに送信する前に、このsig
チャネルからトークンを受け取る必要があります。これにより、同時にアクティブな接続試行の数を制御し、リソースの過剰な消費を防ぎます。lane
チャネルによる結果収集: 各ゴルーチンは、接続試行の結果(成功した接続またはエラー)をlane
チャネルに送信します。- 最初の成功の採用:
dialMulti
はlane
チャネルを監視し、最初に成功した接続が到着すると、直ちにその接続を呼び出し元に返します。 - リソースのクリーンアップ: 成功した接続が返された後、まだ進行中の他の接続試行は、
sig
チャネルが閉じられることで、そのゴルーチン内でc.Close()
が呼び出され、適切にクリーンアップされます。これにより、不要な接続が残り続けることによるファイルディスクリプタのリークやリソースの浪費を防ぎます。 - エラーハンドリング: 全ての接続試行が失敗した場合、
dialMulti
は最後に発生したエラーをOpError
として返します。
このメカニズムにより、Goアプリケーションは、デュアルスタック環境において、ネットワークの状況に左右されずに、最も早く確立できた接続を自動的に選択し、ユーザーエクスペリエンスを向上させることができます。
関連リンク
- Go Issue #3610: https://github.com/golang/go/issues/3610
- Go Issue #5267: https://github.com/golang/go/issues/5267
- Go Change-Id: 12416043 (Gerrit): https://golang.org/cl/12416043
参考にした情報源リンク
- Happy Eyeballs: Success with Dual-Stack Hosts (RFC 6555): http://tools.ietf.org/html/rfc6555
- Analysing Dual Stack Behaviour and IPv6 Quality: http://www.potaroo.net/presentations/2012-04-17-dual-stack-quality.pdf
- Go
net
package documentation: https://pkg.go.dev/net (現在のドキュメントはコミット当時のものとは異なる可能性があります)