[インデックス 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回のメッセージ交換(スリーウェイハンドシェイク)を必要とします。
- SYN (Synchronize): クライアントがサーバーに接続要求を送信。
- SYN-ACK (Synchronize-Acknowledge): サーバーが接続要求を受け入れ、自身のシーケンス番号を同期し、クライアントのSYNを承認。
- 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
// ...
}
idleConnCh
は map[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
ステートメントは、pconn
を idleConnCh
に送信しようとします。もし、その 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
}
この新しいロジックは以下のステップで動作します。
- ダイヤルGoroutineの起動:
t.dialConn(cm)
を呼び出して新しい接続を確立する処理を、独立したGoroutineで実行します。このGoroutineは、接続結果をdialc
チャネルに送信します。 - アイドル接続チャネルの取得:
t.getIdleConnCh(cm)
を呼び出して、現在の接続先に対応するidleConnCh
を取得します。このチャネルは、他のリクエストが完了してアイドルになった接続を受け取る可能性があります。 select
ステートメントによる待機:getConn
はdialc
とidleConnCh
の両方から値を受信できるまでブロックします。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()
で生成される文字列で、これは接続先のホストとポート、プロキシ情報などを一意に識別します。
putIdleConn
の select
ロジック
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の並行処理の強力な例です。
dialc
チャネルと、新しいGoroutineで実行されるt.dialConn
が、新しい接続の確立を非同期で行います。idleConnCh
は、他のリクエストによって既に確立され、アイドル状態になった接続を受け取るためのチャネルです。select
ステートメントは、dialc
とidleConnCh
のどちらかから先に値が届くのを待ちます。- もし
dialc
から先に値が届けば、自身が開始した接続が先に完了したことを意味します。 - もし
idleConnCh
から先に値が届けば、他のリクエストが完了してアイドルになった接続が利用可能になったことを意味します。この場合、自身のダイヤルはまだ進行中であるため、そのダイヤルが完了した際に、その接続をアイドルプールに戻すための別のGoroutineを起動します。これにより、リソースの無駄がなくなります。
- もし
このパターンは、複数の非同期操作の結果を効率的に待機し、最初に利用可能になったリソースを優先的に使用するという、Goの並行処理における一般的なイディオムです。コミットメッセージで言及されている「Learning Go Concurrency」のスライドは、このようなチャネルとGoroutineを使った並行処理のパターンを指していると考えられます。
関連リンク
- Connection Management in Chromium: https://insouciant.org/tech/connection-management-in-chromium/ - Chromeブラウザの接続管理戦略について詳細に解説されており、ソケット遅延バインディングの概念が説明されています。
- Chromeの関連コミット: http://src.chromium.org/viewvc/chrome?view=rev&revision=36230 - Chromeにおけるソケット遅延バインディングの実装に関するコミット。
- Go CL 7587043: https://golang.org/cl/7587043 - このコミットに対応するGoのコードレビューシステム (Gerrit) のチェンジリスト。
参考にした情報源リンク
- https://insouciant.org/tech/connection-management-in-chromium/
- http://src.chromium.org/viewvc/chrome?view=rev&revision=36230
- Go言語の公式ドキュメント (
net/http
パッケージ) - Go言語の並行処理に関する一般的な知識 (Goroutine, Channel, select)