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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける Transport の挙動を改善し、ソケットの「遅延バインディング (late binding)」を導入するものです。これにより、HTTPクライアントがTCP接続を確立する際の効率が向上し、特に多数の同時接続を扱うシナリオでのパフォーマンスが改善されます。

コミット

commit b6e0d39a343128759988c12e7560d21cd35472ca
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 7 17:56:00 2013 -0800

    net/http: Transport socket late binding
    
    Implement what Chrome calls socket "late binding". See:
    https://insouciant.org/tech/connection-management-in-chromium/
    
    In a nutshell, if our HTTP client needs a TCP connection to a
    remote host and there's not an idle one available, rather than
    kick off a dial and wait for that specific dial, we instead
    kick off a dial and wait for either our own dial to finish, or
    any other TCP connection to that same host to become
    available.
    
    The implementation looks like a classic "Learning Go
    Concurrency" slide.
    
    Chrome's commit and numbers:
    http://src.chromium.org/viewvc/chrome?view=rev&revision=36230
    
    R=golang-dev, daniel.morsing, adg
    CC=golang-dev
    https://golang.org/cl/7587043

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

https://github.com/golang/go/commit/b6e0d39a343128759988c12e7560d21cd35472ca

元コミット内容

このコミットは、Goの net/http パッケージの Transport において、Chromeブラウザが採用している「ソケットの遅延バインディング (socket late binding)」という接続管理戦略を実装するものです。

具体的には、HTTPクライアントがリモートホストへのTCP接続を必要とし、かつアイドル状態の既存接続がない場合、通常は新しい接続の確立(ダイヤル)を開始し、その特定のダイヤルが完了するのを待ちます。しかし、遅延バインディングでは、新しいダイヤルを開始しつつも、そのダイヤル自身が完了するのを待つだけでなく、同じホストへの他のTCP接続が利用可能になるのを待つように挙動を変更します。これにより、先に利用可能になった接続をすぐに利用できるようになり、接続待ちの時間を短縮し、リソースの利用効率を高めます。

実装は、Goの並行処理の典型的なパターン("Learning Go Concurrency" のスライドに出てくるようなパターン)に似ていると述べられています。

参照として、Chromeの関連コミットと、その変更によるパフォーマンス改善の数値が示されています。

変更の背景

この変更の背景には、HTTPクライアントが多数の同時リクエストを処理する際のパフォーマンス最適化があります。従来の接続管理では、新しいリクエストが来た際にアイドルな接続がなければ、そのリクエスト専用の新しいTCP接続を確立しようとします。この際、ネットワークの遅延やサーバーの応答時間によっては、接続確立に時間がかかり、そのリクエストがブロックされる可能性があります。

特に、Webブラウザのようなアプリケーションでは、多数のHTTPリクエストが同時に発生し、それらが同じドメインへの接続を必要とすることが頻繁にあります。このような状況で、各リクエストが個別に接続確立を待つと、全体の応答性が低下します。

「ソケットの遅延バインディング」は、この問題を解決するための戦略です。複数のリクエストが同時に接続を必要とする場合、それらすべてが個別の接続確立を待つのではなく、最初に利用可能になった接続を共有することで、リクエストの処理を早めることができます。これにより、接続プールの利用効率が最大化され、全体的なスループタイムが向上します。

このアプローチは、Google Chromeブラウザの接続管理戦略から着想を得ており、その効果が実証されています。

前提知識の解説

1. HTTP Transport (Go言語の net/http パッケージ)

Go言語の net/http パッケージは、HTTPクライアントとサーバーを構築するための強力な機能を提供します。http.Client はHTTPリクエストを送信するための主要な構造体であり、その内部で http.RoundTripper インターフェースを実装した http.Transport を利用して実際のネットワーク通信を行います。

