[インデックス 15599] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおけるHTTPクライアントの Response.Body.Close
メソッドの挙動を変更するものです。具体的には、HTTPレスポンスボディの読み取りが完了していない状態で Close
が呼び出された際に、TCPコネクションを即座に閉じるように修正されています。これにより、以前の「EOFまで読み続ける」という挙動に起因する潜在的な問題(EOFが来ない、または時間がかかりすぎる)が解消されます。
コミット
commit ce8341554caa8be64aeafd9bf2077a98db462fda
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Mar 5 18:47:27 2013 -0800
net/http: close TCP connection on Response.Body.Close
Previously the HTTP client's (*Response).Body.Close would try
to keep reading until EOF, hoping to reuse the keep-alive HTTP
connection, but the EOF might never come, or it might take a
long time. Now we immediately close the TCP connection if we
haven't seen EOF.
This shifts the burden onto clients to read their whole response
bodies if they want the advantage of reusing TCP connections.
In the future maybe we could decide on heuristics to read some
number of bytes for some max amount of time before forcefully
closing, but I'd rather not for now.
Statistically, touching this code makes things regress, so I
wouldn't be surprised if this introduces new bugs, but all the
tests pass, and I think the code is simpler now too. Maybe.
Please test your HTTP client code before Go 1.1.
Fixes #3672
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7419050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce8341554caa8be64aeafd9bf2077a98db462fda
元コミット内容
net/http: close TCP connection on Response.Body.Close
以前のHTTPクライアントの (*Response).Body.Close
は、キープアライブHTTPコネクションを再利用するために、EOF(End Of File)が来るまで読み続けようとしていました。しかし、EOFが全く来ないか、来るまでに非常に長い時間がかかる可能性がありました。この変更により、EOFがまだ来ていない場合は、TCPコネクションを即座に閉じます。
この変更は、TCPコネクションの再利用の恩恵を受けたいクライアントに対して、レスポンスボディ全体を読み切る責任を負わせるものです。
将来的には、強制的に閉じる前に、一定量のバイトを最大時間読み取るためのヒューリスティックを導入することも考えられますが、現時点では行いません。
統計的に、このコードに手を加えると問題が発生しやすい傾向があるため、新しいバグが導入されても驚きませんが、全てのテストはパスしており、コードも以前よりシンプルになったと考えています。おそらく。
Go 1.1リリース前に、HTTPクライアントコードをテストしてください。
Fixes #3672
変更の背景
このコミットの背景には、Goの net/http
クライアントがHTTPレスポンスボディを処理する際の、キープアライブコネクションの管理における課題がありました。
従来の Response.Body.Close()
の実装では、HTTP/1.1のキープアライブ機能を利用してTCPコネクションを再利用するために、レスポンスボディの残りをEOFまで読み切ろうとする挙動がありました。これは、コネクションプールにコネクションを戻す前に、そのコネクションが完全にクリーンな状態であることを保証し、次のリクエストで予期せぬデータが読み込まれることを防ぐための一般的なプラクティスです。
しかし、このアプローチにはいくつかの問題がありました。
- EOFが来ない可能性: サーバーがレスポンスボディの送信を途中で停止したり、ネットワークの問題が発生したりした場合、クライアントは永遠にEOFを待ち続ける可能性がありました。これは、リソースリークやアプリケーションのハングアップにつながる可能性があります。
- EOFまでの時間が長すぎる可能性: 非常に大きなレスポンスボディの場合、クライアントがボディ全体を読み切るのに時間がかかりすぎることがありました。クライアントがボディの途中で処理を打ち切り、
Close()
を呼び出した場合でも、裏側でEOFまで読み続ける処理がブロックされ、コネクションの解放が遅れる原因となっていました。 - リソースの無駄: クライアントがレスポンスボディの全てを必要としない場合でも、キープアライブのために不要なデータを読み続ける必要がありました。これは帯域幅の無駄遣いにもなり得ます。
これらの問題は、特にストリーミングデータや、クライアントがレスポンスボディの一部だけを必要とするようなシナリオで顕著でした。Issue #3672("Client can't close HTTP stream")は、まさにこの問題点を指摘しており、クライアントがレスポンスボディの読み取りを途中で中断し、コネクションを閉じたい場合に、それが適切に行えないという状況を報告していました。
このコミットは、これらの問題を解決し、クライアントが Response.Body.Close()
を呼び出した際に、より予測可能で即時的なリソース解放を可能にすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
HTTP/1.1 Keep-Alive (持続的接続):
- HTTP/1.0では、各リクエスト/レスポンスのペアごとに新しいTCPコネクションが確立され、閉じられていました。これはオーバーヘッドが大きく、特に多数の小さなリクエストを送信する場合に非効率でした。
- HTTP/1.1では、デフォルトで「Keep-Alive」が有効になっています。これにより、一つのTCPコネクション上で複数のHTTPリクエスト/レスポンスのやり取りが可能になります。コネクションの再利用は、TCPハンドシェイクやTLSハンドシェイクのオーバーヘッドを削減し、ネットワークの混雑を緩和し、全体的なパフォーマンスを向上させます。
- コネクションを再利用するためには、前のレスポンスボディが完全に読み込まれ、コネクションがクリーンな状態になっている必要があります。
-
io.Reader
とio.Closer
インターフェース:- Go言語の
io.Reader
インターフェースは、データを読み取るためのRead(p []byte) (n int, err error)
メソッドを定義します。 io.Closer
インターフェースは、リソースを閉じるためのClose() error
メソッドを定義します。Response.Body
はio.ReadCloser
インターフェース(io.Reader
とio.Closer
の両方を満たす)を実装しており、レスポンスボディの読み取りと、その基盤となるリソース(通常はTCPコネクション)のクローズを可能にします。
- Go言語の
-
EOF (End Of File):
- データストリームの終端を示す概念です。ファイル読み取りやネットワークストリームにおいて、これ以上データがないことを示すために使用されます。Goの
io.Reader
のRead
メソッドは、EOFに達すると(0, io.EOF)
を返します。
- データストリームの終端を示す概念です。ファイル読み取りやネットワークストリームにおいて、これ以上データがないことを示すために使用されます。Goの
-
TCPコネクション:
- インターネットプロトコルスイートの一部であり、信頼性の高い、コネクション指向のデータ転送サービスを提供します。HTTP通信の基盤となります。
-
Goの
net/http
パッケージ:- Go言語の標準ライブラリに含まれるパッケージで、HTTPクライアントとサーバーの実装を提供します。
http.Client
: HTTPリクエストを送信し、レスポンスを受信するクライアント。http.Response
: HTTPレスポンスを表す構造体。そのBody
フィールドはio.ReadCloser
です。http.Transport
:http.Client
の基盤となる実装で、実際のネットワークI/O、コネクションプーリング、プロキシ設定などを扱います。
-
bodyEOFSignal
構造体:net/http
パッケージ内部で使用されるラッパー構造体で、Response.Body
の読み取りがEOFに達したか、またはClose
が呼び出されたときに特定のコールバック関数 (fn
) を実行するために使用されます。これにより、レスポンスボディのライフサイクルイベントをフックできます。
これらの概念を理解することで、このコミットがなぜ必要とされ、どのように機能するのかを深く把握することができます。特に、キープアライブの利点と、そのためにレスポンスボディを完全に読み切る必要性、そしてその読み切りがうまくいかない場合の課題が、この変更の核心にあります。
技術的詳細
このコミットの技術的な核心は、net/http
パッケージの Transport
構造体内の persistConn
(持続的コネクション)の管理ロジックと、bodyEOFSignal
の挙動の変更にあります。
変更の目的と新しい挙動
- 目的:
Response.Body.Close()
が呼び出された際に、レスポンスボディが完全に読み込まれていない場合でも、基盤となるTCPコネクションを即座に閉じることで、リソースの早期解放とデッドロックの回避を実現します。 - 新しい挙動:
- クライアントが
resp.Body.Close()
を呼び出し、かつレスポンスボディがEOFまで読み込まれていない場合、TCPコネクションは即座に閉じられます。 - これにより、そのTCPコネクションは再利用されず、コネクションプールに戻されることもありません。
- コネクションの再利用(キープアライブ)の恩恵を受けたい場合は、クライアントは
resp.Body
を完全に読み切る必要があります。
- クライアントが
transport.go
の変更点
-
persistConn.readLoop()
の変更:persistConn
は、単一のTCPコネクション上で複数のHTTPリクエスト/レスポンスを処理するロジックをカプセル化しています。readLoop()
は、このコネクションからのレスポンスを継続的に読み取るゴルーチンです。lastbody
変数の削除: 以前は、前のレスポンスボディが完全に読み込まれていない場合にlastbody.Close()
を呼び出すロジックがありましたが、これが削除されました。これは、新しいアプローチではResponse.Body.Close()
が直接コネクションを閉じるため、persistConn
側でボディの読み込み完了を待つ必要がなくなったためです。bodyEOFSignal
のearlyCloseFn
の導入:resp.Body.(*bodyEOFSignal).earlyCloseFn
という新しいコールバック関数が設定されるようになりました。- この関数は、
bodyEOFSignal
のClose()
メソッドが呼び出され、かつio.EOF
がまだ見られていない(つまり、ボディが完全に読み込まれていない)場合に実行されます。 earlyCloseFn
はwaitForBodyRead <- false
をチャネルに送信します。これはreadLoop
ゴルーチンに、現在のコネクションをalive = false
に設定し、ループを終了してコネクションを閉じるように指示します。
-
bodyEOFSignal
構造体の変更:earlyCloseFn func() error
という新しいフィールドが追加されました。これは、EOFに達する前にClose()
が呼び出された場合に実行されるオプションの関数です。Close()
メソッドのロジックが変更されました。es.closed
がtrue
でないことを確認した後、es.earlyCloseFn != nil && es.rerr != io.EOF
の条件がチェックされます。- この条件が真(つまり、
earlyCloseFn
が設定されており、かつEOFに達していない)の場合、es.earlyCloseFn()
が呼び出され、その戻り値がClose()
の戻り値となります。 - それ以外の場合(EOFに達しているか、
earlyCloseFn
が設定されていない場合)、従来のes.body.Close()
が呼び出され、es.condfn(err)
が実行されます。
response_test.go
および sniff_test.go
の変更点
readFirstCloseBoth
構造体とdiscardOnCloseReadCloser
構造体が削除され、より汎用的なreaderAndCloser
構造体が導入されました。これは、io.Reader
とio.Closer
を組み合わせるためのシンプルなラッパーです。以前の構造体は、ボディの読み込み完了を待つという古いロジックに関連していたため、不要になりました。
transport_test.go
の変更点
TestTransportCloseResponseBody
の追加:- この新しいテストケースは、コミットの意図を直接検証するために追加されました。
- サーバーは無限にデータを書き込み続けるストリーミングサーバーとして機能します。
- クライアントはレスポンスボディの一部(
len(msg)*repeats
バイト)だけを読み取ります。 - その後、
res.Body.Close()
を呼び出します。 - テストは、
res.Body.Close()
が呼び出された後に、サーバー側で書き込みエラー(パイプが閉じられたことによるエラー)が発生することを検証します。これは、クライアントがClose()
を呼び出したことでTCPコネクションが即座に閉じられ、サーバーへの書き込みが失敗したことを示します。 - これにより、
Response.Body.Close()
が実際にTCPコネクションを閉じるという新しい挙動が確認されます。
まとめ
この変更は、net/http
クライアントの Response.Body.Close()
のセマンティクスを根本的に変更するものです。以前は「ボディを読み切ってコネクションを再利用しようとする」という挙動でしたが、新しい挙動は「ボディが読み切られていなければコネクションを即座に閉じる」というものです。これにより、クライアントはより明示的にコネクションのライフサイクルを制御できるようになり、リソースリークやハングアップのリスクが低減されます。ただし、キープアライブの恩恵を受けたい場合は、レスポンスボディを完全に読み切る責任がクライアント側に移転します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/net/http/transport.go
ファイルに集中しています。
-
src/pkg/net/http/transport.go
:persistConn.readLoop()
関数内のロジック変更。lastbody io.ReadCloser
変数の削除。resp.Body.(*bodyEOFSignal).earlyCloseFn
の設定ロジックの追加。
bodyEOFSignal
構造体へのearlyCloseFn func() error
フィールドの追加。bodyEOFSignal.Close()
メソッドのロジック変更。earlyCloseFn
が設定されており、かつEOFに達していない場合にearlyCloseFn
を呼び出す条件分岐の追加。io/ioutil
のインポート削除(ioutil.Discard
が不要になったため)。readFirstCloseBoth
およびdiscardOnCloseReadCloser
構造体の削除。readerAndCloser
構造体の追加。
-
src/pkg/net/http/response_test.go
:TestReadResponseCloseInMiddle
関数内で、readFirstCloseBoth
の代わりにreaderAndCloser
を使用するように変更。
-
src/pkg/net/http/sniff_test.go
:TestContentTypeWithCopy
およびTestSniffWriteSize
関数内で、res.Body.Close()
の前にio.Copy(ioutil.Discard, res.Body)
を呼び出すロジックが追加されました。これは、新しいClose
の挙動を考慮し、テストが意図した通りに動作するように、明示的にボディを読み捨てることでEOFに達させるための変更です。
-
src/pkg/net/http/transport_test.go
:TestTransportCloseResponseBody
という新しいテストケースの追加。これは、Response.Body.Close()
が実際にTCPコネクションを閉じることを検証するためのものです。
これらの変更は、HTTPクライアントのコネクション管理とレスポンスボディのライフサイクルに直接影響を与え、Go 1.1における net/http
パッケージの重要な挙動変更を構成しています。
コアとなるコードの解説
このコミットの核心は、net/http/transport.go
内の bodyEOFSignal
構造体とその Close
メソッド、そして persistConn.readLoop
の変更にあります。
bodyEOFSignal
構造体と Close
メソッドの変更
type bodyEOFSignal struct {
body io.ReadCloser
mu sync.Mutex // guards following 4 fields
closed bool // whether Close has been called
rerr error // sticky Read error
fn func(error) // error will be nil on Read io.EOF
earlyCloseFn func() error // optional alt Close func used if io.EOF not seen
}
func (es *bodyEOFSignal) Close() error {
es.mu.Lock()
defer es.mu.Unlock()
if es.closed {
return nil
}
es.closed = true
// ★ここが新しい変更点★
if es.earlyCloseFn != nil && es.rerr != io.EOF {
return es.earlyCloseFn()
}
err := es.body.Close()
es.condfn(err)
return err
}
-
earlyCloseFn func() error
フィールドの追加:- この新しいフィールドは、
bodyEOFSignal
がラップしているio.ReadCloser
のRead
メソッドがio.EOF
を返す前にClose()
が呼び出された場合に実行される、オプションのコールバック関数です。 - この関数が設定されている場合、通常の
es.body.Close()
の代わりにこの関数が実行され、その戻り値がbodyEOFSignal.Close()
の戻り値となります。
- この新しいフィールドは、
-
Close()
メソッドのロジック変更:es.closed = true
の設定後、新しい条件if es.earlyCloseFn != nil && es.rerr != io.EOF
が評価されます。es.rerr
は、Read
メソッドが最後に返したエラーを保持します。io.EOF
でないということは、ボディの読み取りが完了していないことを意味します。- この条件が真の場合、つまり
earlyCloseFn
が設定されており、かつボディが完全に読み込まれていない状態でClose()
が呼び出された場合、es.earlyCloseFn()
が実行されます。 - これにより、クライアントがボディを途中で閉じても、
earlyCloseFn
を通じて基盤となるTCPコネクションを即座に閉じることが可能になります。
persistConn.readLoop()
の変更
persistConn.readLoop()
は、HTTPコネクションからのレスポンスを読み取り、それを処理するゴルーチンです。この関数内で、bodyEOFSignal
の earlyCloseFn
が設定されるようになりました。
func (pc *persistConn) readLoop() {
defer close(pc.closech)
alive := true
for alive {
// ... (リクエストの受信とレスポンスの読み取り) ...
var waitForBodyRead chan bool
if hasBody {
waitForBodyRead = make(chan bool, 2) // バッファサイズが1から2に変更
// ★ここが新しい変更点★
resp.Body.(*bodyEOFSignal).earlyCloseFn = func() error {
// Sending false here sets alive to
// false and closes the connection
// below.
waitForBodyRead <- false // falseを送信してコネクションを閉じる
return nil
}
resp.Body.(*bodyEOFSignal).fn = func(err error) {
alive1 := alive
if err != nil {
// ... (エラー処理) ...
}
waitForBodyRead <- alive1 // 読み取り完了時にaliveの状態を送信
}
}
// ... (レスポンスの送信) ...
if hasBody {
alive = <-waitForBodyRead // ボディの読み取り完了またはearlyCloseFnからのシグナルを待つ
} else {
// ... (ボディがない場合の処理) ...
}
}
// ... (コネクションのクローズ) ...
}
earlyCloseFn
の設定:- レスポンスにボディがある場合 (
hasBody
がtrue
)、resp.Body
が*bodyEOFSignal
に型アサートされ、そのearlyCloseFn
フィールドに匿名関数が設定されます。 - この匿名関数は、
waitForBodyRead <- false
をチャネルに送信します。 readLoop
はalive = <-waitForBodyRead
でこのチャネルからの値を受け取ります。false
を受け取るとalive
がfalse
になり、ループが終了して基盤となるTCPコネクションが閉じられます。
- レスポンスにボディがある場合 (
動作のシーケンス
- HTTPクライアントがリクエストを送信し、レスポンスを受け取ります。
http.Transport
はレスポンスボディをbodyEOFSignal
でラップし、そのearlyCloseFn
を設定します。このearlyCloseFn
は、基盤となるpersistConn
にコネクションを閉じるようシグナルを送る役割を担います。- クライアントが
resp.Body.Close()
を呼び出します。 bodyEOFSignal.Close()
メソッドが実行されます。- もしレスポンスボディがまだ完全に読み込まれていない場合(
es.rerr != io.EOF
)、設定されたearlyCloseFn
が呼び出されます。 earlyCloseFn
はpersistConn.readLoop
にfalse
を送信し、readLoop
はalive
をfalse
に設定してループを終了します。readLoop
が終了すると、defer close(pc.closech)
が実行され、最終的にTCPコネクションが閉じられます。
このメカニズムにより、クライアントが Response.Body.Close()
を呼び出すと、ボディの読み取り状況に関わらず、TCPコネクションが即座に解放されるようになりました。これにより、リソースリークやハングアップのリスクが大幅に低減されます。
関連リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - HTTP/1.1 Persistent Connections (Keep-Alive): https://www.rfc-editor.org/rfc/rfc2616#section-8.1 (HTTP/1.1の古いRFCですが、Keep-Aliveの基本概念を理解するのに役立ちます)
参考にした情報源リンク
- Go Issue #3672: Client can't close HTTP stream: https://github.com/golang/go/issues/3672
- Go Change List 7419050: net/http: close TCP connection on Response.Body.Close: https://golang.org/cl/7419050 (このコミットの公式な変更リストページ)
- Go 1.1 Release Notes (net/http section): https://go.dev/doc/go1.1#net_http (Go 1.1のリリースノートでこの変更が言及されている可能性があります)
- Go言語のソースコード (net/httpパッケージ): https://github.com/golang/go/tree/master/src/net/http
- TCP/IPに関する一般的な情報源 (例: Wikipedia, RFCドキュメントなど)
- Go言語の並行処理に関する一般的な情報源 (チャネル、ゴルーチンなど)