[インデックス 16071] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるパフォーマンス改善を目的としています。具体的には、HTTP GETリクエストにおいて、ボディが0バイトである場合に io.LimitedReader を不必要に割り当てることを避け、既存の eofReader グローバル変数を利用することで、メモリ割り当てと処理時間を削減しています。
コミット
commit 468851f1d57eb5cd3ec0ec6d3ce306ea5749090b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Apr 3 10:31:12 2013 -0700
net/http: don't allocate 0-byte io.LimitedReaders for GET requests
Save an allocation per GET request and don't call io.LimitedReader(r, 0)
just to read 0 bytes. There's already an eofReader global variable
for when we just want a non-nil io.Reader to immediately EOF.
(Sorry, I know Rob told me to stop, but I was bored on the plane and
wrote this before I received the recent "please, really stop" email.)
benchmark old ns/op new ns/op delta
BenchmarkServerHandlerTypeLen 13888 13279 -4.39%
BenchmarkServerHandlerNoLen 12912 12229 -5.29%
BenchmarkServerHandlerNoType 13348 12632 -5.36%
BenchmarkServerHandlerNoHeader 10911 10261 -5.96%
benchmark old allocs new allocs delta
BenchmarkServerHandlerTypeLen 20 19 -5.00%
BenchmarkServerHandlerNoLen 18 17 -5.56%
BenchmarkServerHandlerNoType 18 17 -5.56%
BenchmarkServerHandlerNoHeader 13 12 -7.69%
benchmark old bytes new bytes delta
BenchmarkServerHandlerTypeLen 1913 1878 -1.83%
BenchmarkServerHandlerNoLen 1878 1843 -1.86%
BenchmarkServerHandlerNoType 1878 1844 -1.81%
BenchmarkServerHandlerNoHeader 1085 1051 -3.13%
Fixes #5188
R=golang-dev, adg, r
CC=golang-dev
https://golang.org/cl/8297044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/468851f1d57eb5cd3ec0ec6d3ce306ea5749090b
元コミット内容
このコミットは、net/http パッケージにおいて、GETリクエストに対する0バイトの io.LimitedReader の割り当てを停止することを目的としています。これにより、GETリクエストごとの割り当てを1つ削減し、0バイトを読み取るためだけに io.LimitedReader(r, 0) を呼び出すことを避けます。代わりに、すぐにEOF(End Of File)を返す非nilの io.Reader が必要な場合に利用できる eofReader グローバル変数が既に存在します。
コミットメッセージには、ベンチマーク結果も含まれており、ns/op (操作あたりのナノ秒)、allocs (割り当て数)、bytes (割り当てバイト数) の各指標で改善が見られることが示されています。
BenchmarkServerHandlerTypeLen: -4.39% (ns/op), -5.00% (allocs), -1.83% (bytes)BenchmarkServerHandlerNoLen: -5.29% (ns/op), -5.56% (allocs), -1.86% (bytes)BenchmarkServerHandlerNoType: -5.36% (ns/op), -5.56% (allocs), -1.81% (bytes)BenchmarkServerHandlerNoHeader: -5.96% (ns/op), -7.69% (allocs), -3.13% (bytes)
この変更は、Issue #5188 を修正するものです。
変更の背景
この変更の背景には、Go言語の net/http パッケージにおけるHTTPリクエストボディの処理効率の改善があります。特に、HTTP GETリクエストのように通常ボディを持たないリクエストや、Content-Length: 0 が指定されたリクエストにおいて、不必要なメモリ割り当てが発生しているという問題がありました。
Goの io.LimitedReader は、指定されたバイト数だけを読み取る io.Reader のラッパーです。HTTP/1.1の仕様では、GETリクエストはメッセージボディを持つべきではありませんが、一部のクライアントやプロキシが誤ってボディを送信する可能性も考慮し、サーバー側ではボディの存在を処理できる必要があります。しかし、ボディが0バイトであることが明確な場合でも、以前の実装では io.LimitedReader(r, 0) のように io.LimitedReader のインスタンスを生成していました。
io.LimitedReader(r, 0) は、確かに0バイトを読み取るとすぐにEOFを返しますが、このオブジェクト自体を生成する際にメモリ割り当てが発生します。HTTPサーバーは大量のリクエストを処理するため、このような小さな割り当てであっても、リクエストごとに発生すると全体としてパフォーマンスに影響を与え、ガベージコレクションの頻度を増加させる可能性があります。
Issue #5188 は、この特定のパフォーマンス問題、すなわち0バイトのボディに対して io.LimitedReader が割り当てられることによるオーバーヘッドを指摘していました。このコミットは、この問題を解決し、サーバーの効率を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とHTTPの基礎知識が必要です。
-
io.Readerインターフェース: Go言語における基本的なI/O操作のインターフェースです。Read([]byte) (n int, err error)メソッドを持ち、データを読み取るための抽象化を提供します。ファイル、ネットワーク接続、メモリ上のバッファなど、様々なデータソースからデータを読み取る際に利用されます。 -
io.LimitedReader:io.LimitedReaderは、既存のio.Readerをラップし、指定されたバイト数(N)までしか読み取らないように制限する構造体です。Readメソッドが呼び出されるたびに、内部のNが減少し、Nが0になるとそれ以上読み取らずにio.EOFを返します。io.LimitedReader(r, 0)の場合、Nが最初から0であるため、Readが呼び出されるとすぐにio.EOFを返します。 -
io.EOF(End Of File):io.ReaderのReadメソッドが、それ以上読み取るデータがないことを示すために返すエラーです。 -
eofReader: Goのnet/httpパッケージ内部で定義されている、すぐにio.EOFを返す特別なio.Readerのインスタンスです。これは、0バイトのボディを表現するために、新しいio.LimitedReaderを毎回割り当てる代わりに再利用できるシングルトン(またはそれに近い)オブジェクトとして機能します。これにより、不要なメモリ割り当てを避けることができます。 -
HTTP GET リクエスト: HTTPメソッドの一つで、指定されたリソースの表現を要求するために使用されます。GETリクエストは、通常、リクエストボディを持ちません。HTTP/1.1の仕様では、GETリクエストのメッセージボディは意味を持たないとされています。
-
メモリ割り当てとガベージコレクション (GC): Go言語はガベージコレクタを持つ言語です。プログラムが新しいオブジェクトを作成するたびに、メモリが割り当てられます。これらのオブジェクトが不要になった場合、ガベージコレクタがそれらを検出し、メモリを解放します。メモリ割り当ての頻度が高いと、ガベージコレクタがより頻繁に実行され、プログラムの実行が一時的に停止(ストップ・ザ・ワールド)する時間が長くなり、全体的なパフォーマンスが低下する可能性があります。不要なメモリ割り当てを削減することは、GCの負荷を軽減し、アプリケーションのスループットを向上させる上で重要です。
-
bufio.Reader: バッファリングされたI/Oを提供するio.Readerの実装です。これにより、基になるio.Readerからの読み取り回数を減らし、パフォーマンスを向上させることができます。 -
ioutil.Discard:io.Writerの一種で、書き込まれたすべてのデータを破棄します。io.Copy(ioutil.Discard, reader)のように使用すると、readerからのデータをすべて読み捨て、EOFに到達するまで消費するために使われます。これは、HTTPリクエストボディを完全に読み切る必要があるが、その内容には興味がない場合に特に有用です。
技術的詳細
このコミットの技術的な核心は、HTTPリクエストのボディ処理における最適化です。
以前の実装では、HTTPリクエストのボディの長さが0である場合(例えば、GETリクエストや Content-Length: 0 のヘッダを持つリクエスト)、io.LimitedReader(r, 0) を使用してボディを表現していました。
io.LimitedReader は、以下のような構造を持っています(簡略化された概念):
type LimitedReader struct {
R io.Reader // underlying reader
N int64 // max bytes remaining
}
func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, io.EOF
}
// ... (実際の読み取りロジック)
}
io.LimitedReader(r, 0) を呼び出すと、LimitedReader の新しいインスタンスがヒープに割り当てられ、その N フィールドが0に設定されます。このインスタンスは、Read メソッドが呼び出されるとすぐに io.EOF を返します。機能的には正しいですが、この LimitedReader オブジェクト自体を生成するためのメモリ割り当てが、リクエストごとに発生していました。
Goの net/http パッケージには、既に eofReader というグローバル変数(またはパッケージレベルの変数)が存在します。これは、Read メソッドが常に (0, io.EOF) を返すように実装された io.Reader のインスタンスです。
// eofReader is an io.Reader that always returns 0, io.EOF.
var eofReader = &eofReaderT{}
type eofReaderT struct{}
func (eofReaderT) Read(b []byte) (n int, err error) { return 0, io.EOF }
この eofReader は一度だけ初期化され、その後は何度でも再利用できます。したがって、0バイトのボディを表現するために io.LimitedReader(r, 0) を毎回新しく割り当てる代わりに、既存の eofReader を参照するだけで済みます。これにより、新しいオブジェクトの割り当てが不要になり、ヒープの使用量が削減され、ガベージコレクションの頻度が低下し、結果として全体的なパフォーマンスが向上します。
コミットメッセージに示されているベンチマーク結果は、この最適化が実際に効果をもたらしたことを明確に示しています。特に allocs (割り当て数) の削減は、この変更の直接的な効果を反映しています。
また、body.Close() メソッドの変更も重要です。以前は、b.Reader == eofReader の場合でも io.Copy(ioutil.Discard, b) を呼び出していました。io.Copy は、たとえリーダーがすぐにEOFを返すとしても、関数呼び出しのオーバーヘッドや、場合によっては内部的なバッファリング処理などが発生する可能性があります。新しいコードでは、b.Reader == eofReader の場合は io.Copy をスキップすることで、この不要な処理をさらに削減しています。これにより、Close メソッドの実行効率も向上しています。
コアとなるコードの変更箇所
変更は src/pkg/net/http/transfer.go ファイルで行われています。
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -328,12 +328,13 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {\
switch {
case chunked(t.TransferEncoding):
if noBodyExpected(t.RequestMethod) {
- t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}
+ t.Body = &body{Reader: eofReader, closing: t.Close}
} else {
t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
}
- case realLength >= 0:
- // TODO: limit the Content-Length. This is an easy DoS vector.
+ case realLength == 0:
+ t.Body = &body{Reader: eofReader, closing: t.Close}
+ case realLength > 0:
t.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}
default:
// realLength < 0, i.e. "Content-Length" not mentioned in header
@@ -342,7 +343,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {\
if t.Close {
t.Body = &body{Reader: r, closing: t.Close}
} else {
// Persistent connection (i.e. HTTP/1.1)
- t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}
+ t.Body = &body{Reader: eofReader, closing: t.Close}
}
}
@@ -612,30 +613,26 @@ func (b *body) Close() error {\
if b.closed {
return nil
}
- defer func() {\
- b.closed = true
- }()
- if b.hdr == nil && b.closing {
- // no trailer and closing the connection next.
- // no point in reading to EOF.
- return nil
- }
-
- // In a server request, don't continue reading from the client
- // if we've already hit the maximum body size set by the
- // handler. If this is set, that also means the TCP connection
- // is about to be closed, so getting to the next HTTP request
- // in the stream is not necessary.
- if b.res != nil && b.res.requestBodyLimitHit {
- return nil
- }
-
- // Fully consume the body, which will also lead to us reading
- // the trailer headers after the body, if present.
- if _, err := io.Copy(ioutil.Discard, b); err != nil {
- return err
+ var err error
+ switch {
+ case b.hdr == nil && b.closing:
+ // no trailer and closing the connection next.
+ // no point in reading to EOF.
+ case b.res != nil && b.res.requestBodyLimitHit:
+ // In a server request, don't continue reading from the client
+ // if we've already hit the maximum body size set by the
+ // handler. If this is set, that also means the TCP connection
+ // is about to be closed, so getting to the next HTTP request
+ // in the stream is not necessary.
+ case b.Reader == eofReader:
+ // Nothing to read. No need to io.Copy from it.
+ default:
+ // Fully consume the body, which will also lead to us reading
+ // the trailer headers after the body, if present.
+ _, err = io.Copy(ioutil.Discard, b)
}
- return nil
+ b.closed = true
+ return err
}
// parseContentLength trims whitespace from s and returns -1 if no value
コアとなるコードの解説
このコミットは、主に readTransfer 関数と body.Close メソッドの2箇所を変更しています。
readTransfer 関数内の変更
readTransfer 関数は、HTTPリクエストまたはレスポンスのボディを読み取るためのロジックをカプセル化しています。
-
noBodyExpected(t.RequestMethod)のケース: これは、GETリクエストのようにボディが期待されない場合に該当します。- 変更前:
t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}io.LimitReader(r, 0)を使用して、0バイトを読み取るリーダーを生成していました。これにより、新しいLimitedReaderオブジェクトがヒープに割り当てられていました。 - 変更後:
t.Body = &body{Reader: eofReader, closing: t.Close}既存のeofReaderグローバル変数を使用するように変更されました。eofReaderは常にEOFを返すため、機能的にはio.LimitReader(r, 0)と同じですが、新しいオブジェクトの割り当てが不要になります。
- 変更前:
-
realLengthの処理:Content-Lengthヘッダが存在する場合の処理です。- 変更前:
case realLength >= 0:のブロックで、io.LimitReader(r, realLength)を使用していました。 このロジックは、realLengthが0の場合も含まれていました。 - 変更後:
case realLength == 0:を新しく追加し、この場合はt.Body = &body{Reader: eofReader, closing: t.Close}を使用するようにしました。これにより、Content-Length: 0の場合もeofReaderが利用され、不要な割り当てが削減されます。case realLength > 0:を追加し、realLengthが正の値の場合のみio.LimitReader(r, realLength)を使用するようにしました。
- 変更前:
-
永続接続 (Persistent connection) のケース:
Content-Lengthヘッダがなく、かつ接続が永続的(HTTP/1.1など)な場合の処理です。- 変更前:
t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}ここでもio.LimitReader(r, 0)が使用されていました。 - 変更後:
t.Body = &body{Reader: eofReader, closing: t.Close}同様にeofReaderを使用するように変更され、割り当てが削減されました。
- 変更前:
これらの変更により、0バイトのボディを表現する際に、毎回新しい io.LimitedReader を割り当てる代わりに、既存の eofReader を再利用するようになり、メモリ割り当てが削減されます。
body.Close() メソッド内の変更
body.Close() メソッドは、HTTPリクエストボディの読み取りを完了し、必要に応じて残りのデータを消費するために呼び出されます。
-
変更前: 複数の
if文とdeferを使用していましたが、io.Copy(ioutil.Discard, b)の呼び出しは、b.Readerがio.LimitedReader(r, 0)であっても実行される可能性がありました。io.Copyは、たとえすぐにEOFを返すリーダーであっても、関数呼び出しのオーバーヘッドや、場合によっては内部的なバッファリング処理などが発生する可能性があります。 -
変更後:
switchステートメントを使用して、ボディを読み捨てる必要があるかどうかをより効率的に判断するように変更されました。 特に重要なのは、case b.Reader == eofReader:という新しい条件が追加された点です。この条件が真の場合、つまりボディがeofReaderで表現されている場合、io.Copy(ioutil.Discard, b)の呼び出しが完全にスキップされます。これは、eofReaderは既にEOFを返すと分かっているため、そこからデータを読み取ろうとするのは無駄だからです。 これにより、Closeメソッドが呼び出された際の不要な処理が削減され、パフォーマンスがさらに向上します。
全体として、このコミットは、HTTPリクエストボディの処理パスにおいて、0バイトのボディを扱う際の不要なメモリ割り当てとCPUサイクルを削減することで、net/http パッケージの効率を向上させています。
関連リンク
- Go Change-Id:
https://golang.org/cl/8297044 - Go Issue:
https://golang.org/issue/5188
参考にした情報源リンク
- Go言語の
ioパッケージのドキュメント (特にio.Reader,io.LimitedReader,io.EOF): https://pkg.go.dev/io - Go言語の
net/httpパッケージのドキュメント: https://pkg.go.dev/net/http - Go言語のガベージコレクションに関する一般的な情報
- HTTP/1.1 仕様 (RFC 2616 または後続のRFC): 特にGETメソッドとメッセージボディに関するセクション。
- Go言語のソースコード (特に
src/pkg/net/http/transfer.goおよびsrc/pkg/io/io.goの関連部分)