http.Transport は、TCP接続の確立、TLSハンドシェイク、HTTP/1.1のKeep-Alive、HTTP/2の多重化、プロキシの利用など、低レベルのネットワーク通信の詳細を管理します。特に重要なのは、接続の再利用(Keep-Alive)接続プールの管理です。Transport は、一度確立されたTCP接続をアイドル状態のまま保持し、同じホストへの後続のリクエストで再利用することで、接続確立のオーバーヘッドを削減し、パフォーマンスを向上させます。

2. TCP接続の確立 (Three-way Handshake)

TCP接続の確立は、クライアントとサーバー間で3回のメッセージ交換(スリーウェイハンドシェイク)を必要とします。

  1. SYN (Synchronize): クライアントがサーバーに接続要求を送信。
  2. SYN-ACK (Synchronize-Acknowledge): サーバーが接続要求を受け入れ、自身のシーケンス番号を同期し、クライアントのSYNを承認。
  3. ACK (Acknowledge): クライアントがサーバーのSYN-ACKを承認し、接続が確立。

このプロセスにはネットワークのラウンドトリップタイム (RTT) がかかるため、接続確立は無視できない遅延要因となります。

3. ソケットの遅延バインディング (Socket Late Binding)

ソケットの遅延バインディングは、ネットワーク接続を効率的に管理するための戦略の一つです。これは、特定のHTTPリクエストが特定のTCP接続に「バインド」されるタイミングを遅らせるという考え方に基づいています。

従来のモデルでは、HTTPリクエストが発行されると、そのリクエストのために新しいTCP接続が確立されるか、既存のアイドル接続が割り当てられます。もしアイドル接続がなければ、新しい接続の確立が開始され、その接続が完了するまでリクエストは待機します。

遅延バインディングでは、複数のリクエストが同時に接続を必要とする場合、それぞれが個別の接続確立を開始するのではなく、同じ宛先への複数の接続確立プロセスが並行して進行している状況で、最初に完了した接続を、待機している任意のリクエストに割り当てることができます。つまり、リクエストは「自分のダイヤル」が完了するのを待つだけでなく、「同じ宛先への他のダイヤル」が完了するのを待つこともできます。これにより、リクエストが接続を待つ平均時間が短縮され、スループットが向上します。

4. Go言語の並行処理 (Goroutines and Channels)

Go言語は、軽量なスレッドである「Goroutine」と、それらのGoroutine間で安全にデータをやり取りするための「Channel」というプリミティブを提供することで、並行処理を強力にサポートします。

  • Goroutine: go キーワードを使って関数呼び出しの前に置くことで、その関数を新しいGoroutineとして実行できます。非常に軽量で、数百万のGoroutineを同時に実行することも可能です。
  • Channel: Goroutine間の通信と同期のためのパイプのようなものです。チャネルに値を送信したり、チャネルから値を受信したりすることで、Goroutineは互いに協調して動作できます。バッファリングされたチャネルとバッファリングされていないチャネルがあります。

このコミットの変更は、まさにこれらのGoの並行処理の機能を活用して、ソケットの遅延バインディングを実現しています。特に、複数のGoroutineが接続を確立しようとし、チャネルを通じてその結果を共有するパターンが用いられています。

技術的詳細

このコミットの主要な変更点は、net/http/transport.go 内の Transport 構造体に idleConnCh という新しいフィールドが追加されたことです。

type Transport struct {
	idleMu     sync.Mutex
	idleConn   map[string][]*persistConn
	idleConnCh map[string]chan *persistConn // 新しく追加されたフィールド
	reqMu      sync.Mutex
	reqConn    map[*Request]*persistConn
	altMu      sync.RWMutex
	altProto   map[string]RoundTripper // nil or map of URI scheme => RoundTripper
	// ...
}

idleConnChmap[string]chan *persistConn 型であり、persistConn (永続的なTCP接続を表す内部構造体) のチャネルをホストのキー(connectMethod のキー)ごとに保持します。このチャネルは、特定のホストへの接続を待っているGoroutineに対して、利用可能な persistConn を通知するために使用されます。

