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

[インデックス 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接続が再利用可能な状態になり、効率的なリダイレクト処理が可能になります。

具体的には、以下のロジックが導入されました。

  1. リダイレクトのステータスコード(3xx)が検出された場合。
  2. レスポンスボディのサイズが不明(ContentLength == -1)であるか、またはmaxBodySlurpSize(2KB)以下の小さいサイズである場合。
  3. io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)を使って、最大maxBodySlurpSizeバイトまでレスポンスボディを読み込み、ioutil.Discardに書き込むことでデータを破棄します。これにより、ボディが消費され、TCP接続が再利用可能な状態になります。エラーチェックは不要です。なぜなら、もし読み込みに失敗しても、Transportは接続を再利用しないため、問題ないからです。
  4. その後、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.godoFollowingRedirects関数は、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)
			}
  1. const maxBodySlurpSize = 2 << 10: maxBodySlurpSizeは、レスポンスボディを読み込む最大サイズを定義しています。2 << 10は2の11乗、つまり2048バイト(2KB)を意味します。これは、リダイレクトレスポンスのボディが通常は非常に小さいという仮定に基づいています。

  2. if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize: この条件文は、レスポンスボディを消費するかどうかを決定します。

    • resp.ContentLength == -1: レスポンスにContent-Lengthヘッダーがない場合(例: チャンクエンコーディングの場合など)。この場合、ボディのサイズが不明であるため、安全のために小さなボディとして扱います。
    • resp.ContentLength <= maxBodySlurpSize: レスポンスにContent-Lengthヘッダーがあり、その値がmaxBodySlurpSize(2KB)以下である場合。

    これらの条件のいずれかが真であれば、レスポンスボディは「小さい」と判断され、消費の対象となります。

  3. 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クライアントのパフォーマンスと効率が向上します。

関連リンク

参考にした情報源リンク