[インデックス 16348] ファイルの概要
このコミットは、Go言語のnet/http
パッケージにおけるHTTPボディの転送処理を簡素化し、それに伴うメモリ割り当て(allocation)の削減とパフォーマンスの向上を目的としています。具体的には、eofReader
の実装を改善し、body
構造体から不要なフィールドを削除することで、HTTPリクエスト/レスポンスボディの読み込みロジックを効率化しています。
コミット
commit 27f7427995782bf60195ca65fd9b44aa34913b75
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon May 20 07:23:59 2013 -0700
net/http: simplify transfer body; reduces allocations too
benchmark old ns/op new ns/op delta
BenchmarkServerFakeConnNoKeepAlive 14431 14247 -1.28%
BenchmarkServerFakeConnWithKeepAlive 11618 11357 -2.25%
BenchmarkServerFakeConnWithKeepAliveLite 6735 6427 -4.57%
BenchmarkServerHandlerTypeLen 8842 8740 -1.15%
BenchmarkServerHandlerNoLen 8001 7828 -2.16%
BenchmarkServerHandlerNoType 8270 8227 -0.52%
BenchmarkServerHandlerNoHeader 6148 5920 -3.71%
benchmark old allocs new allocs delta
BenchmarkServerFakeConnNoKeepAlive 30 29 -3.33%
BenchmarkServerFakeConnWithKeepAlive 25 24 -4.00%
BenchmarkServerFakeConnWithKeepAliveLite 10 9 -10.00%
BenchmarkServerHandlerTypeLen 18 17 -5.56%
BenchmarkServerHandlerNoLen 15 14 -6.67%
BenchmarkServerHandlerNoType 16 15 -6.25%
BenchmarkServerHandlerNoHeader 10 9 -10.00%
benchmark old bytes new bytes delta
BenchmarkServerFakeConnNoKeepAlive 2557 2492 -2.54%
BenchmarkServerFakeConnWithKeepAlive 2260 2194 -2.92%
BenchmarkServerFakeConnWithKeepAliveLite 1092 1026 -6.04%
BenchmarkServerHandlerTypeLen 1941 1875 -3.40%
BenchmarkServerHandlerNoLen 1898 1832 -3.48%
BenchmarkServerHandlerNoType 1906 1840 -3.46%
BenchmarkServerHandlerNoHeader 1092 1026 -6.04%
Update #5195
R=golang-dev, daniel.morsing
CC=golang-dev
https://golang.org/cl/9492044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/27f7427995782bf60195ca65fd9b44aa34913b75
元コミット内容
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージにおいて、HTTPボディの転送処理を簡素化し、それに伴うメモリ割り当て(allocation)を削減することを目的としています。コミットメッセージには、様々なベンチマーク結果が示されており、処理時間(ns/op)、メモリ割り当て回数(allocs)、割り当てられたバイト数(bytes)の全てにおいて改善が見られることが報告されています。特に、メモリ割り当て回数とバイト数の削減は、ガベージコレクションの負荷軽減に繋がり、全体的なパフォーマンス向上に寄与します。
変更の背景
Goのnet/http
パッケージは、ウェブサーバーやクライアントを構築するための基盤であり、そのパフォーマンスはGoアプリケーション全体の効率に直結します。特に、HTTPリクエストやレスポンスのボディを処理する部分は、データ量が多い場合にボトルネックとなりやすい箇所です。
このコミットが行われた背景には、以下の課題意識があったと考えられます。
- メモリ割り当ての最適化: Goのガベージコレクタは非常に効率的ですが、不要なメモリ割り当ては依然としてパフォーマンスオーバーヘッドの原因となります。特に、リクエスト/レスポンスごとに頻繁に発生する小さなオブジェクトの割り当ては、ガベージコレクションの頻度を増やし、アプリケーションのレイテンシに影響を与える可能性があります。
- コードの簡素化と可読性: 複雑なコードはバグの温床となりやすく、メンテナンスコストも高くなります。HTTPボディの転送処理は、チャンク転送、Content-Length、Keep-Aliveなど、様々なケースを考慮する必要があるため、コードが複雑になりがちです。これを簡素化することで、将来的な開発やデバッグが容易になります。
- パフォーマンスの向上: 上記のメモリ割り当ての最適化とコードの簡素化は、結果として処理時間の短縮に繋がります。ベンチマーク結果が示すように、この変更によってHTTPサーバーの処理性能が向上しています。
特に、eofReader
の改善は、ボディが存在しないHTTPリクエスト/レスポンス(例: GET
リクエストのレスポンスボディがない場合や、HEAD
リクエストなど)において、不必要なio.Reader
のラッパーオブジェクトの生成を避けることで、メモリ割り当てを削減し、処理を効率化することを意図しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する前提知識が必要です。
-
Go言語の
io
パッケージ:io.Reader
インターフェース: データを読み込むための基本的なインターフェースで、Read([]byte) (n int, err error)
メソッドを持ちます。ファイル、ネットワーク接続、メモリ上のデータなど、様々なソースからの読み込みを抽象化します。io.Closer
インターフェース: リソースを閉じるためのインターフェースで、Close() error
メソッドを持ちます。ファイルハンドルやネットワーク接続など、使用後に解放する必要があるリソースに実装されます。io.ReadCloser
インターフェース:io.Reader
とio.Closer
の両方を満たすインターフェースです。io.LimitReader
: 指定されたバイト数までしか読み込まないio.Reader
を返します。HTTPのContent-Lengthヘッダで指定されたボディサイズを読み込む際に利用されます。ioutil.NopCloser
: 任意のio.Reader
をio.ReadCloser
に変換する関数です。Close
メソッドは何もしません。strings.NewReader
: 文字列からデータを読み込むio.Reader
を生成します。
-
HTTPプロトコルにおけるボディ転送:
- Content-Length: HTTPメッセージボディの長さをバイト単位で示すヘッダです。クライアントとサーバーは、この値に基づいてボディの読み込みを終了します。
- Transfer-Encoding: chunked: ボディの長さを事前に知ることができない場合に用いられる転送エンコーディングです。ボディは複数の「チャンク」に分割され、各チャンクの前にそのサイズが記述されます。ボディの終端は、サイズが0のチャンクで示されます。
- Keep-Alive: 複数のHTTPリクエスト/レスポンスを同じTCP接続上で送受信することを可能にするメカニズムです。これにより、接続の確立と切断のオーバーヘッドを削減し、パフォーマンスを向上させます。
- ボディが存在しないケース:
GET
リクエストのレスポンスボディがない場合や、HEAD
リクエスト、OPTIONS
リクエストなど、HTTPメソッドによってはボディが存在しないことがあります。この場合、io.Reader
は即座にEOF(End Of File)を返す必要があります。
-
Go言語のベンチマーク:
- Goには、コードのパフォーマンスを測定するための組み込みのベンチマークツールがあります。
go test -bench=.
コマンドで実行され、ns/op
(操作あたりのナノ秒)、allocs
(操作あたりのメモリ割り当て回数)、bytes
(操作あたりのメモリ割り当てバイト数)などの指標が出力されます。これらの指標は、コードの最適化において非常に重要です。
- Goには、コードのパフォーマンスを測定するための組み込みのベンチマークツールがあります。
技術的詳細
このコミットの主要な変更点は、net/http
パッケージ内のeofReader
変数の定義と、transfer.go
におけるbody
構造体の利用方法です。
eofReader
の変更
変更前、eofReader
は以下のように定義されていました。
var eofReader = ioutil.NopCloser(strings.NewReader(""))
これは、空文字列を読み込むstrings.NewReader
をioutil.NopCloser
でラップしたもので、常にEOFを返すio.ReadCloser
として機能していました。しかし、この実装では、ioutil.NopCloser
が内部で新しいio.ReadCloser
インターフェース型のアロケーションを発生させていました。
変更後、eofReader
は以下のように定義されました。
var eofReader = &struct {
*strings.Reader
io.Closer
}{
strings.NewReader(""),
ioutil.NopCloser(nil),
}
この変更により、eofReader
は匿名構造体へのポインタとなり、*strings.Reader
とio.Closer
を直接埋め込む形になりました。
*strings.Reader
を埋め込むことで、io.Copy
がWriteTo
メソッドを利用できるようになり、バッファを必要とせずに効率的なコピーが可能になります。strings.Reader
はio.Reader
インターフェースを満たし、かつWriteTo
メソッドも持っています。ioutil.NopCloser(nil)
は、Close
メソッドが何もしないio.Closer
を返します。以前のioutil.NopCloser(strings.NewReader(""))
のようにstrings.NewReader
をラップするのではなく、直接nil
を渡すことで、ioutil.NopCloser
が内部で余分なio.Reader
の参照を持たなくなり、よりシンプルになります。
この新しいeofReader
の定義は、単一の静的変数として初期化されるため、HTTPボディが存在しない場合に毎回新しいio.ReadCloser
オブジェクトを割り当てる必要がなくなります。これにより、メモリ割り当てが削減され、ガベージコレクションの負荷が軽減されます。
transfer.go
におけるbody
構造体の利用方法の変更
src/pkg/net/http/transfer.go
のreadTransfer
関数では、HTTPボディの読み込み方法を決定し、適切なio.Reader
をt.Body
に設定していました。
変更前は、ボディが存在しない場合や、Content-Lengthが0の場合に、t.Body
に&body{Reader: eofReader, closing: t.Close}
のようにbody
構造体のインスタンスを割り当てていました。
// 変更前 (transfer.go)
case chunked(t.TransferEncoding):
if noBodyExpected(t.RequestMethod) {
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:
t.Body = &body{Reader: eofReader, closing: t.Close}
// ...
else {
// Persistent connection (i.e. HTTP/1.1)
t.Body = &body{Reader: eofReader, closing: t.Close}
}
変更後、これらのケースでは直接t.Body = eofReader
と設定されるようになりました。
// 変更後 (transfer.go)
case chunked(t.TransferEncoding):
if noBodyExpected(t.RequestMethod) {
t.Body = eofReader
} else {
t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
}
case realLength == 0:
t.Body = eofReader
// ...
else {
// Persistent connection (i.e. HTTP/1.1)
t.Body = eofReader
}
これは、eofReader
自体がio.ReadCloser
インターフェースを満たすように変更されたため、body
構造体でラップする必要がなくなったためです。body
構造体は、チャンク転送やContent-Lengthを持つボディなど、実際のデータ読み込みが必要な場合にのみ使用されるようになりました。これにより、ボディが存在しない場合にbody
構造体のインスタンスを割り当てるオーバーヘッドが削減されます。
body
構造体からのフィールド削除
src/pkg/net/http/transfer.go
のbody
構造体から、res *response
フィールドが削除されました。
// 変更前 (transfer.go)
type body struct {
Reader io.Reader // underlying wire-format reader for the trailer
r *bufio.Reader // underlying wire-format reader for the trailer
closing bool // is the connection to be closed after reading body?
closed bool
res *response // response writer for server requests, else nil
}
// 変更後 (transfer.go)
type body struct {
Reader io.Reader // underlying wire-format reader for the trailer
r *bufio.Reader // underlying wire-format reader for the trailer
closing bool // is the connection to be closed after reading body?
closed bool
}
このres
フィールドは、サーバーリクエストのレスポンスライターへの参照を保持していましたが、コミットメッセージやコードの変更内容から判断すると、このフィールドがbody
のClose
メソッド内で特定のロジック(requestBodyLimitHit
のチェックなど)のために使用されていたようです。しかし、このロジックが不要になったか、別の方法で処理されるようになったため、フィールドが削除されました。これにより、body
構造体のサイズが小さくなり、そのインスタンスが割り当てられる際のメモリ使用量がわずかに削減されます。
また、body.Close()
メソッド内の以下の条件分岐が削除されました。
// 削除されたコード (transfer.go)
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.
b.res != nil && b.res.requestBodyLimitHit
の条件は、res
フィールドの削除に伴い不要になりました。
b.Reader == eofReader
の条件は、eofReader
が直接t.Body
に設定されるようになったため、body
構造体のReader
フィールドがeofReader
であるケースがなくなったため削除されました。これにより、Close
メソッドのロジックが簡素化されています。
これらの変更は、HTTPボディの転送処理における不要なオブジェクトの生成を削減し、コードパスを簡素化することで、全体的なパフォーマンスと効率を向上させています。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go
index fe35562447..698d3f9d46 100644
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1814,7 +1814,15 @@ func (globalOptionsHandler) ServeHTTP(w ResponseWriter, r *Request) {
}
// eofReader is a non-nil io.ReadCloser that always returns EOF.
-var eofReader = ioutil.NopCloser(strings.NewReader(""))
+// It embeds a *strings.Reader so it still has a WriteTo method
+// and io.Copy won't need a buffer.
+var eofReader = &struct {
+ *strings.Reader
+ io.Closer
+}{
+ strings.NewReader(""),
+ ioutil.NopCloser(nil),
+}
// initNPNRequest is an HTTP handler that initializes certain
// uninitialized fields in its *Request. Such partially-initialized
diff --git a/src/pkg/net/http/transfer.go b/src/pkg/net/http/transfer.go
index 53569bcc2f..b97f7160f4 100644
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -328,12 +328,12 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
switch {
case chunked(t.TransferEncoding):
if noBodyExpected(t.RequestMethod) {
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
} else {
t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
}
case realLength == 0:
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
case realLength > 0:
t.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}
default:
@@ -343,7 +343,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
\tt.Body = &body{Reader: r, closing: t.Close}
} else {
// Persistent connection (i.e. HTTP/1.1)
-\t\t\tt.Body = &body{Reader: eofReader, closing: t.Close}
+\t\t\tt.Body = eofReader
}
}\n
@@ -518,8 +518,6 @@ type body struct {
r *bufio.Reader // underlying wire-format reader for the trailer
closing bool // is the connection to be closed after reading body?
closed bool
-\n-\tres *response // response writer for server requests, else nil
}
// ErrBodyReadAfterClose is returned when reading a Request or Response
@@ -618,14 +616,6 @@ func (b *body) Close() error {\n case b.hdr == nil && b.closing:\n \t// no trailer and closing the connection next.\n \t// no point in reading to EOF.\n-\tcase b.res != nil && b.res.requestBodyLimitHit:\n-\t\t// In a server request, don\'t continue reading from the client\n-\t\t// if we\'ve already hit the maximum body size set by the\n-\t\t// handler. If this is set, that also means the TCP connection\n-\t\t// is about to be closed, so getting to the next HTTP request\n-\t\t// in the stream is not necessary.\n-\tcase b.Reader == eofReader:\n-\t\t// Nothing to read. No need to io.Copy from it.\n \tdefault:\n \t\t// Fully consume the body, which will also lead to us reading\n \t\t// the trailer headers after the body, if present.\n```
## コアとなるコードの解説
### `src/pkg/net/http/server.go`の変更
* **`eofReader`変数の再定義**:
* 変更前は`ioutil.NopCloser(strings.NewReader(""))`という形式で、`io.ReadCloser`インターフェースを満たすオブジェクトを生成していました。これは、空の文字列を読み込むリーダーを`NopCloser`(`Close`メソッドが何もしない)でラップしたものです。
* 変更後は、匿名構造体へのポインタ`&struct{...}{...}`として定義されています。この匿名構造体は、`*strings.Reader`と`io.Closer`を埋め込んでいます。
* `strings.NewReader("")`は、空文字列を読み込む`*strings.Reader`のインスタンスを生成します。`*strings.Reader`は`io.Reader`インターフェースを満たすだけでなく、`io.WriterTo`インターフェースも満たします。これにより、`io.Copy`が`WriteTo`メソッドを利用して、バッファを介さずに直接データをコピーできるようになり、効率が向上します。
* `ioutil.NopCloser(nil)`は、`Close`メソッドが何もしない`io.Closer`のインスタンスを生成します。以前のように`strings.NewReader("")`をラップするのではなく、直接`nil`を渡すことで、`NopCloser`が余分な参照を持たなくなり、よりシンプルになります。
* この変更により、`eofReader`は単一の静的オブジェクトとして存在し、HTTPボディが存在しない場合に毎回新しい`io.ReadCloser`オブジェクトを割り当てる必要がなくなります。これにより、メモリ割り当てが削減されます。
### `src/pkg/net/http/transfer.go`の変更
* **`readTransfer`関数内の`t.Body`への割り当ての簡素化**:
* `readTransfer`関数は、HTTPリクエスト/レスポンスのボディをどのように読み込むかを決定する重要な関数です。
* 変更前は、ボディが存在しない場合(`noBodyExpected`が真の場合、`realLength == 0`の場合、または永続接続でボディがない場合)に、`t.Body`に`&body{Reader: eofReader, closing: t.Close}`という形式で`body`構造体の新しいインスタンスを割り当てていました。
* 変更後は、これらのケースで直接`t.Body = eofReader`と設定されるようになりました。これは、`eofReader`自体が`io.ReadCloser`インターフェースを満たすように再定義されたため、`body`構造体でラップする必要がなくなったためです。これにより、ボディが存在しない場合に`body`構造体のインスタンスを割り当てるオーバーヘッドが削減されます。
* **`body`構造体からの`res *response`フィールドの削除**:
* `body`構造体は、HTTPボディの読み込み状態を管理するためのものです。
* 変更前は、`res *response`というフィールドを持っていました。これは、サーバーリクエストのレスポンスライターへの参照を保持していました。
* 変更後は、この`res`フィールドが削除されました。これにより、`body`構造体のサイズが小さくなり、そのインスタンスが割り当てられる際のメモリ使用量がわずかに削減されます。このフィールドが不要になったのは、関連するロジックが変更されたか、別の方法で処理されるようになったためと考えられます。
* **`body.Close()`メソッド内の条件分岐の削除**:
* `body.Close()`メソッドは、HTTPボディの読み込みを終了し、必要に応じて残りのデータを消費するためのものです。
* 変更前は、`b.res != nil && b.res.requestBodyLimitHit`と`b.Reader == eofReader`という2つの条件分岐がありました。
* `b.res != nil && b.res.requestBodyLimitHit`の条件は、`res`フィールドの削除に伴い不要になりました。
* `b.Reader == eofReader`の条件は、`eofReader`が直接`t.Body`に設定されるようになったため、`body`構造体の`Reader`フィールドが`eofReader`であるケースがなくなったため削除されました。
* これらの条件分岐の削除により、`Close`メソッドのロジックが簡素化され、実行パスが短縮されます。
これらの変更は全体として、HTTPボディの転送処理における不要なオブジェクトの生成を削減し、コードパスを簡素化することで、メモリ割り当ての削減とパフォーマンスの向上を実現しています。
## 関連リンク
* Go Issue: [#5195](https://github.com/golang/go/issues/5195) (このコミットが解決した、または関連するGoのIssue)
* Gerrit Change: [https://golang.org/cl/9492044](https://golang.org/cl/9492044) (このコミットの元のGerritレビューページ)
## 参考にした情報源リンク
* Go言語の`io`パッケージに関する公式ドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go言語の`net/http`パッケージに関する公式ドキュメント: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
* Go言語のベンチマークに関する公式ドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* HTTP/1.1仕様 (RFC 2616): [https://datatracker.ietf.org/doc/html/rfc2616](https://datatracker.ietf.org/doc/html/rfc2616) (特にContent-Length, Transfer-Encoding, Keep-Aliveに関するセクション)
* Go言語のガベージコレクションに関する情報 (一般的な概念): [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide) (GoのGCの仕組みを理解するのに役立ちます)
* `ioutil.NopCloser`のドキュメント: [https://pkg.go.dev/io/ioutil#NopCloser](https://pkg.go.dev/io/ioutil#NopCloser)
* `strings.NewReader`のドキュメント: [https://pkg.go.dev/strings#NewReader](https://pkg.go.dev/strings#NewReader)
* `io.Copy`のドキュメント: [https://pkg.go.dev/io#Copy](https://pkg.go.dev/io#Copy) (特に`WriterTo`インターフェースの利用について)