[インデックス 18377] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおけるチャンク転送エンコーディングの読み込み処理を改善し、HTTPクライアント接続の再利用効率を高めることを目的としています。具体的には、チャンクリーダーが可能な限り多くのデータを読み込み、特にチャンクの終端(EOF)を早期に検出することで、http.Transport
クライアント接続がより積極的に再利用されるように変更されています。
コミット
commit ff29be14c4c63912963c442109da56a98960ea2d
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jan 29 13:44:21 2014 +0100
net/http: read as much as possible (including EOF) during chunked reads
This is the chunked half of https://golang.org/cl/49570044 .
We want full reads to return EOF as early as possible, when we
know we're at the end, so http.Transport client connections are eagerly
re-used in the common case, even if no Read or Close follows.
To do this, make the chunkedReader.Read fill up its argument p []byte
buffer as much as possible, as long as that doesn't involve doing
any more blocking reads to read chunk headers. That means if we
have a chunk EOF ("0\r\n") sitting in the incoming bufio.Reader,
we see it and set EOF on our final Read.
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/58240043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ff29be14c4c63912963c442109da56a98960ea2d
元コミット内容
net/http: read as much as possible (including EOF) during chunked reads
このコミットは、チャンク読み込み中に可能な限り多くのデータ(EOFを含む)を読み込むように net/http
パッケージを変更します。これは、https://golang.org/cl/49570044
のチャンク部分の実装です。
完全な読み込みが可能な限り早くEOFを返すようにすることで、http.Transport
クライアント接続が、その後に Read
や Close
が続かない場合でも、一般的なケースで積極的に再利用されるようにします。
これを実現するために、chunkedReader.Read
は、チャンクヘッダーを読み込むためのブロッキング読み込みを伴わない限り、引数 p []byte
バッファを可能な限り埋めるようにします。これは、受信 bufio.Reader
にチャンクEOF("0\r\n")が存在する場合、それを検出し、最終的な Read
でEOFを設定することを意味します。
変更の背景
HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に「チャンク転送エンコーディング (Chunked Transfer Encoding)」が使用されます。これは、ボディを複数の「チャンク」に分割し、各チャンクの前にそのサイズを示すヘッダーを付加し、最後にサイズ0のチャンクで終了を示す方式です。
Goの net/http
パッケージは、HTTPクライアントとサーバーの実装を提供しており、クライアント側では http.Transport
が接続の管理と再利用を担当します。HTTP/1.1のKeep-Alive機能により、一度確立されたTCP接続は複数のリクエスト/レスポンスで再利用されることが期待されます。これにより、接続確立のオーバーヘッドを削減し、パフォーマンスを向上させることができます。
しかし、チャンク転送エンコーディングを使用している場合、レスポンスボディの読み込みが完了したかどうかを正確に判断することが重要です。もし、クライアントがレスポンスボディの最後まで読み込まないうちに接続を閉じたり、次のリクエストを送信しようとしたりすると、接続が適切に再利用されない可能性があります。特に、レスポンスボディを完全に読み込まない場合でも、接続を再利用するためには、チャンクの終端を示す「0\r\n」を確実に読み取り、ストリームの終わりを認識する必要があります。
このコミットの背景には、http.Transport
がクライアント接続をより積極的に再利用できるようにするという明確な目標があります。以前の実装では、チャンクの終端を早期に検出できない場合があり、その結果、接続の再利用が遅れたり、不必要に新しい接続が確立されたりする可能性がありました。特に、Read
や Close
が後続しない場合でも接続を再利用できるようにするためには、EOFを可能な限り早く返すことが求められました。
前提知識の解説
HTTP/1.1 チャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1のメッセージボディを転送するメカニズムの一つで、メッセージボディの長さを事前に決定できない場合(例:動的に生成されるコンテンツ)に利用されます。
- 形式: メッセージボディは一連のチャンクで構成されます。各チャンクは、そのチャンクのサイズ(16進数)と、それに続くデータで構成されます。サイズとデータの間にはCRLF(
\r\n
)が挿入されます。 - 終端: 最後のチャンクはサイズが0であり、その後にオプションのトレーラーヘッダーが続くことがあります。サイズ0のチャンクの後には、必ず2つのCRLF(
\r\n\r\n
)が続きます。この0\r\n\r\n
がメッセージボディの終端を示します。 - 利点:
- サーバーがレスポンスボディの全長を知る前に送信を開始できるため、動的なコンテンツ生成に適しています。
- Keep-Alive接続で、複数のレスポンスを効率的に送信できます。
Go net/http
パッケージ
Goの標準ライブラリに含まれるHTTPクライアントおよびサーバーの実装を提供するパッケージです。
http.Client
: HTTPリクエストを送信し、HTTPレスポンスを受信するクライアント。http.Transport
:http.Client
の内部で使用され、HTTP接続の確立、管理、再利用を担当します。Keep-Alive接続のプールを維持し、効率的な通信を実現します。io.Reader
インターフェース: データを読み込むための基本的なインターフェース。Read(p []byte) (n int, err error)
メソッドを持ち、n
は読み込んだバイト数、err
はエラー(EOFを含む)を示します。io.EOF
:io.Reader
のRead
メソッドが、これ以上読み込むデータがないことを示すために返す特別なエラーです。このエラーが返されると、読み込みストリームの終端に達したことを意味します。
bufio.Reader
bufio
パッケージは、I/O操作をバッファリングすることで効率を向上させる機能を提供します。bufio.Reader
は、基になる io.Reader
からデータを読み込み、内部バッファに格納します。これにより、小さな読み込み要求が多数発生しても、実際のシステムコールは少なくなり、パフォーマンスが向上します。
Buffered()
: 内部バッファに現在利用可能なバイト数を返します。Peek(n int)
: 次のn
バイトを読み込まずに覗き見します。バッファに十分なデータがない場合でも、ブロックせずに利用可能なデータを返します。
接続の再利用とEOFの重要性
HTTP/1.1のKeep-Alive接続では、クライアントがレスポンスボディの最後まで読み込み、サーバーが接続の終端を認識することで、そのTCP接続を次のリクエストのために再利用できます。もしクライアントがボディの途中で読み込みを停止すると、サーバーは接続がまだ使用中であると判断し、接続を閉じることができず、結果として接続プールが枯渇したり、新しい接続が不必要に確立されたりする可能性があります。
io.EOF
を早期に返すことは、クライアントがレスポンスボディの終端に達したことを迅速に認識し、接続を解放して再利用可能にするために不可欠です。特に、クライアントがレスポンスボディを完全に消費しない場合(例:ヘッダーのみを読み取り、ボディは無視する場合)でも、接続を再利用するためには、ストリームの終端を示す EOF
を受け取ることが重要になります。
技術的詳細
このコミットの主要な変更点は、net/http
および net/http/httputil
パッケージ内の chunkedReader
の Read
メソッドの動作です。以前の実装では、Read
メソッドは、要求されたバッファサイズ b
に応じてデータを読み込み、チャンクの終端に達した場合にのみ次のチャンクヘッダーの読み込みを試みていました。しかし、これにより、チャンクの終端を示す 0\r\n
が bufio.Reader
の内部バッファに存在していても、それがすぐに処理されず、io.EOF
が遅れて返される可能性がありました。
新しい実装では、以下の点が改善されています。
-
積極的な読み込みとEOFの早期検出:
chunkedReader.Read
は、ループ内でcr.err == nil
である限り、可能な限り多くのデータを読み込もうとします。cr.n == 0
(現在のチャンクのデータがすべて読み込まれた状態) になった場合、次のチャンクヘッダーを読み込む前に、chunkHeaderAvailable()
メソッドを呼び出して、bufio.Reader
の内部バッファに改行文字 (\n
) が含まれているかどうかを確認します。chunkHeaderAvailable()
がtrue
を返す(つまり、次のチャンクヘッダーがバッファに存在し、ブロッキングなしで読み取れる)場合、beginChunk()
を呼び出して次のチャンクの処理に進みます。chunkHeaderAvailable()
がfalse
を返す(つまり、次のチャンクヘッダーを読み込むにはブロッキングが必要)場合、またはlen(b) == 0
の場合、Read
ループをbreak
します。これにより、不必要なブロッキングを避けつつ、可能な限り多くのデータを読み込みます。- 特に重要なのは、
0\r\n
という終端チャンクがbufio.Reader
のバッファに存在する場合、chunkHeaderAvailable()
がtrue
を返し、beginChunk()
が呼び出されて終端チャンクが処理され、io.EOF
が早期に返されるようになった点です。
-
chunkHeaderAvailable()
メソッドの導入:- この新しいヘルパー関数は、
bufio.Reader
のBuffered()
とPeek()
メソッドを利用して、内部バッファに改行文字(\n
)が含まれているかどうかをチェックします。改行文字はチャンクヘッダーの終端を示すため、これが存在するということは、ブロッキングなしで次のチャンクヘッダーを読み取れる可能性が高いことを意味します。
- この新しいヘルパー関数は、
-
net/http/httputil
とのコード重複の解消:- 以前は
net/http/chunked.go
とnet/http/httputil/chunked.go
でチャンク関連のコードが重複していました。このコミットでは、httputil
パッケージにNewChunkedReader
とNewChunkedWriter
という公開関数を導入し、これらがnet/http
パッケージ内の非公開のnewChunkedReader
とnewChunkedWriter
を呼び出すように変更されました。これにより、コードの重複が解消され、メンテナンス性が向上しました。
- 以前は
これらの変更により、chunkedReader
は、たとえ Read
の呼び出しで要求されたバッファサイズが小さくても、内部バッファに存在するチャンクデータと、ブロッキングなしで読み取れる次のチャンクヘッダー(特に終端チャンク)を積極的に消費するようになりました。これにより、レスポンスボディの終端がより早く検出され、http.Transport
が接続を再利用できるタイミングが早まります。
テストケース TestChunkReadMultiple
の追加は、この新しい動作、特に小さなチャンクがまとめて読み込まれるケースや、bufio.Reader
のバッファサイズがチャンクヘッダーの読み込みに影響を与えるケース、そしてバッファがすでに満たされている場合でもEOFチャンクが検出されるケースを検証しています。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/pkg/net/http/chunked.go
と src/pkg/net/http/httputil/chunked.go
の chunkedReader
構造体の Read
メソッドと、新しく追加された chunkHeaderAvailable
メソッドにあります。
src/pkg/net/http/chunked.go
(および src/pkg/net/http/httputil/chunked.go
も同様)
--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -57,26 +58,45 @@ func (cr *chunkedReader) beginChunk() {
}
}
-func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
- if cr.err != nil {
- return 0, cr.err
+func (cr *chunkedReader) chunkHeaderAvailable() bool {
+ n := cr.r.Buffered()
+ if n > 0 {
+ peek, _ := cr.r.Peek(n)
+ return bytes.IndexByte(peek, '\n') >= 0
}
- if cr.n == 0 {
- cr.beginChunk()
- if cr.err != nil {
- return 0, cr.err
+ return false
+}
+
+func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
+ for cr.err == nil {
+ if cr.n == 0 {
+ if n > 0 && !cr.chunkHeaderAvailable() {
+ // We've read enough. Don't potentially block
+ // reading a new chunk header.
+ break
+ }
+ cr.beginChunk()
+ continue
}
- }
- if uint64(len(b)) > cr.n {
- b = b[0:cr.n]
- }
- n, cr.err = cr.r.Read(b)
- cr.n -= uint64(n)
- if cr.n == 0 && cr.err == nil {
- // end of chunk (CRLF)
- if _, cr.err = io.ReadFull(cr.r, cr.buf[:]); cr.err == nil {
- if cr.buf[0] != '\r' || cr.buf[1] != '\n' {
- cr.err = errors.New("malformed chunked encoding")
+ if len(b) == 0 {
+ break
+ }
+ rbuf := b
+ if uint64(len(rbuf)) > cr.n {
+ rbuf = rbuf[:cr.n]
+ }
+ var n0 int
+ n0, cr.err = cr.r.Read(rbuf)
+ n += n0
+ b = b[n0:]
+ cr.n -= uint64(n0)
+ // If we're at the end of a chunk, read the next two
+ // bytes to verify they are "\r\n".
+ if cr.n == 0 && cr.err == nil {
+ if _, cr.err = io.ReadFull(cr.r, cr.buf[:2]); cr.err == nil {
+ if cr.buf[0] != '\r' || cr.buf[1] != '\n' {
+ cr.err = errors.New("malformed chunked encoding")
+ }
}
}
}
src/pkg/net/http/httputil/httputil.go
(新規ファイル)
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package httputil provides HTTP utility functions, complementing the
// more common ones in the net/http package.
package httputil
import "io"
// NewChunkedReader returns a new chunkedReader that translates the data read from r
// out of HTTP "chunked" format before returning it.
// The chunkedReader returns io.EOF when the final 0-length chunk is read.
//
// NewChunkedReader is not needed by normal applications. The http package
// automatically decodes chunking when reading response bodies.
func NewChunkedReader(r io.Reader) io.Reader {
return newChunkedReader(r)
}
// NewChunkedWriter returns a new chunkedWriter that translates writes into HTTP
// "chunked" format before writing them to w. Closing the returned chunkedWriter
// sends the final 0-length chunk that marks the end of the stream.
//
// NewChunkedWriter is not needed by normal applications. The http
// package adds chunking automatically if handlers don't set a
// Content-Length header. Using NewChunkedWriter inside a handler
// would result in double chunking or chunking with a Content-Length
// length, both of which are wrong.
func NewChunkedWriter(w io.Writer) io.WriteCloser {
return newChunkedWriter(w)
}
コアとなるコードの解説
chunkedReader.Read
メソッドの変更
旧来の Read
メソッドは、現在のチャンクのデータが読み尽くされた (cr.n == 0
) 場合にのみ beginChunk()
を呼び出し、次のチャンクヘッダーの読み込みを試みていました。これは、Read
が要求されたバッファ b
を満たすとすぐに終了し、次のチャンクヘッダーが bufio.Reader
のバッファに存在していても、それが処理されない可能性がありました。
新しい Read
メソッドは、for cr.err == nil
ループ内で動作します。
-
cr.n == 0
(現在のチャンクのデータが読み尽くされた場合):if n > 0 && !cr.chunkHeaderAvailable()
: ここが重要な変更点です。n > 0
は、Read
メソッドに渡されたバッファb
にまだ書き込む余地があることを意味します。!cr.chunkHeaderAvailable()
は、bufio.Reader
の内部バッファに次のチャンクヘッダー(特に改行文字)がまだ存在しないことを意味します。つまり、次のチャンクヘッダーを読み込むにはブロッキングが必要になる可能性が高い状況です。- この条件が真の場合、
break
してループを終了します。これにより、不必要なブロッキング読み込みを避けつつ、現在利用可能なデータをすべてb
に書き込み、Read
を終了します。
cr.beginChunk()
: 上記の条件が偽の場合(つまり、次のチャンクヘッダーがブロッキングなしで読み取れる場合)、beginChunk()
を呼び出して次のチャンクヘッダーを読み込みます。これには、終端チャンク0\r\n
の検出も含まれます。continue
:beginChunk()
の処理後、ループの先頭に戻り、新しいチャンクデータ(またはEOF)の処理を続行します。
-
len(b) == 0
:Read
に空のバッファが渡された場合、すぐにループをbreak
します。 -
データ読み込み:
rbuf := b
: 読み込み先のバッファb
をrbuf
にコピーします。if uint64(len(rbuf)) > cr.n { rbuf = rbuf[:cr.n] }
:rbuf
のサイズを現在のチャンクの残りバイト数cr.n
に制限します。これにより、現在のチャンクの範囲を超えて読み込むことを防ぎます。n0, cr.err = cr.r.Read(rbuf)
: 基になるbufio.Reader
からデータを読み込みます。n += n0; b = b[n0:]; cr.n -= uint64(n0)
: 読み込んだバイト数を合計n
に加算し、b
とcr.n
を更新します。
-
チャンク終端のCRLF検証:
if cr.n == 0 && cr.err == nil
: 現在のチャンクのデータがすべて読み込まれ、エラーがない場合、チャンクの終端を示す\r\n
を読み込み、それが正しい形式であるか検証します。
chunkHeaderAvailable()
メソッド
この新しいメソッドは、bufio.Reader
の内部バッファに次のチャンクヘッダーの終端(改行文字 \n
)が存在するかどうかを非ブロッキングで確認するために導入されました。
n := cr.r.Buffered()
:bufio.Reader
の内部バッファに現在利用可能なバイト数を取得します。if n > 0
: バッファにデータが存在する場合のみ処理を続行します。peek, _ := cr.r.Peek(n)
: バッファ内のすべてのデータを読み込まずに覗き見します。return bytes.IndexByte(peek, '\n') >= 0
: 覗き見したデータの中に改行文字\n
が含まれているかどうかをチェックします。含まれていればtrue
を返し、次のチャンクヘッダーがブロッキングなしで読み取れる可能性が高いことを示します。
この変更により、chunkedReader
は、Read
メソッドが呼び出された際に、要求されたバイト数を満たすだけでなく、bufio.Reader
のバッファに存在する次のチャンクヘッダー(特に終端チャンク)も積極的に処理するようになりました。これにより、io.EOF
がより早く返され、http.Transport
が接続を再利用できる機会が増加します。
httputil
パッケージの変更
src/pkg/net/http/httputil/httputil.go
が新しく追加され、NewChunkedReader
と NewChunkedWriter
という公開関数が定義されました。これらは、net/http
パッケージ内の非公開の newChunkedReader
と newChunkedWriter
をラップしています。これにより、net/http
と net/http/httputil
間で重複していたチャンク関連のコードが、net/http
に集約され、httputil
はそのラッパーを提供する形になりました。これはコードの重複を避け、一貫性を保つためのリファクタリングです。
関連リンク
- Go Issue: net/http: read as much as possible (including EOF) during chunked reads (このコミットのコードレビューリンク)
- Go Issue: net/http: make Transport reuse connections more eagerly (このコミットが関連する、より広範な接続再利用の改善に関するコードレビューリンク)
- HTTP/1.1 RFC 2616 - 3.6.1 Chunked Transfer Encoding: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
参考にした情報源リンク
- Goのソースコード (上記コミットの差分)
- HTTP/1.1 RFC 2616
- Go
net/http
パッケージのドキュメント - Go
bufio
パッケージのドキュメント - Go
io
パッケージのドキュメント - Goのコードレビューシステム (Gerrit) の情報