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

[インデックス 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 #3610Fixes #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.DialerDualStackフィールドを追加し、Dialメソッドの内部ロジックを拡張してHappy Eyeballsのような並行接続試行を可能にした点にあります。

  1. Dialer構造体の拡張: net.Dialer構造体にDualStack boolフィールドが追加されました。このフィールドがtrueに設定され、かつネットワークが"tcp"であり、宛先が複数のアドレスファミリー(IPv4とIPv6)のDNSレコードを持つホスト名である場合に、Happy Eyeballsの挙動が有効になります。

  2. Dialメソッドの変更: Dialメソッドは、まずresolveAddrを呼び出して宛先アドレスを解決します。解決されたアドレスがaddrList(複数のアドレスを含むリスト)であり、DualStacktrue、かつネットワークが"tcp"の場合、新しいdialMulti関数が接続確立のために使用されます。それ以外の場合は、既存のdialSingle関数(単一の接続試行)が使用されます。

  3. dialMulti関数の導入: これがHappy Eyeballsの中核をなす関数です。

    • dialMultiは、解決された複数の宛先アドレス(addrList)を受け取ります。
    • 各アドレスに対して、個別のゴルーチン内でdialSingleを呼び出し、並行して接続を試みます。
    • racerという内部構造体と、sig(シグナル)およびlane(結果チャネル)というチャネルを使用して、並行実行される接続試行を管理します。
    • sigチャネルは、各ゴルーチンが接続試行を開始する許可を与えるためのトークンとして機能します。これにより、同時に開始される接続試行の数を制御できます。
    • laneチャネルは、各ゴルーチンからの接続結果(成功した接続、またはエラー)を受け取ります。
    • dialMultiは、laneチャネルから最初に成功した接続を受け取ると、その接続を返し、他の進行中の接続試行は内部的に閉じられます(リソースリークを防ぐため)。
    • すべての接続試行が失敗した場合、最後に発生したエラーが返されます。
  4. dialSingle関数の導入: 既存の単一接続試行ロジックがdialSingleとして分離されました。これは、dialMultiから呼び出されることで、個々の接続試行を担当します。

  5. プラットフォーム固有のdial関数の抽象化: src/pkg/net/fd_plan9.gosrc/pkg/net/fd_unix.gosrc/pkg/net/fd_windows.go内のdial関数(以前はresolveAndDial)のシグネチャが変更され、dialer func(time.Time) (Conn, error)という関数を受け取るようになりました。これにより、プラットフォーム固有の低レベルな接続確立メカニズム(例: WindowsのConnectEx、またはPlan 9や古いWindowsでのゴルーチンベースのdialChannel)が、新しいdialMultidialSingleのロジックと統合されやすくなりました。

  6. テストの追加:

    • TestDialMultiFDLeak: dialMultiがファイルディスクリプタを適切にクリーンアップすることを確認するためのテスト。lsofコマンドを使用してオープンされているTCPセッション数を検証します。
    • TestDialDualStackLocalhost: localhostに対してデュアルスタック接続が正しく機能するかを検証します。
    • TestDialGoogle: 実際の外部ホスト(www.google.com)に対してデュアルスタック接続が機能するかを検証します。
    • mockserver_test.goという新しいテストヘルパーファイルが追加され、デュアルスタック環境でのテストを容易にするためのモックサーバー機能が提供されます。

これらの変更により、Goのnetパッケージは、デュアルスタック環境におけるネットワーク接続の堅牢性とパフォーマンスを大幅に向上させています。

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

このコミットの主要な変更は、src/pkg/net/dial.goファイルに集中しています。

  1. Dialer構造体へのDualStackフィールドの追加:

    type Dialer struct {
        // ... 既存のフィールド ...
        DualStack bool
    }
    
  2. 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())
    }
    
  3. 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} // 全て失敗した場合
    }
    
  4. 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フラグに基づいて、接続確立の戦略を動的に選択するようになりました。

  1. アドレス解決: まず、resolveAddr関数が呼び出され、指定されたホスト名(例: www.example.com)に対応するIPアドレスが解決されます。この解決プロセスは、IPv4 (Aレコード) とIPv6 (AAAAレコード) の両方のアドレスを取得する可能性があります。
  2. dialer関数の選択:
    • 解決されたアドレスが単一の場合、またはDualStackfalseの場合、あるいはネットワークが"tcp"以外の場合(例: "udp"など)、従来の単一接続試行を行うdialSingle関数がdialer変数に設定されます。
    • 一方、解決されたアドレスが複数のアドレス(addrList)を含み、DualStacktrue、かつネットワークが"tcp"である場合、Happy Eyeballsロジックを実装したdialMulti関数がdialer変数に設定されます。
  3. 最終的な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チャネルに送信します。
  • 最初の成功の採用: dialMultilaneチャネルを監視し、最初に成功した接続が到着すると、直ちにその接続を呼び出し元に返します。
  • リソースのクリーンアップ: 成功した接続が返された後、まだ進行中の他の接続試行は、sigチャネルが閉じられることで、そのゴルーチン内でc.Close()が呼び出され、適切にクリーンアップされます。これにより、不要な接続が残り続けることによるファイルディスクリプタのリークやリソースの浪費を防ぎます。
  • エラーハンドリング: 全ての接続試行が失敗した場合、dialMultiは最後に発生したエラーをOpErrorとして返します。

このメカニズムにより、Goアプリケーションは、デュアルスタック環境において、ネットワークの状況に左右されずに、最も早く確立できた接続を自動的に選択し、ユーザーエクスペリエンスを向上させることができます。

関連リンク

参考にした情報源リンク