[インデックス 18715] ファイルの概要
このコミットは、Goのnet/http
パッケージのClient
において、リダイレクト処理時に小さなレスポンスボディを消費することで、基盤となるTCP接続が再利用されるように修正するものです。Go 1.2でリクエストボディをEOFまで読み込まない場合にTCP接続が再利用されなくなった変更に対応していなかった問題が解決されています。
コミット
commit 76cc0a271286b7facfd5233d56737a3d92dd9670
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 3 11:25:57 2014 -0800
net/http: in Client, consume small redirect bodies before making next request
In Go 1.2, closing a request body without reading to EOF
causes the underlying TCP connection to not be reused. This
client code following redirects was never updated when that
happened.
This was part of a previous CL but moved to its own CL at
Josh's request. Now with test too.
LGTM=josharian
R=josharian
CC=golang-codereviews
https://golang.org/cl/70800043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/76cc0a271286b7facfd5233d56737a3d92dd9670
元コミット内容
net/http: Clientにおいて、次のリクエストを行う前に小さなリダイレクトボディを消費する
Go 1.2では、リクエストボディをEOFまで読み込まずにクローズすると、基盤となるTCP接続が再利用されなくなりました。リダイレクトを追跡するこのクライアントコードは、その変更時に更新されていませんでした。
これは以前のCLの一部でしたが、Joshの要求により独自のCLとして分離されました。テストも追加されました。
変更の背景
この変更の背景には、Go 1.2におけるnet/http
パッケージの挙動変更があります。Go 1.2より前は、HTTPレスポンスのボディを完全に読み込まなくても、TCP接続は再利用される可能性がありました。しかし、Go 1.2からは、パフォーマンスとリソース管理の最適化のため、レスポンスボディをEOF(End Of File)まで読み込まない限り、その基盤となるTCP接続は再利用されなくなりました。
この変更は、特にHTTPクライアントがリダイレクトを処理する際に問題を引き起こしました。リダイレクトが発生した場合、クライアントは通常、現在のレスポンスボディを破棄し、新しいリダイレクト先のURLに対して次のリクエストを発行します。Go 1.2の変更以前は、ボディを完全に読み込まなくても接続が再利用されたため問題ありませんでしたが、変更後は、ボディを読み込まないまま接続を閉じると、その接続は再利用されず、新しいTCP接続が確立されることになり、オーバーヘッドが増加し、パフォーマンスが低下する可能性がありました。
このコミットは、このGo 1.2の挙動変更に対応し、リダイレクト時に小さなレスポンスボディを明示的に消費することで、TCP接続の再利用を促進し、クライアントの効率を向上させることを目的としています。
前提知識の解説
-
HTTPリダイレクト (HTTP Redirect): HTTPリダイレクトは、ウェブサーバーがクライアント(ブラウザなど)に対して、要求されたリソースが別のURLに移動したことを伝えるメカニズムです。サーバーは3xx系のステータスコード(例: 301 Moved Permanently, 302 Found, 303 See Other, 307 Temporary Redirect, 308 Permanent Redirect)と
Location
ヘッダーを返します。クライアントはLocation
ヘッダーに示された新しいURLに自動的にリクエストを再発行します。 -
TCP接続の再利用 (TCP Connection Reuse): HTTP/1.1では、
Keep-Alive
メカニズムを通じてTCP接続の再利用がサポートされています。これは、一度確立されたTCP接続を複数のHTTPリクエスト/レスポンスのペアで使い回すことで、接続確立(スリーウェイハンドシェイク)と切断(フォーウェイハンドシェイク)のオーバーヘッドを削減し、ネットワークの遅延を減らし、サーバーのリソース消費を抑える効果があります。接続を再利用するためには、通常、前のレスポンスボディを完全に読み込む必要があります。 -
io.CopyN
: Goのio
パッケージにある関数で、指定されたバイト数(n
)だけsrc
からdst
へデータをコピーします。io.CopyN(dst, src, n int64)
の形式で使われます。 -
ioutil.Discard
: Goのio/ioutil
パッケージ(Go 1.16以降はio
パッケージに移動)にあるDiscard
は、io.Writer
インターフェースを実装した特殊な変数です。これに書き込まれたデータはすべて破棄されます。つまり、データを読み込むがどこにも保存しない、という用途で使われます。 -
resp.Body.Close()
: HTTPレスポンスボディはio.ReadCloser
インターフェースを実装しており、データを読み込むためのRead
メソッドと、リソースを解放するためのClose
メソッドを持っています。Close()
を呼び出すことは、基盤となるネットワーク接続を適切に管理するために重要です。しかし、Go 1.2以降では、Close()
を呼び出す前にボディをEOFまで読み込まないと、TCP接続が再利用されないという制約が加わりました。 -
resp.ContentLength
: HTTPレスポンスヘッダーのContent-Length
フィールドに対応する値です。レスポンスボディのバイト単位のサイズを示します。値が-1
の場合は、Content-Length
ヘッダーが存在しないか、チャンクエンコーディングなど別の方法でボディの長さが示されていることを意味します。
技術的詳細
このコミットが解決しようとしている技術的な問題は、Go 1.2で導入されたTCP接続再利用の挙動変更に起因します。具体的には、net/http
パッケージのClient
がリダイレクトを処理する際に、リダイレクト元のレスポンスボディを完全に読み込まないままresp.Body.Close()
を呼び出してしまうと、その接続が再利用されずに閉じられてしまうという問題です。これにより、リダイレクトのたびに新しいTCP接続が確立され、パフォーマンスの低下やリソースの無駄が発生していました。
解決策は、リダイレクトが発生する前に、リダイレクト元のレスポンスボディが小さい場合に限り、そのボディを明示的に消費することです。これにより、TCP接続が再利用可能な状態になり、効率的なリダイレクト処理が可能になります。
具体的には、以下のロジックが導入されました。
- リダイレクトのステータスコード(3xx)が検出された場合。
- レスポンスボディのサイズが不明(
ContentLength == -1
)であるか、またはmaxBodySlurpSize
(2KB)以下の小さいサイズである場合。 io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
を使って、最大maxBodySlurpSize
バイトまでレスポンスボディを読み込み、ioutil.Discard
に書き込むことでデータを破棄します。これにより、ボディが消費され、TCP接続が再利用可能な状態になります。エラーチェックは不要です。なぜなら、もし読み込みに失敗しても、Transportは接続を再利用しないため、問題ないからです。- その後、
resp.Body.Close()
が呼び出されます。
このアプローチにより、小さなボディを持つリダイレクトレスポンスの場合にのみボディを消費し、大きなボディを持つレスポンスの場合は、ボディの読み込みによるオーバーヘッドを避けることができます。これは、リダイレクト元のレスポンスボディが通常は非常に小さいという仮定に基づいています。
コアとなるコードの変更箇所
変更はsrc/pkg/net/http/client.go
ファイルとsrc/pkg/net/http/client_test.go
ファイルにあります。
src/pkg/net/http/client.go
--- a/src/pkg/net/http/client.go
+++ b/src/pkg/net/http/client.go
@@ -14,6 +14,7 @@ import (
"errors"
"fmt"
"io"
+ "io/ioutil"
"log"
"net/url"
"strings"
@@ -337,6 +338,12 @@ func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bo
}
if shouldRedirect(resp.StatusCode) {
+ // Read the body if small so underlying TCP connection will be re-used.
+ // No need to check for errors: if it fails, Transport won't reuse it anyway.
+ const maxBodySlurpSize = 2 << 10
+ if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
+ io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
+ }
resp.Body.Close()
if urlStr = resp.Header.Get("Location"); urlStr == "" {
err = errors.New(fmt.Sprintf("%d response missing Location header", resp.StatusCode))
src/pkg/net/http/client_test.go
テストケースTestClientRedirectEatsBody
が追加されています。
コアとなるコードの解説
src/pkg/net/http/client.go
のdoFollowingRedirects
関数は、HTTPクライアントがリダイレクトを処理する主要なロジックを含んでいます。
追加されたコードは以下の通りです。
// Read the body if small so underlying TCP connection will be re-used.
// No need to check for errors: if it fails, Transport won't reuse it anyway.
const maxBodySlurpSize = 2 << 10
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
}
-
const maxBodySlurpSize = 2 << 10
:maxBodySlurpSize
は、レスポンスボディを読み込む最大サイズを定義しています。2 << 10
は2の11乗、つまり2048バイト(2KB)を意味します。これは、リダイレクトレスポンスのボディが通常は非常に小さいという仮定に基づいています。 -
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize
: この条件文は、レスポンスボディを消費するかどうかを決定します。resp.ContentLength == -1
: レスポンスにContent-Length
ヘッダーがない場合(例: チャンクエンコーディングの場合など)。この場合、ボディのサイズが不明であるため、安全のために小さなボディとして扱います。resp.ContentLength <= maxBodySlurpSize
: レスポンスにContent-Length
ヘッダーがあり、その値がmaxBodySlurpSize
(2KB)以下である場合。
これらの条件のいずれかが真であれば、レスポンスボディは「小さい」と判断され、消費の対象となります。
-
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
: この行が実際にレスポンスボディを消費する部分です。ioutil.Discard
: 読み込んだデータを破棄するためのio.Writer
です。resp.Body
: リダイレクト元のHTTPレスポンスボディです。これはio.Reader
インターフェースを実装しています。maxBodySlurpSize
: 読み込む最大バイト数です。
この関数呼び出しにより、
resp.Body
から最大maxBodySlurpSize
バイトのデータが読み込まれ、ioutil.Discard
に書き込まれることで実質的に破棄されます。これにより、基盤となるTCP接続が再利用可能な状態になります。コメントにあるように、この操作でエラーが発生しても、Transportは接続を再利用しないため、エラーチェックは不要とされています。
この変更により、リダイレクト処理におけるTCP接続の再利用が改善され、net/http
クライアントのパフォーマンスと効率が向上します。
関連リンク
- Go CL 70800043: https://golang.org/cl/70800043
参考にした情報源リンク
- Go 1.2 Release Notes (net/http section): https://golang.org/doc/go1.2#net_http (このコミットの背景にあるGo 1.2の変更について言及されている可能性がありますが、直接的な言及は確認できませんでした。一般的なGoのリリースノートの構造から推測しています。)
io.CopyN
documentation: https://pkg.go.dev/io#CopyNio/ioutil.Discard
documentation (Go 1.16以降はio.Discard
): https://pkg.go.dev/io/ioutil#Discard- HTTP/1.1 Persistent Connections: https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html (HTTPのKeep-Aliveに関する一般的な情報源)