putIdleConn メソッドの変更

putIdleConn メソッドは、使用済みでアイドル状態になった接続を接続プールに戻す役割を担います。このメソッドに以下のロジックが追加されました。

	select {
	case t.idleConnCh[key] <- pconn:
		// We're done with this pconn and somebody else is
		// currently waiting for a conn of this type (they're
		// actively dialing, but this conn is ready
		// first). Chrome calls this socket late binding.  See
		// https://insouciant.org/tech/connection-management-in-chromium/
		t.idleMu.Unlock()
		return true
	default:
	}

この select ステートメントは、pconnidleConnCh に送信しようとします。もし、その key に対応するチャネルに受信者が(つまり、接続を待っているGoroutineが)いる場合、pconn は直接そのチャネルに送信され、即座に利用されます。これは、新しい接続が確立されるのを待っている間に、別のリクエストが完了して接続がアイドル状態になった場合に、そのアイドル接続を優先的に利用する「遅延バインディング」の核心部分です。default ケースがあるため、チャネルに受信者がいない場合はブロックされず、従来のアイドル接続プール (t.idleConn) に追加されます。

getConn メソッドの変更

getConn メソッドは、HTTPリクエストのために利用可能な接続を取得する主要なロジックを含んでいます。このメソッドが大幅にリファクタリングされ、遅延バインディングのロジックが組み込まれました。

変更前は、getConn の中で直接 t.dialConn(cm) を呼び出して接続を確立していました。変更後は、dialConn の呼び出しが getConn から分離され、新しいGoroutineで実行されるようになりました。

	type dialRes struct {
		pc  *persistConn
		err error
	}
	dialc := make(chan dialRes)
	go func() {
		pc, err := t.dialConn(cm)
		dialc <- dialRes{pc, err}
	}()

	idleConnCh := t.getIdleConnCh(cm)
	select {
	case v := <-dialc:
		// Our dial finished.
		return v.pc, v.err
	case pc := <-idleConnCh:
		// Another request finished first and its net.Conn
		// became available before our dial. Or somebody
		// else's dial that they didn't use.
		// But our dial is still going, so give it away
		// when it finishes:
		go func() {
			if v := <-dialc; v.err == nil {
				t.putIdleConn(v.pc)
			}
		}()
		return pc, nil
	}

この新しいロジックは以下のステップで動作します。

  1. ダイヤルGoroutineの起動: t.dialConn(cm) を呼び出して新しい接続を確立する処理を、独立したGoroutineで実行します。このGoroutineは、接続結果を dialc チャネルに送信します。
  2. アイドル接続チャネルの取得: t.getIdleConnCh(cm) を呼び出して、現在の接続先に対応する idleConnCh を取得します。このチャネルは、他のリクエストが完了してアイドルになった接続を受け取る可能性があります。
  3. select ステートメントによる待機: getConndialcidleConnCh の両方から値を受信できるまでブロックします。
    • dialc から受信した場合: 自身が開始したダイヤルが先に完了した場合です。その接続 (v.pc) をそのまま返します。
    • idleConnCh から受信した場合: 自身がダイヤルしている間に、他のリクエストが完了してアイドルになった接続が利用可能になった場合です。このアイドル接続 (pc) をすぐに返します。この際、自身が開始したダイヤルはまだ進行中であるため、そのダイヤルが完了した際には、その接続をアイドル接続プールに返すための新しいGoroutineを起動します。これにより、無駄な接続が破棄されることなく、将来のリクエストで再利用されます。

dialConn メソッドの分離

以前 getConn の一部だった接続確立ロジックは、dialConn という新しいプライベートメソッドとして分離されました。これにより、コードのモジュール性が向上し、getConn のロジックが遅延バインディングの制御に集中できるようになりました。

connectMethod.key() の導入

