[インデックス 13096] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける、HTTPコネクションのクローズ処理に関するバグ修正と改善を目的としています。具体的には、Request.Close
または Response.Close
が true
に設定されている場合、あるいはサーバーが Connection: close
ヘッダーを送信した場合に、コネクションが適切にクローズされない問題を解決します。
コミット
net/http
: 非キープアライブコネクションが正常にクローズされるように修正
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ccd63c3c19b1598d8e717c1575a01f77dd83a499
元コミット内容
commit ccd63c3c19b1598d8e717c1575a01f77dd83a499
Author: James Gray <james@james4k.com>
Date: Fri May 18 10:34:37 2012 -0700
net/http: non-keepalive connections close successfully
Connections did not close if Request.Close or Response.Close was true. This meant that if the user wanted the connection to close, or if the server requested it via "Connection: close", the connection would not be closed.
Fixes #1967.
R=golang-dev, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/6201044
---
src/pkg/net/http/transport.go | 4 ++\n src/pkg/net/http/transport_test.go | 86 +++++++++++++++++++++++++++++++++++++-\n 2 files changed, 88 insertions(+), 2 deletions(-)\n\ndiff --git a/src/pkg/net/http/transport.go b/src/pkg/net/http/transport.go\nindex 024975946e..5f3d3fbfb1 100644\n--- a/src/pkg/net/http/transport.go\n+++ b/src/pkg/net/http/transport.go\n@@ -599,6 +599,10 @@ func (pc *persistConn) readLoop() {\n \t\t// before we race and peek on the underlying bufio reader.\n \t\tif waitForBodyRead != nil {\n \t\t\t<-waitForBodyRead\n+\t\t} else if !alive {\n+\t\t\t// If waitForBodyRead is nil, and we\'re not alive, we\n+\t\t\t// must close the connection before we leave the loop.\n+\t\t\tpc.close()\n \t\t}\n \t}\n }\ndiff --git a/src/pkg/net/http/transport_test.go b/src/pkg/net/http/transport_test.go\nindex a9e401de58..ebf4a8102d 100644\n--- a/src/pkg/net/http/transport_test.go\n+++ b/src/pkg/net/http/transport_test.go\n@@ -13,6 +13,7 @@ import (\n \t\"fmt\"\n \t\"io\"\n \t\"io/ioutil\"\n+\t\"net\"\n \t. \"net/http\"\n \t\"net/http/httptest\"\n \t\"net/url\"\n@@ -20,6 +21,7 @@ import (\n \t\"runtime\"\n \t\"strconv\"\n \t\"strings\"\n+\t\"sync\"\n \t\"testing\"\n \t\"time\"\n )\n@@ -35,6 +37,64 @@ var hostPortHandler = HandlerFunc(func(w ResponseWriter, r *Request) {\n \tw.Write([]byte(r.RemoteAddr))\n })\n \n+type testCloseConn struct {\n+\tnet.Conn\n+\tset *testConnSet\n+}\n+\n+func (conn *testCloseConn) Close() error {\n+\tconn.set.remove(conn)\n+\treturn conn.Conn.Close()\n+}\n+\n+type testConnSet struct {\n+\tset map[net.Conn]bool\n+\tmutex sync.Mutex\n+}\n+\n+func (tcs *testConnSet) insert(c net.Conn) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\ttcs.set[c] = true\n+}\n+\n+func (tcs *testConnSet) remove(c net.Conn) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\t// just change to false, so we have a full set of opened connections\n+\ttcs.set[c] = false\n+}\n+\n+// some tests use this to manage raw tcp connections for later inspection\n+func makeTestDial() (*testConnSet, func(n, addr string) (net.Conn, error)) {\n+\tconnSet := &testConnSet{\n+\t\tset: make(map[net.Conn]bool),\n+\t}\n+\tdial := func(n, addr string) (net.Conn, error) {\n+\t\tc, err := net.Dial(n, addr)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\ttc := &testCloseConn{c, connSet}\n+\t\tconnSet.insert(tc)\n+\t\treturn tc, nil\n+\t}\n+\treturn connSet, dial\n+}\n+\n+func (tcs *testConnSet) countClosed() (closed, total int) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\n+\ttotal = len(tcs.set)\n+\tfor _, open := range tcs.set {\n+\t\tif !open {\n+\t\t\tclosed += 1\n+\t\t}\n+\t}\n+\treturn\n+}\n+\n // Two subsequent requests and verify their response is the same.\n // The response from the server is our own IP:port\n func TestTransportKeepAlives(t *testing.T) {\n@@ -72,8 +132,12 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {\n \tts := httptest.NewServer(hostPortHandler)\n \tdefer ts.Close()\n \n+\tconnSet, testDial := makeTestDial()\n+\n \tfor _, connectionClose := range []bool{false, true} {\n-\t\ttr := &Transport{}\n+\t\ttr := &Transport{\n+\t\t\tDial: testDial,\n+\t\t}\n \t\tc := &Client{Transport: tr}\n \n \t\tfetch := func(n int) string {\n@@ -107,6 +171,13 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {\n \t\t\tt.Errorf(\"error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q\",\n \t\t\t\tconnectionClose, bodiesDiffer, body1, body2)\n \t\t}\n+\n+\t\ttr.CloseIdleConnections()\n+\t}\n+\n+\tclosed, total := connSet.countClosed()\n+\tif closed < total {\n+\t\tt.Errorf(\"%d out of %d tcp connections were not closed\", total-closed, total)\n \t}\n }\n \n@@ -114,8 +185,12 @@ func TestTransportConnectionCloseOnRequest(t *testing.T) {\n \tts := httptest.NewServer(hostPortHandler)\n \tdefer ts.Close()\n \n+\tconnSet, testDial := makeTestDial()\n+\n \tfor _, connectionClose := range []bool{false, true} {\n-\t\ttr := &Transport{}\n+\t\ttr := &Transport{\n+\t\t\tDial: testDial,\n+\t\t}\n \t\tc := &Client{Transport: tr}\n \n \t\tfetch := func(n int) string {\n@@ -149,6 +224,13 @@ func TestTransportConnectionCloseOnRequest(t *testing.T) {\n \t\t\tt.Errorf(\"error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q\",\n \t\t\t\tconnectionClose, bodiesDiffer, body1, body2)\n \t\t}\n+\n+\t\ttr.CloseIdleConnections()\n+\t}\n+\n+\tclosed, total := connSet.countClosed()\n+\tif closed < total {\n+\t\tt.Errorf(\"%d out of %d tcp connections were not closed\", total-closed, total)\n \t}\n }\n \n```
## 変更の背景
このコミットは、Go言語の `net/http` パッケージにおける重要なバグ修正に対応しています。具体的には、Issue #1967 で報告された問題に対処しています。この問題は、HTTPクライアントが明示的にコネクションをクローズしたいと要求した場合(`Request.Close = true`)や、HTTPサーバーがレスポンスヘッダーでコネクションのクローズを指示した場合(`Connection: close`)に、`net/http` の `Transport` が基盤となるTCPコネクションを適切にクローズしないというものでした。
HTTP/1.1では、デフォルトでコネクションの再利用(キープアライブ)が有効になっていますが、特定のシナリオではコネクションをクローズする必要があります。例えば、クライアントが単一のリクエストのみを送信し、その後コネクションを解放したい場合や、サーバーがリソースの制約やプロトコルの都合上、コネクションを維持できない場合に `Connection: close` ヘッダーを送信します。
このバグが存在すると、これらのシナリオでコネクションが閉じられず、以下のような問題が発生する可能性がありました。
* **リソースリーク**: クライアント側で不要なTCPコネクションが開きっぱなしになり、ファイルディスクリプタやメモリなどのシステムリソースを消費し続ける。
* **サーバー側の負荷増大**: サーバー側でも不要なコネクションが維持され、リソースを圧迫し、スケーラビリティに影響を与える。
* **予期せぬ動作**: コネクションが閉じられることを期待しているアプリケーションが、閉じられないことによってデッドロックやタイムアウトなどの予期せぬ動作を引き起こす。
* **プロトコル違反**: `Connection: close` ヘッダーが無視されることで、HTTPプロトコルの意図に反する動作となる。
このコミットは、これらの問題を解決し、`net/http` パッケージがHTTPプロトコルの仕様に厳密に従い、コネクション管理をより堅牢に行うことを保証します。
## 前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
### 1. HTTP/1.1におけるコネクション管理
* **キープアライブ (Keep-Alive)**: HTTP/1.1のデフォルトの動作です。クライアントとサーバー間で一度TCPコネクションを確立すると、複数のHTTPリクエスト/レスポンスをそのコネクション上で送受信できます。これにより、コネクション確立のオーバーヘッド(TCPハンドシェイク、TLSハンドシェイクなど)を削減し、パフォーマンスを向上させます。
* **非キープアライブ (Non-Keep-Alive)**: 各HTTPリクエスト/レスポンスの後にTCPコネクションをクローズする動作です。これは、HTTP/1.0のデフォルト動作でした。HTTP/1.1でも、特定のヘッダーを使用することで非キープアライブ動作を明示的に要求できます。
* **`Connection` ヘッダー**: HTTPヘッダーの一つで、コネクションに関する制御情報を提供します。
* `Connection: close`: 送信側が、現在のリクエスト/レスポンスの処理後にコネクションをクローズすることを意図していることを示します。
* `Connection: keep-alive`: コネクションを維持することを意図していることを示します(HTTP/1.1では通常省略されますが、HTTP/1.0でキープアライブを要求する際に使用されました)。
* **`Request.Close` と `Response.Close`**: Goの `net/http` パッケージにおけるフィールドで、それぞれクライアントがリクエスト送信後にコネクションをクローズしたいか、サーバーがレスポンス送信後にコネクションをクローズしたいかを示すフラグです。これらが `true` に設定されている場合、コネクションはクローズされるべきです。
### 2. Go言語の `net/http` パッケージ
* **`http.Client`**: HTTPリクエストを送信するためのクライアントです。
* **`http.Transport`**: `http.Client` の内部で使用され、実際のHTTPリクエストの送信、コネクションの管理(キープアライブ、プロキシなど)、TLSハンドシェイクなどを担当します。`Transport` はコネクションプールを管理し、効率的なコネクション再利用を可能にします。
* **`persistConn`**: `http.Transport` の内部で使用される構造体で、単一の永続的なHTTPコネクション(TCPコネクション)を表します。この構造体は、リクエストの送信、レスポンスの受信、コネクションのライフサイクル管理を行います。
* **`readLoop()`**: `persistConn` のメソッドで、バックグラウンドでコネクションからレスポンスを読み取るためのゴルーチン内で実行されます。このループは、新しいリクエストが来るのを待ったり、レスポンスボディの読み取りが完了するのを待ったりします。
### 3. ゴルーチンとチャネル
* **ゴルーチン (Goroutine)**: Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
* **チャネル (Channel)**: ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期にも使用されます。このコミットでは、`waitForBodyRead` というチャネルが使用されており、レスポンスボディの読み取りが完了するのを待つために利用されています。
### 4. テスト駆動開発 (TDD) とテストコード
* Go言語の標準ライブラリは、堅牢なテストコードによって品質が保証されています。このコミットでも、バグ修正と同時に、その修正が正しく機能することを検証するための新しいテストケースが追加されています。
* `net.Conn`: Go言語のネットワークコネクションを表すインターフェースです。TCPコネクションなどもこのインターフェースを実装します。
* `httptest.NewServer`: テスト目的でHTTPサーバーを簡単に起動するためのユーティリティ関数です。
## 技術的詳細
このコミットの核心は、`net/http/transport.go` 内の `persistConn` 構造体の `readLoop()` メソッドの修正にあります。
`readLoop()` は、`persistConn` が管理するHTTPコネクション上で、レスポンスを継続的に読み取るためのゴルーチンです。このループは、新しいリクエストが来るのを待機し、レスポンスヘッダーを解析し、レスポンスボディの読み取りを処理します。
修正前のコードでは、`readLoop()` は `waitForBodyRead` チャネルが閉じられるのを待つか、または `alive` フラグが `false` になるまでループを継続していました。しかし、`waitForBodyRead` が `nil` であり、かつ `alive` が `false` の場合に、コネクションを明示的にクローズするロジックが欠けていました。
具体的には、以下の条件が揃った場合に問題が発生していました。
1. `waitForBodyRead` が `nil` である: これは、レスポンスボディの読み取りが不要な場合(例: HEADリクエスト)や、既にボディが読み終わっている場合などに発生します。
2. `alive` が `false` である: これは、クライアントが `Request.Close = true` を設定した場合、またはサーバーが `Connection: close` ヘッダーを送信した場合に設定されます。つまり、コネクションをクローズすべきであると判断された状態です。
この二つの条件が同時に満たされた場合、`readLoop()` は `waitForBodyRead` を待つこともなく、また `alive` が `false` であるためループを継続する意味もありませんでした。しかし、明示的な `pc.close()` 呼び出しがなかったため、コネクションが閉じられずにリソースリークが発生していました。
このコミットでは、この特定のケースを捕捉し、`pc.close()` を呼び出すことで、コネクションが適切にクローズされるように修正しています。
また、`transport_test.go` には、この修正を検証するための包括的なテストケースが追加されています。
* `testCloseConn` と `testConnSet` というヘルパー構造体が導入され、`net.Conn` の `Close()` メソッドが呼び出されたかどうかを追跡できるようになっています。これにより、テスト中に実際にTCPコネクションが閉じられたかどうかを検証できます。
* `makeTestDial` 関数は、カスタムの `Dial` 関数を生成し、`http.Transport` に設定することで、テスト対象のコネクションのライフサイクルを監視できるようにします。
* `TestTransportConnectionCloseOnResponse` と `TestTransportConnectionCloseOnRequest` という新しいテスト関数が追加され、それぞれレスポンス側とリクエスト側でコネクションクローズが要求された場合に、コネクションが正しく閉じられることを検証しています。これらのテストは、`Connection: close` ヘッダーの有無や `Request.Close` フラグの設定を変えながら、複数回リクエストを送信し、最終的に開かれたTCPコネクションがすべて閉じられていることを確認します。
## コアとなるコードの変更箇所
### `src/pkg/net/http/transport.go`
```diff
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -599,6 +599,10 @@ func (pc *persistConn) readLoop() {
// before we race and peek on the underlying bufio reader.
if waitForBodyRead != nil {
<-waitForBodyRead
+ } else if !alive {
+ // If waitForBodyRead is nil, and we're not alive, we
+ // must close the connection before we leave the loop.
+ pc.close()
}
}
}
src/pkg/net/http/transport_test.go
テストコードの変更は多岐にわたりますが、主要な追加は以下の通りです。
testCloseConn
構造体とClose()
メソッドの追加。testConnSet
構造体とinsert()
,remove()
,countClosed()
メソッドの追加。makeTestDial()
関数の追加。TestTransportConnectionCloseOnResponse
テスト関数の追加。TestTransportConnectionCloseOnRequest
テスト関数の追加。
これらのテストは、http.Transport
の Dial
フィールドにカスタムのダイヤラーを設定し、コネクションの開閉を監視することで、バグが修正されたことを検証しています。
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更点
readLoop()
メソッド内の変更は非常に小さいですが、その影響は大きいです。
if waitForBodyRead != nil {
<-waitForBodyRead
} else if !alive {
// If waitForBodyRead is nil, and we're not alive, we
// must close the connection before we leave the loop.
pc.close()
}
if waitForBodyRead != nil
: これは、レスポンスボディがまだ完全に読み込まれていない場合、またはボディの読み込みを待機する必要がある場合に実行される既存のロジックです。<-waitForBodyRead
は、ボディの読み込みが完了するまでゴルーチンをブロックします。else if !alive
: このelse if
ブロックが新たに追加された部分です。waitForBodyRead
がnil
であるということは、ボディの読み込みが不要であるか、既に完了していることを意味します。!alive
は、このpersistConn
がもはやキープアライブ状態ではなく、クローズされるべきであることを示します。これは、Request.Close = true
やConnection: close
ヘッダーによって設定されます。- この両方の条件が満たされた場合、つまり「ボディの読み込みを待つ必要がなく、かつコネクションをクローズすべきである」という状況で、以前はコネクションが閉じられずにループを抜けてしまっていました。
pc.close()
: この行が追加されたことで、上記の条件が満たされた場合にpersistConn
が管理する基盤となるTCPコネクションが明示的にクローズされるようになりました。これにより、リソースリークが防止され、HTTPプロトコルの意図通りにコネクションが管理されるようになります。
src/pkg/net/http/transport_test.go
の変更点
テストコードは、この修正が正しく機能することを保証するためのものです。
testCloseConn
とtestConnSet
:testCloseConn
はnet.Conn
をラップし、そのClose()
メソッドが呼び出された際に、testConnSet
にその情報を記録します。testConnSet
は、開かれたコネクションと閉じられたコネクションの状態を追跡するためのマップとミューテックス(並行アクセス保護のため)を保持します。insert()
はコネクションが開かれたときにマップに追加し、remove()
はコネクションが閉じられたときにマップ内の状態を更新します。countClosed()
は、開かれたコネクションと閉じられたコネクションの数を返します。
makeTestDial()
:- この関数は、
http.Transport
のDial
フィールドに設定できるカスタムのダイヤル関数を生成します。 - このカスタムダイヤル関数は、実際の
net.Dial
を呼び出してTCPコネクションを確立した後、そのコネクションをtestCloseConn
でラップし、testConnSet
に登録します。これにより、テスト中に開かれたすべてのコネクションを監視できるようになります。
- この関数は、
TestTransportConnectionCloseOnResponse
とTestTransportConnectionCloseOnRequest
:- これらのテストは、
httptest.NewServer
を使用してテスト用のHTTPサーバーを起動します。 http.Transport
のDial
フィールドにmakeTestDial()
で生成したカスタムダイヤル関数を設定します。- ループ内で
connectionClose
フラグをfalse
とtrue
に切り替えながら、リクエストを送信します。connectionClose = true
の場合、Request.Close = true
を設定するか、サーバーがConnection: close
ヘッダーを返すようにします。
- 各リクエストの後、
tr.CloseIdleConnections()
を呼び出して、アイドル状態のコネクションをクローズさせます。 - ループの最後に、
connSet.countClosed()
を呼び出して、開かれたすべてのコネクションが適切に閉じられたかどうかを検証します。もし閉じられていないコネクションがあれば、テストは失敗します。
- これらのテストは、
これらのテストは、net/http
パッケージがHTTPプロトコルの仕様に従い、ユーザーの意図やサーバーの指示に基づいてコネクションを正しくクローズすることを、厳密に検証しています。
関連リンク
- Go Issue #1967: net/http: non-keepalive connections close successfully
- Go Code Review: https://golang.org/cl/6201044
参考にした情報源リンク
- HTTP/1.1 Persistent Connections: https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go
net
package documentation: https://pkg.go.dev/net - Go
sync
package documentation: https://pkg.go.dev/sync - Go
testing
package documentation: https://pkg.go.dev/testing - Go
httptest
package documentation: https://pkg.go.dev/net/http/httptest - Go Concurrency Patterns: Goroutines and Channels: https://go.dev/blog/concurrency-patterns
- Understanding Go's net/http Transport: https://blog.golang.org/http-transport (このブログ記事はコミット当時のものではない可能性がありますが、
Transport
の概念理解に役立ちます)