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

[インデックス 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は、MaxIdleConnsIdleConnTimeoutMaxIdleConnsPerHostなどの設定を通じて、接続の再利用を細かく制御できます。効率的な接続再利用のためには、単一の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()メソッドの変更にあります。このメソッドは、接続プールにおけるキャッシュキーを生成するために使用されます。

変更前は、connectMethodString()メソッドは、プロキシ文字列、ターゲットスキーム、そして常にターゲットアドレスを結合してキャッシュキーを生成していました。

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.comhttp://server2.example.comにアクセスする場合を考えます。変更前の実装では、server1.example.comserver2.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ファイルにあります。

  1. src/pkg/net/http/proxy_test.go:

    • net/urlパッケージのインポートが追加されました。
    • cacheKeysTestsという新しいテストデータ構造が定義されました。これは、プロキシURL、スキーム、ターゲットアドレス、そして期待されるキャッシュキーの組み合わせを定義します。
    • TestCacheKeysという新しいテスト関数が追加されました。この関数はcacheKeysTestsの各エントリをループし、connectMethodString()メソッドが正しいキャッシュキーを生成するかどうかを検証します。
  2. src/pkg/net/http/transport.go:

    • connectMethod構造体のString()メソッドが変更されました。
    • 変更前は、strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|")のように、常にck.targetAddrを含んでいました。
    • 変更後は、targetAddrという新しいローカル変数を導入し、ck.targetAddrで初期化します。
    • ck.proxyURLnilでない(プロキシが設定されている)かつ、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}, "|")
}
  1. targetAddr := ck.targetAddr: まず、connectMethodの元のターゲットアドレスをtargetAddrというローカル変数にコピーします。これは、後で条件付きで変更される可能性があるためです。
  2. if ck.proxyURL != nil: この条件は、リクエストがプロキシを介して行われるかどうかをチェックします。プロキシが設定されていない場合、proxyStrは空のままで、targetAddrは元のck.targetAddrのまま使用されます。
  3. if ck.targetScheme == "http": プロキシが設定されており、かつターゲットスキームが"http"(つまり、HTTPプロキシを介して通常のHTTPリクエストを行う場合)であるかをチェックします。
  4. targetAddr = "": 上記の条件(プロキシ経由のHTTPリクエスト)が真である場合、targetAddrを空文字列に設定します。これにより、この特定のシナリオでは、ターゲットアドレスがキャッシュキーの一部として使用されなくなります。
  5. return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|"): 最終的に、proxyStrck.targetScheme、そして(必要に応じて修正された)targetAddr|で結合して、接続プールのキャッシュキーとして使用される文字列を生成します。

この変更により、HTTPプロキシを介したHTTPリクエストの場合、キャッシュキーは"http://proxy.example.com|http|"のようになり、ターゲットサーバーのアドレス(例: server1.example.comserver2.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リクエストにおいて、より効率的な接続再利用を実現し、全体的なネットワークパフォーマンスを向上させます。

関連リンク

参考にした情報源リンク