connectMethod 構造体には key() メソッドが追加され、String() メソッドの代わりに接続のキャッシュキーとして使用されるようになりました。これは、将来的に String() 以外の方法でキーを生成する可能性を考慮したものです。

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

src/pkg/net/http/transport.go

  • Transport 構造体に idleConnCh map[string]chan *persistConn フィールドを追加。
  • putIdleConn メソッドに、idleConnCh への遅延バインディングロジックを追加。
  • getIdleConnCh メソッドを新しく追加。これは idleConnCh マップからチャネルを取得または作成する。
  • getConn メソッドを大幅にリファクタリングし、新しいGoroutineでダイヤルを開始し、select ステートメントで自身のダイヤル完了またはアイドル接続の利用を待つロジックを実装。
  • dialConn メソッドを新しく追加。これは以前 getConn の一部だった接続確立ロジックを分離したもの。
  • connectMethod 構造体に key() メソッドを追加し、String() の代わりにキャッシュキーとして使用。

src/pkg/net/http/transport_test.go

  • TestTransportSocketLateBinding という新しいテストケースを追加。このテストは、ソケット遅延バインディングの挙動を検証するために書かれています。具体的には、/foo へのリクエストが接続をブロックし、その間に /bar へのリクエストが同じ接続を再利用できることを確認しています。

コアとなるコードの解説

Transport 構造体の idleConnCh

idleConnCh は、特定の宛先(ホストとポート)への接続を待っているGoroutineに対して、利用可能な persistConn を通知するためのチャネルのマップです。キーは connectMethod.key() で生成される文字列で、これは接続先のホストとポート、プロキシ情報などを一意に識別します。

putIdleConnselect ロジック

	select {
	case t.idleConnCh[key] <- pconn:
		// ...
		t.idleMu.Unlock()
		return true
	default:
		// ...
	}

この部分は、pconn がアイドル状態になったときに、もしその接続を待っているGoroutineが idleConnCh をリッスンしていれば、すぐにそのGoroutineに pconn を渡すことを試みます。これにより、接続がアイドルプールに入るのを待つことなく、即座に再利用される可能性が生まれます。これは、接続の「遅延バインディング」の重要な側面です。

getConn の並行処理と select

	dialc := make(chan dialRes)
	go func() {
		pc, err := t.dialConn(cm)
		dialc <- dialRes{pc, err}
	}()

	idleConnCh := t.getIdleConnCh(cm)
	select {
	case v := <-dialc:
		// ...
	case pc := <-idleConnCh:
		// ...
		go func() {
			if v := <-dialc; v.err == nil {
				t.putIdleConn(v.pc)
			}
		}()
		// ...
	}

このコードブロックは、Goの並行処理の強力な例です。

  1. dialc チャネルと、新しいGoroutineで実行される t.dialConn が、新しい接続の確立を非同期で行います。
  2. idleConnCh は、他のリクエストによって既に確立され、アイドル状態になった接続を受け取るためのチャネルです。
  3. select ステートメントは、dialcidleConnCh のどちらかから先に値が届くのを待ちます。
    • もし dialc から先に値が届けば、自身が開始した接続が先に完了したことを意味します。
    • もし idleConnCh から先に値が届けば、他のリクエストが完了してアイドルになった接続が利用可能になったことを意味します。この場合、自身のダイヤルはまだ進行中であるため、そのダイヤルが完了した際に、その接続をアイドルプールに戻すための別のGoroutineを起動します。これにより、リソースの無駄がなくなります。

このパターンは、複数の非同期操作の結果を効率的に待機し、最初に利用可能になったリソースを優先的に使用するという、Goの並行処理における一般的なイディオムです。コミットメッセージで言及されている「Learning Go Concurrency」のスライドは、このようなチャネルとGoroutineを使った並行処理のパターンを指していると考えられます。

関連リンク

参考にした情報源リンク