[インデックス 18939] ファイルの概要
このコミットは、Goのnet/http
パッケージにおいて、TLS接続がサーバー側で予期せず閉じられた(EOFが検出された)場合に、その接続を再利用しないようにするための改善です。特に、Amazonのような特定のサーバーとの永続的なHTTPS接続の堅牢性を高めることを目的としています。
コミット
commit cc2c5fc3d28ef2e179e605fa41d5e7eec04e34ac
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Mar 25 10:59:09 2014 -0700
net/http: don't re-use Transport connections if we've seen an EOF
This the second part of making persistent HTTPS connections to
certain servers (notably Amazon) robust.
See the story in part 1: https://golang.org/cl/76400046/
This is the http Transport change that notes whether our
net.Conn.Read has ever seen an EOF. If it has, then we use
that as an additional signal to not re-use that connection (in
addition to the HTTP response headers)
Fixes #3514
LGTM=rsc
R=agl, rsc
CC=golang-codereviews
https://golang.org/cl/79240044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cc2c5fc3d28ef2e179e605fa41d5e7eec04e34ac
元コミット内容
このコミットは、net/http
パッケージのTransport
において、接続がEOF(End Of File)を検出した場合にその接続を再利用しないようにするものです。これは、特定のサーバー(特にAmazon)への永続的なHTTPS接続をより堅牢にするための第二段階の変更です。
この変更は、net.Conn.Read
がEOFを検出したかどうかを記録し、その情報をHTTPレスポンスヘッダーに加えて、接続を再利用しないための追加のシグナルとして使用します。
このコミットは、Issue 3514を修正します。関連する第一段階の変更については、CL 76400046を参照してください。
変更の背景
この変更の背景には、Goのnet/http
クライアントが、サーバーによって既に閉じられたTLS接続を再利用しようとすることで発生する問題がありました。これはIssue 3514として報告されており、「net/http: http tries to reuse dead TLS connections」(net/http: httpが死んだTLS接続を再利用しようとする)というタイトルで、特に連続したリクエストを行う際に、サーバーが接続を閉じた後にクライアントがその接続に書き込もうとしてEOF
エラーや「http: can't write HTTP request on broken connection」といったエラーが発生する問題が指摘されていました。
この問題は、サーバーがclose_notify
アラートを送信してTLS接続を正常に閉じようとした場合でも、Goのnet/http
クライアントがすぐにそのEOF状態を認識せず、接続がまだ有効であると誤認してしまうレースコンディションに起因していました。
このコミットは、その問題に対する「第二段階」の解決策として位置づけられています。第一段階の変更(CL 76400046)では、crypto/tls
パッケージのRead
メソッドがclose_notify
アラートをより積極的に処理し、即座にio.EOF
を返すように修正されました。これにより、TLS層での接続終了の検出が改善されました。
しかし、TLS層でのEOF検出だけでは不十分なケースがありました。例えば、サーバーがHTTPレスポンスを送信し終えた後、TCPレベルで接続を突然切断するような場合です。このような場合、TLS層ではclose_notify
が送信されないため、クライアントは接続が閉じられたことを即座に認識できません。このコミットは、net.Conn.Read
が実際にEOFを検出したという事実を、接続再利用の判断材料に加えることで、より広範なシナリオでの接続堅牢性を確保しようとするものです。
前提知識の解説
- HTTP永続的接続 (Persistent Connections / Keep-Alive): HTTP/1.1では、複数のHTTPリクエスト/レスポンスを単一のTCP接続上で送受信できる「永続的接続」がデフォルトで有効になっています。これにより、接続の確立と切断にかかるオーバーヘッドが削減され、パフォーマンスが向上します。
- TLS (Transport Layer Security): ネットワーク通信のセキュリティを確保するためのプロトコルです。HTTPSはHTTP通信をTLSで暗号化したものです。TLS接続の終了には、通常、両端が
close_notify
アラートを交換して正常に接続を閉じる手順があります。 net/http/Transport
: Goのnet/http
パッケージにおける、HTTPリクエストの送信とレスポンスの受信を処理する低レベルのコンポーネントです。接続のプール、プロキシの処理、TLSハンドシェイクなどを担当します。Transport
は、永続的接続を管理し、アイドル状態の接続を再利用することで効率を高めます。bufio.Reader
:io.Reader
インターフェースをラップし、バッファリング機能を追加するGoの標準ライブラリの型です。これにより、小さな読み取り操作が効率化されます。io.EOF
:io.Reader
のRead
メソッドが、それ以上読み取るデータがないことを示すために返すエラーです。これは、ストリームの終端に達したことを意味します。- 接続の再利用 (Connection Reuse):
net/http/Transport
は、パフォーマンス向上のために、一度確立されたTCP/TLS接続を複数のHTTPリクエストで再利用しようとします。しかし、サーバーが接続を閉じたにもかかわらずクライアントがそれを認識できない場合、無効な接続を再利用しようとしてエラーが発生します。
技術的詳細
このコミットの技術的な核心は、net.Conn.Read
がEOFを検出したという事実を、net/http/Transport
が管理する接続の状態に反映させることです。これにより、たとえHTTPレスポンスヘッダーが接続の永続化を示唆していても、基盤となる接続が既に閉じられている(または閉じられつつある)場合に、その接続の再利用を避けることができます。
具体的な変更点は以下の通りです。
-
persistConn.sawEOF
フィールドの追加:src/pkg/net/http/transport.go
のpersistConn
構造体に、sawEOF bool
という新しいフィールドが追加されました。このフィールドは、persistConn
がラップするnet.Conn
からEOFが検出されたかどうかを追跡します。readLoop
によって所有され、読み取り操作中に設定されます。 -
noteEOFReader
ラッパーの導入:io.Reader
インターフェースを実装する新しい型noteEOFReader
が定義されました。このラッパーは、内部のio.Reader
(ここではnet.Conn
)からの読み取り操作を監視します。Read
メソッドがio.EOF
エラーを返した場合、noteEOFReader
はsawEOF
フラグ(persistConn.sawEOF
へのポインタ)をtrue
に設定します。 -
bufio.Reader
の初期化変更:Transport.dialConn
メソッド内で、persistConn.br
(bufio.Reader
)を初期化する際に、直接pconn.conn
を渡すのではなく、noteEOFReader{pconn.conn, &pconn.sawEOF}
でラップしたものを渡すように変更されました。これにより、bufio.Reader
が基盤となる接続から読み取る際に、EOFが検出されるとpconn.sawEOF
が自動的に更新されるようになります。 -
readLoop
でのsawEOF
の利用:persistConn.readLoop
メソッド内で、接続をアイドルプールに戻す前に、pc.sawEOF
フラグがチェックされるようになりました。もしpc.sawEOF
がtrue
であれば、たとえ他の条件(例えば、HTTPレスポンスヘッダーがConnection: keep-alive
を示していても)が接続の再利用を許可していても、その接続は再利用されずに閉じられます。具体的には、alive1
という接続が再利用可能かどうかを示すフラグが、pc.sawEOF
がtrue
の場合にfalse
に設定されます。
これらの変更により、GoのHTTPクライアントは、サーバーが明示的にclose_notify
を送信しない場合や、TCPレベルで突然接続を切断した場合でも、基盤となる接続のEOF状態をより正確に検出し、無効な接続の再利用を避けることができるようになります。これにより、特にAmazonのような、接続管理の挙動が異なる可能性のあるサーバーとの通信において、HTTPクライアントの堅牢性が向上します。
コアとなるコードの変更箇所
src/pkg/net/http/transport.go
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -588,7 +588,7 @@ func (t *Transport) dialConn(cm connectMethod) (*persistConn, error) {
pconn.conn = tlsConn
}
- pconn.br = bufio.NewReader(pconn.conn)
+ pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF})
pconn.bw = bufio.NewWriter(pconn.conn)
go pconn.readLoop()
go pconn.writeLoop()
@@ -723,6 +723,7 @@ type persistConn struct {
tlsState *tls.ConnectionState
closed bool // whether conn has been closed
br *bufio.Reader // from conn
+ sawEOF bool // whether we've seen EOF from conn; owned by readLoop
bw *bufio.Writer // to conn
reqch chan requestAndChan // written by roundTrip; read by readLoop
writech chan writeRequest // written by roundTrip; read by writeLoop
@@ -841,6 +842,9 @@ func (pc *persistConn) readLoop() {
if err != nil {
alive1 = false
}
+ if alive1 && pc.sawEOF {
+ alive1 = false
+ }
if alive1 && !pc.t.putIdleConn(pc) {
alive1 = false
}
@@ -1134,3 +1138,16 @@ type tlsHandshakeTimeoutError struct{}
func (tlsHandshakeTimeoutError) Timeout() bool { return true }
func (tlsHandshakeTimeoutError) Temporary() bool { return true }
func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" }
+
+type noteEOFReader struct {
+ r io.Reader
+ sawEOF *bool
+}
+
+func (nr noteEOFReader) Read(p []byte) (n int, err error) {
+ n, err = nr.r.Read(p)
+ if err == io.EOF {
+ *nr.sawEOF = true
+ }
+ return
+}
src/pkg/net/http/transport_test.go
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -11,6 +11,7 @@ import (
"bytes"
"compress/gzip"
"crypto/rand"
+ "crypto/tls"
"errors"
"fmt"
"io"
@@ -1836,6 +1837,71 @@ func TestTransportTLSHandshakeTimeout(t *testing.T) {
}
}
+// Trying to repro golang.org/issue/3514
+func TestTLSServerClosesConnection(t *testing.T) {
+ defer afterTest(t)
+ closedc := make(chan bool, 1)
+ ts := httptest.NewTLSServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ if strings.Contains(r.URL.Path, "/keep-alive-then-die") {
+ conn, _, _ := w.(Hijacker).Hijack()
+ conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo"))
+ conn.Close()
+ closedc <- true
+ return
+ }
+ fmt.Fprintf(w, "hello")
+ }))
+ defer ts.Close()
+ tr := &Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ }
+ defer tr.CloseIdleConnections()
+ client := &Client{Transport: tr}
+
+ var nSuccess = 0
+ var errs []error
+ const trials = 20
+ for i := 0; i < trials; i++ {
+ tr.CloseIdleConnections()
+ res, err := client.Get(ts.URL + "/keep-alive-then-die")
+ if err != nil {
+ t.Fatal(err)
+ }
+ <-closedc
+ slurp, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(slurp) != "foo" {
+ t.Errorf("Got %q, want foo", slurp)
+ }
+
+ // Now try again and see if we successfully
+ // pick a new connection.
+ res, err = client.Get(ts.URL + "/")
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ slurp, err = ioutil.ReadAll(res.Body)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ nSuccess++
+ }
+ if nSuccess > 0 {
+ t.Logf("successes = %d of %d", nSuccess, trials)
+ } else {
+ t.Errorf("All runs failed:")
+ }
+ for _, err := range errs {
+ t.Logf(" err: %v", err)
+ }
+}
+
func newLocalListener(t *testing.T) net.Listener {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
コアとなるコードの解説
src/pkg/net/http/transport.go
-
persistConn.sawEOF bool
の追加:persistConn
は、HTTPトランスポートが管理する単一の永続的接続を表す構造体です。このsawEOF
フィールドは、この接続の基盤となるnet.Conn
からio.EOF
が読み取られたかどうかを記録します。これはreadLoop
ゴルーチンによってのみ書き込まれ、読み取り操作中にEOFが検出された場合にtrue
に設定されます。 -
noteEOFReader
構造体とRead
メソッド: これはio.Reader
インターフェースを満たす新しいヘルパー型です。r io.Reader
: ラップする元のリーダー(この場合はnet.Conn
)。sawEOF *bool
:persistConn.sawEOF
フィールドへのポインタ。Read
メソッドは、まず内部のr.Read(p)
を呼び出します。もしこの呼び出しがio.EOF
を返した場合、*nr.sawEOF
(つまりpersistConn.sawEOF
)をtrue
に設定します。これにより、bufio.Reader
が基盤の接続から読み取る際にEOFが検出されると、persistConn
の状態が更新されるようになります。
-
Transport.dialConn
でのbufio.NewReader
の変更: 以前はpconn.br = bufio.NewReader(pconn.conn)
のように直接net.Conn
をbufio.Reader
に渡していました。 変更後はpconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF})
となり、net.Conn
がnoteEOFReader
でラップされてからbufio.Reader
に渡されます。これにより、bufio.Reader
がデータを読み取るたびに、noteEOFReader
がEOFの有無を監視し、persistConn.sawEOF
を適切に更新するようになります。 -
persistConn.readLoop
でのsawEOF
の利用:readLoop
は、HTTPレスポンスを読み取り、接続をアイドルプールに戻すかどうかを決定するゴルーチンです。if alive1 && pc.sawEOF { alive1 = false }
という行が追加されました。alive1
は、接続が再利用可能であると判断された場合にtrue
になるフラグです。- この新しい条件は、「もし接続がまだ
alive
(再利用可能と判断されそう)であり、かつsawEOF
がtrue
(基盤の接続でEOFが検出された)ならば、alive1
をfalse
にする」ことを意味します。 これにより、たとえHTTPレスポンスヘッダーがConnection: keep-alive
を示していても、実際に基盤の接続が閉じられたことが検出された場合は、その接続はアイドルプールに戻されず、再利用されなくなります。
src/pkg/net/http/transport_test.go
TestTLSServerClosesConnection
テストの追加: この新しいテストは、Issue 3514で報告された問題を再現し、このコミットによる修正が正しく機能することを確認するために追加されました。- テストサーバーは、
/keep-alive-then-die
パスへのリクエストに対して、HTTPレスポンスを送信した直後に接続を強制的に閉じます(conn.Close()
)。 - クライアントはまず
/keep-alive-then-die
にリクエストを送信し、サーバーが接続を閉じることを確認します。 - その後、同じクライアントを使って
/
に別のリクエストを送信します。このとき、以前の接続が再利用されずに新しい接続が確立されることを期待します。 - テストはこれを複数回(20回)繰り返し、新しい接続が成功裏に確立される回数をカウントします。もし修正がなければ、2回目以降のリクエストでエラーが発生する可能性が高くなります。このテストは、
sawEOF
フラグが正しく機能し、死んだ接続が再利用されないことを検証します。
- テストサーバーは、
これらの変更により、Goのnet/http
クライアントは、サーバーが接続を閉じたことをより確実に検出し、無効な接続を再利用しようとすることを防ぎ、結果としてHTTP通信の堅牢性と信頼性が向上します。
関連リンク
- 関連Issue: Issue 3514: net/http: http tries to reuse dead TLS connections
- 関連Change List (Part 1): CL 76400046: crypto/tls: return io.EOF from Read when close_notify is received
- このコミットのChange List: CL 79240044 (コミットメッセージに記載されているが、直接のリンクは提供されていないため、GitHubのコミットページが最も直接的な情報源)
参考にした情報源リンク
- GitHub - golang/go: net/http: http tries to reuse dead TLS connections #3514
- Go Code Review - crypto/tls: return io.EOF from Read when close_notify is received
- Go Code Review - net/http: don't re-use Transport connections if we've seen an EOF (これはコミットメッセージに記載されているCLのリンクですが、直接アクセスするとコードレビューページに飛びます)
- Go issue 3514: net/http: http tries to reuse dead TLS connections - Google Search
- golang.org/cl/76400046/ - Google Search
- Go の net/http パッケージの Transport について (一般的な
net/http/Transport
の理解のため) - Go言語のio.Readerとio.Writerについて (io.Readerとio.EOFの理解のため)
- TLSのclose_notifyとは何か (TLSのclose_notifyの理解のため)
- HTTP Keep-Aliveとは (HTTP Keep-Aliveの理解のため)