[インデックス 13175] ファイルの概要
このコミットは、Go言語のnet/http
パッケージにおけるHTTPプロキシ接続の再利用に関する改善を目的としています。具体的には、異なるHTTPリクエスト間で同じHTTPプロキシ接続を再利用できるようにすることで、ネットワーク効率を向上させます。
コミット
commit cb62365f5737d8c6a803b0737b3f34a64e526b6b
Author: Alexey Borzenkov <snaury@gmail.com>
Date: Mon May 28 10:46:51 2012 -0700
net/http: reuse http proxy connections for different http requests
Comment on cache keys above connectMethod says "http to proxy, http
anywhere after that", however in reality target address was always
included, which prevented http requests to different target
addresses to reuse the same http proxy connection.
R=golang-dev, r, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5901064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cb62365f5737d8c6a803b0737b3f34a64e526b6b
元コミット内容
net/http: reuse http proxy connections for different http requests
このコミットの目的は、HTTPプロキシを介した接続において、異なるターゲットアドレスへのHTTPリクエストであっても、同じHTTPプロキシ接続を再利用できるようにすることです。既存の実装では、connectMethod
のキャッシュキーにターゲットアドレスが常に含まれていたため、異なるターゲットアドレスへのリクエストでは新しいプロキシ接続が確立されてしまい、接続の再利用が妨げられていました。
変更の背景
Goのnet/http
パッケージは、HTTPクライアントとサーバーの機能を提供します。クライアント側では、http.Transport
がネットワーク接続の確立と管理を担当し、アイドル状態の(キープアライブ)接続をプールして再利用することで、パフォーマンスを向上させます。これは、特にプロキシを介した接続においても同様に機能することが期待されます。
しかし、このコミット以前のnet/http
パッケージの動作では、HTTPプロキシを使用する際に、プロキシへの接続が効率的に再利用されないという問題がありました。具体的には、connectMethod
という内部構造体が接続のキャッシュキーを生成する際に、HTTPスキームのリクエストであってもターゲットアドレスを含めていました。これにより、たとえ同じプロキシサーバーを経由していても、異なる宛先ホストへのHTTPリクエストはそれぞれ新しいプロキシ接続を確立する必要がありました。これは、特に多数の異なるHTTPエンドポイントにアクセスするアプリケーションにおいて、不必要な接続確立のオーバーヘッドとリソース消費を引き起こしていました。
このコミットは、この非効率性を解消し、HTTPプロキシ接続の再利用を最適化することを目的としています。
前提知識の解説
HTTPプロキシ
HTTPプロキシは、クライアントとサーバーの間に位置し、クライアントからのリクエストをサーバーに転送し、サーバーからのレスポンスをクライアントに転送する仲介サーバーです。プロキシは、セキュリティ、キャッシング、ロードバランシング、アクセス制御など、様々な目的で使用されます。
HTTP/1.1の接続管理とKeep-Alive
HTTP/1.1では、Connection: keep-alive
ヘッダを使用することで、単一のTCP接続上で複数のHTTPリクエストとレスポンスをやり取りする「持続的接続(Persistent Connection)」がサポートされています。これにより、リクエストごとにTCP接続を確立・切断するオーバーヘッドが削減され、パフォーマンスが向上します。Goのnet/http.Transport
は、この持続的接続を管理し、アイドル状態の接続をプールして再利用する機能(コネクションプーリング)を提供します。
http.Transport
とコネクションプーリング
Goのnet/http
パッケージにおいて、http.Transport
はHTTPリクエストの実際の送信を担当する構造体です。これには、TCP接続の確立、TLSハンドシェイク、プロキシの処理、そして最も重要なコネクションプーリングの機能が含まれます。Transport
は、MaxIdleConns
、IdleConnTimeout
、MaxIdleConnsPerHost
などの設定を通じて、接続の再利用を細かく制御できます。効率的な接続再利用のためには、単一のhttp.Client
とその基盤となるhttp.Transport
インスタンスを生成し、複数のリクエストで再利用することが重要です。
CONNECT
メソッド
HTTP CONNECT
メソッドは、主にHTTPSリクエストをHTTPプロキシ経由で送信する際に使用されます。クライアントはプロキシに対してCONNECT
リクエストを送信し、プロキシがこれを受け入れると、クライアントとターゲットサーバー間のTCPトンネルが確立されます。このトンネルが確立された後は、プロキシは単にクライアントとサーバー間の生データを転送するだけになります。一度CONNECT
トンネルが確立されると、その基盤となるTCP接続はキープアライブされ、同じターゲットホストへの後続のHTTPSリクエストで再利用できます。
connectMethod
構造体とキャッシュキー
Goのnet/http
パッケージ内部では、connectMethod
という構造体が、特定の接続方法(プロキシの使用、スキーム、ターゲットアドレスなど)を識別するために使用されます。この構造体は、http.Transport
が接続プール内で接続をキャッシュする際のキーとして利用されます。キャッシュキーが異なると、たとえ同じプロキシへの接続であっても、新しい接続が確立されてしまいます。
技術的詳細
このコミットの核心は、net/http
パッケージ内のconnectMethod
構造体のString()
メソッドの変更にあります。このメソッドは、接続プールにおけるキャッシュキーを生成するために使用されます。
変更前は、connectMethod
のString()
メソッドは、プロキシ文字列、ターゲットスキーム、そして常にターゲットアドレスを結合してキャッシュキーを生成していました。
func (ck *connectMethod) String() string {
proxyStr := ""
if ck.proxyURL != nil {
proxyStr = ck.proxyURL.String()
}
return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|")
}
この実装の問題点は、HTTPスキーム(http
)のリクエストであってもck.targetAddr
(ターゲットアドレス)がキャッシュキーに含まれてしまうことでした。HTTPプロキシを介したHTTPリクエストの場合、プロキシへの接続は、そのプロキシを介してアクセスされる具体的なターゲットアドレスに依存すべきではありません。プロキシへの接続自体は、どのHTTPサーバーにアクセスするかに関わらず、プロキシサーバーのアドレスに対して確立されるべきだからです。
例えば、http://proxy.example.com
を介してhttp://server1.example.com
とhttp://server2.example.com
にアクセスする場合を考えます。変更前の実装では、server1.example.com
とserver2.example.com
が異なるため、connectMethod
のキャッシュキーも異なり、結果としてプロキシへの新しい接続がそれぞれ確立されていました。
このコミットでは、この挙動を修正し、HTTPスキームのリクエストの場合にのみ、キャッシュキーからターゲットアドレスを除外するように変更しています。
func (ck *connectMethod) String() string {
proxyStr := ""
targetAddr := ck.targetAddr // デフォルトでターゲットアドレスを使用
if ck.proxyURL != nil {
proxyStr = ck.proxyURL.String()
if ck.targetScheme == "http" { // HTTPスキームの場合のみ
targetAddr = "" // ターゲットアドレスを空にする
}
}
return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|")
}
この変更により、HTTPプロキシを介したHTTPリクエストでは、キャッシュキーがプロキシのアドレスとスキームのみに基づいて生成されるようになります。これにより、同じプロキシサーバーを使用し、かつHTTPスキームのリクエストであれば、異なるターゲットアドレスへのリクエストであっても、既存のプロキシ接続を再利用できるようになります。これは、特に多数のHTTPリクエストをプロキシ経由で送信する際のパフォーマンス向上に寄与します。
また、この変更を検証するために、proxy_test.go
に新しいテストケースTestCacheKeys
が追加されています。このテストは、様々なプロキシ設定とスキームの組み合わせに対して、connectMethod.String()
が期待通りのキャッシュキーを生成するかどうかを検証します。特に、HTTPプロキシを介したHTTPリクエストの場合にターゲットアドレスがキャッシュキーに含まれないことを確認するテストケースが含まれています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の2ファイルにあります。
-
src/pkg/net/http/proxy_test.go
:net/url
パッケージのインポートが追加されました。cacheKeysTests
という新しいテストデータ構造が定義されました。これは、プロキシURL、スキーム、ターゲットアドレス、そして期待されるキャッシュキーの組み合わせを定義します。TestCacheKeys
という新しいテスト関数が追加されました。この関数はcacheKeysTests
の各エントリをループし、connectMethod
のString()
メソッドが正しいキャッシュキーを生成するかどうかを検証します。
-
src/pkg/net/http/transport.go
:connectMethod
構造体のString()
メソッドが変更されました。- 変更前は、
strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|")
のように、常にck.targetAddr
を含んでいました。 - 変更後は、
targetAddr
という新しいローカル変数を導入し、ck.targetAddr
で初期化します。 ck.proxyURL
がnil
でない(プロキシが設定されている)かつ、ck.targetScheme
が"http"
である場合にのみ、targetAddr
を空文字列に設定する条件分岐が追加されました。- 最終的に、
strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|")
として、修正されたtargetAddr
を使用するように変更されました。
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更
// 変更前
// func (ck *connectMethod) String() string {
// proxyStr := ""
// if ck.proxyURL != nil {
// proxyStr = ck.proxyURL.String()
// }
// return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|")
// }
// 変更後
func (ck *connectMethod) String() string {
proxyStr := ""
targetAddr := ck.targetAddr // ① ターゲットアドレスをデフォルト値として保持
if ck.proxyURL != nil { // ② プロキシが設定されている場合
proxyStr = ck.proxyURL.String()
if ck.targetScheme == "http" { // ③ ターゲットスキームがHTTPの場合
targetAddr = "" // ④ ターゲットアドレスを空にする
}
}
// ⑤ 最終的なキャッシュキーを生成
return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|")
}
targetAddr := ck.targetAddr
: まず、connectMethod
の元のターゲットアドレスをtargetAddr
というローカル変数にコピーします。これは、後で条件付きで変更される可能性があるためです。if ck.proxyURL != nil
: この条件は、リクエストがプロキシを介して行われるかどうかをチェックします。プロキシが設定されていない場合、proxyStr
は空のままで、targetAddr
は元のck.targetAddr
のまま使用されます。if ck.targetScheme == "http"
: プロキシが設定されており、かつターゲットスキームが"http"
(つまり、HTTPプロキシを介して通常のHTTPリクエストを行う場合)であるかをチェックします。targetAddr = ""
: 上記の条件(プロキシ経由のHTTPリクエスト)が真である場合、targetAddr
を空文字列に設定します。これにより、この特定のシナリオでは、ターゲットアドレスがキャッシュキーの一部として使用されなくなります。return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|")
: 最終的に、proxyStr
、ck.targetScheme
、そして(必要に応じて修正された)targetAddr
を|
で結合して、接続プールのキャッシュキーとして使用される文字列を生成します。
この変更により、HTTPプロキシを介したHTTPリクエストの場合、キャッシュキーは"http://proxy.example.com|http|"
のようになり、ターゲットサーバーのアドレス(例: server1.example.com
やserver2.example.com
)は含まれなくなります。これにより、同じプロキシへの接続であれば、異なるHTTPターゲットへのリクエストでも同じ接続を再利用できるようになります。
src/pkg/net/http/proxy_test.go
の変更
var cacheKeysTests = []struct {
proxy string
scheme string
addr string
key string
}{
{"", "http", "foo.com", "|http|foo.com"}, // プロキシなし、HTTP
{"", "https", "foo.com", "|https|foo.com"}, // プロキシなし、HTTPS
{"http://foo.com", "http", "foo.com", "http://foo.com|http|"}, // プロキシあり、HTTP: addrが空になることを期待
{"http://foo.com", "https", "foo.com", "http://foo.com|https|foo.com"}, // プロキシあり、HTTPS: addrが残ることを期待
}
func TestCacheKeys(t *testing.T) {
for _, tt := range cacheKeysTests {
var proxy *url.URL
if tt.proxy != "" {
u, err := url.Parse(tt.proxy)
if err != nil {
t.Fatal(err)
}
proxy = u
}
cm := connectMethod{proxy, tt.scheme, tt.addr}
if cm.String() != tt.key {
t.Fatalf("{%q, %q, %q} cache key %q; want %q", tt.proxy, tt.scheme, tt.addr, cm.String(), tt.key)
}
}
}
このテストは、connectMethod.String()
メソッドの修正が意図通りに機能することを確認します。特に重要なのは、以下のテストケースです。
{"http://foo.com", "http", "foo.com", "http://foo.com|http|"}
: このケースは、HTTPプロキシを介してHTTPリクエストを行うシナリオをテストします。期待されるキーは"http://foo.com|http|"
であり、ターゲットアドレスである"foo.com"
が含まれていないことを示しています。これは、transport.go
の変更が正しく適用されたことを検証します。{"http://foo.com", "https", "foo.com", "http://foo.com|https|foo.com"}
: このケースは、HTTPプロキシを介してHTTPSリクエストを行うシナリオをテストします。HTTPSの場合、CONNECT
メソッドが使用され、ターゲットアドレスはトンネルの確立に必要であるため、キャッシュキーにターゲットアドレスが含まれることが期待されます。このテストケースは、HTTP以外のスキームではターゲットアドレスが削除されないことを検証します。
これらの変更とテストにより、Goのnet/http
パッケージは、HTTPプロキシを介したHTTPリクエストにおいて、より効率的な接続再利用を実現し、全体的なネットワークパフォーマンスを向上させます。
関連リンク
- Go
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go
http.Transport
のドキュメント: https://pkg.go.dev/net/http#Transport - Go
net/url
パッケージのドキュメント: https://pkg.go.dev/net/url - HTTP
CONNECT
メソッドに関するMDN Web Docs: https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/CONNECT
参考にした情報源リンク
- Goの
net/http
パッケージにおけるコネクションプーリングとCONNECT
メソッドに関するWeb検索結果:- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF52jUZgXzewZkRTdnF0nkNGtE5DUsWvfynfPeV3thIghrjpek0rGOsXyCqvcjAMt26jRJjYRhsosTULmv2O-KfqQ11s4Hm6z3UFBPcBpNyXgLwY7k4WA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGNjgv4eQZEseUfmk9383_lLpWoH1xczArkNBeB3qDteHKYRsJmbkcCj-yLK403NkNkfq7pWygk2MxfHe_YTx4_1oF_VkBKY7Vf4CpPiyEF7INYREOBMQLL3mGYtJZB8xYxll4=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEn55DmPowHjW00cLIr6T8I7CrA4eiE_i8MtPILbWGgCppZdYbu12W-bdORtX0zK0x4qMidOGg9UgRQ39Df7Hf18Ar5Igb1vf1R7d2SBeUspq2_wXc-55bNTiET7Xusx2ym-xFIPieyA2ivpXUx-WH6hb1P4gbLHjhJKYsCrnEzzYT3Z3g_3LU1WMs=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGZZf9kwjNafmWDGZchFte2P84xOJkdwBYBgV0RHLF6PdEk5YB4xGWdUE2XsqCixS2O25BWSsbuMWzfpWRtBnjKhJDVcF2OxzuSDTCeg48geHa_fRbVcrYhru2-oUG_XFOKt1O1ZtJXL0WFQHUgwLsUjQlKiDWwRqpWqQKu