[インデックス 17150] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内のHTTPサーバー実装を簡素化し、bufio.Reader
および bufio.Writer
の Reset
メソッドを活用することで、パフォーマンスを向上させることを目的としています。具体的には、以前使用されていた switchReader
や switchWriter
といったカスタムのラッパー型を廃止し、bufio
パッケージが提供する標準機能に直接依存するように変更されています。これにより、コードの複雑性が軽減され、リソースの再利用が効率化されています。
コミット
commit 3e38b7f2465a6ab476cb0e184f2b2abee1a6e76f
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sat Aug 10 19:22:44 2013 -0700
net/http: simplify server, use bufio Reader.Reset and Writer.Reset
Update #5100
Update #6086
Remove switchReader, switchWriter, switchReaderPair,
switchWriterPair, etc.
Now it only maintains pools of bufio Readers and Writers, but
uses Reset instead of working around all their
previously-associated state.
Compared to before the bufio Reset change, it's the same number of
allocations, and also faster:
benchmark old ns/op new ns/op delta
BenchmarkClientServer 111218 109828 -1.25%
BenchmarkClientServerParallel4 70580 70013 -0.80%
BenchmarkClientServerParallel64 72636 68919 -5.12%
BenchmarkServer 139858 137068 -1.99%
BenchmarkServerFakeConnNoKeepAlive 14619 14314 -2.09%
BenchmarkServerFakeConnWithKeepAlive 12390 11361 -8.31%
BenchmarkServerFakeConnWithKeepAliveLite 7630 7306 -4.25%
BenchmarkServerHandlerTypeLen 9688 9342 -3.57%
BenchmarkServerHandlerNoLen 8700 8470 -2.64%
BenchmarkServerHandlerNoType 9255 8949 -3.31%
BenchmarkServerHandlerNoHeader 7058 6806 -3.57%
benchmark old allocs new allocs delta
BenchmarkClientServer 61 61 0.00%
BenchmarkClientServerParallel4 61 61 0.00%
BenchmarkClientServerParallel64 61 61 0.00%
BenchmarkServer 16 16 0.00%
BenchmarkServerFakeConnNoKeepAlive 24 24 0.00%
BenchmarkServerFakeConnWithKeepAlive 19 19 0.00%
BenchmarkServerFakeConnWithKeepAliveLite 9 9 0.00%
BenchmarkServerHandlerTypeLen 17 17 0.00%\n BenchmarkServerHandlerNoLen 14 14 0.00%\n BenchmarkServerHandlerNoType 15 15 0.00%\n BenchmarkServerHandlerNoHeader 9 9 0.00%\n \n benchmark old bytes new bytes delta\n BenchmarkClientServer 6988 6985 -0.04%\n BenchmarkClientServerParallel4 6979 6985 0.09%\n BenchmarkClientServerParallel64 7002 7019 0.24%\n BenchmarkServer 1846 1848 0.11%\n BenchmarkServerFakeConnNoKeepAlive 2420 2412 -0.33%\n BenchmarkServerFakeConnWithKeepAlive 2126 2129 0.14%\n BenchmarkServerFakeConnWithKeepAliveLite 989 990 0.10%\n BenchmarkServerHandlerTypeLen 1818 1819 0.06%\n BenchmarkServerHandlerNoLen 1775 1777 0.11%\n BenchmarkServerHandlerNoType 1783 1785 0.11%\n BenchmarkServerHandlerNoHeader 989 990 0.10%\n \n R=golang-dev, r\n CC=golang-dev\n https://golang.org/cl/12708046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3e38b7f2465a6ab476cb0e184f2b2abee1a6e76f
元コミット内容
このコミットの目的は、net/http
パッケージのサーバー実装を簡素化し、bufio.Reader
と bufio.Writer
の Reset
メソッドを利用することです。これにより、switchReader
、switchWriter
、switchReaderPair
、switchWriterPair
といったカスタム型が削除されました。変更後も bufio.Reader
と bufio.Writer
のプールは維持されますが、以前の関連する状態を回避するために Reset
メソッドが使用されます。
ベンチマーク結果によると、この変更はアロケーション数に影響を与えず、パフォーマンスが向上しています。特に BenchmarkServerFakeConnWithKeepAlive
では8.31%の高速化が見られます。
変更の背景
Goの net/http
パッケージは、ウェブサーバーを構築するための基盤を提供します。HTTPプロトコルはステートレスですが、TCP接続は再利用されることが多く(特にHTTP/1.1のKeep-Alive機能)、これによりパフォーマンスが向上します。サーバーは、新しいリクエストが来るたびに新しい bufio.Reader
や bufio.Writer
を作成するのではなく、既存のものを再利用したいと考えます。
このコミット以前は、bufio.Reader
や bufio.Writer
の基盤となる io.Reader
や io.Writer
を動的に切り替えるために、switchReader
や switchWriter
といったカスタムのラッパー型が使用されていました。これは、bufio
パッケージが提供する Reader
や Writer
のインスタンスを再利用しつつ、異なる基盤となるI/Oストリーム(例えば、異なるHTTPリクエストのボディ)に切り替える必要があったためです。しかし、これらのカスタムラッパーはコードの複雑性を増し、管理が煩雑になる可能性がありました。
このコミットの背景には、bufio
パッケージに Reset
メソッドが導入されたことがあります。Reset
メソッドは、既存の bufio.Reader
や bufio.Writer
の内部状態をリセットし、新しい基盤となる io.Reader
や io.Writer
に関連付ける機能を提供します。これにより、カスタムのラッパー型を使用することなく、bufio
インスタンスの再利用が可能になり、コードの簡素化と効率化が図れるようになりました。
コミットメッセージに記載されている Update #5100
と Update #6086
は、この変更が解決または関連する既存の課題や改善提案を示唆していますが、具体的な内容は提供された情報からは特定できませんでした。しかし、一般的にこのような変更は、リソース管理の改善、ガベージコレクションの負荷軽減、そして全体的なパフォーマンスの向上を目的としています。
前提知識の解説
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。ウェブアプリケーションやAPIサーバーを構築する際に中心的な役割を果たします。bufio
パッケージ: バッファリングされたI/O操作を提供します。bufio.Reader
はio.Reader
をラップしてバッファリングされた読み取りを提供し、bufio.Writer
はio.Writer
をラップしてバッファリングされた書き込みを提供します。これにより、基盤となるI/O操作の回数を減らし、パフォーマンスを向上させます。io.Reader
とio.Writer
インターフェース: Go言語における基本的なI/Oインターフェースです。io.Reader
はRead
メソッドを持ち、データを読み取る能力を定義し、io.Writer
はWrite
メソッドを持ち、データを書き込む能力を定義します。- Keep-Alive: HTTP/1.1で導入された機能で、単一のTCP接続を複数のHTTPリクエスト/レスポンスの送受信に再利用することを可能にします。これにより、接続の確立と切断にかかるオーバーヘッドを削減し、ウェブページのロード時間を短縮します。
- オブジェクトプール: オブジェクトの再利用を目的としたデザインパターンです。新しいオブジェクトを頻繁に作成・破棄する代わりに、事前に作成されたオブジェクトのプールから取得し、使用後にプールに戻すことで、アロケーションとガベージコレクションのオーバーヘッドを削減します。
bufio.Reader.Reset(r io.Reader)
:bufio.Reader
のメソッドで、内部バッファをリセットし、新しいio.Reader
r
から読み取るように設定します。これにより、既存のbufio.Reader
インスタンスを再利用して、異なる入力ソースから読み取ることができます。bufio.Writer.Reset(w io.Writer)
:bufio.Writer
のメソッドで、内部バッファをリセットし、新しいio.Writer
w
に書き込むように設定します。これにより、既存のbufio.Writer
インスタンスを再利用して、異なる出力先に書き込むことができます。
技術的詳細
このコミットの核心は、net/http
パッケージのサーバーがHTTP接続を処理する際に使用する bufio.Reader
と bufio.Writer
の管理方法の変更です。
以前のバージョンでは、conn
構造体(HTTP接続を表す)は bufio.ReadWriter
の他に、その基盤となる io.Reader
と io.Writer
を動的に切り替えるためのカスタムラッパーである switchReader
と switchWriter
を保持していました。これらのラッパーは、bufio.Reader
や bufio.Writer
が内部的に参照する io.Reader
や io.Writer
を変更するために使用されていました。例えば、HTTPリクエストのヘッダーを読み取った後、リクエストボディを読み取るために、基盤となるリーダーを切り替える必要がありました。
このアプローチは機能的には問題ありませんでしたが、switchReader
や switchWriter
といった追加の型と、それらを管理するためのロジックが必要となり、コードの複雑性を増していました。また、これらのラッパー型は、bufio.Reader
や bufio.Writer
のプールと連携して動作するため、bufioReaderPair
や bufioWriterPair
といった構造体も存在し、プールから取得した bufio
インスタンスと、それに対応する switch
インスタンスをペアで管理していました。
このコミットでは、bufio.Reader
と bufio.Writer
に Reset
メソッドが導入されたことを利用して、この複雑な構造を排除しています。Reset
メソッドは、bufio.Reader
や bufio.Writer
の内部状態(バッファの内容、エラー状態など)をクリアし、新しい基盤となる io.Reader
や io.Writer
を設定します。これにより、switchReader
や switchWriter
のようなカスタムラッパーは不要になります。
具体的には、以下の変更が行われました。
conn
構造体からのフィールド削除:conn
構造体からbufswr
(*switchReader
) とbufsww
(*switchWriter
) フィールドが削除されました。bufioReaderPair
とbufioWriterPair
の削除: これらのヘルパー構造体は、switchReader
とswitchWriter
の削除に伴い不要になったため、削除されました。- キャッシュの型変更:
bufioReaderCache
、bufioWriterCache2k
、bufioWriterCache4k
といったキャッシュチャネルの型が、bufioReaderPair
やbufioWriterPair
から直接*bufio.Reader
や*bufio.Writer
に変更されました。 newBufioReader
とnewBufioWriterSize
の簡素化: これらの関数は、プールからbufio
インスタンスを取得する際に、Reset
メソッドを呼び出すように変更されました。これにより、switchReader
やswitchWriter
を作成・設定するロジックが不要になりました。putBufioReader
とputBufioWriter
の簡素化: これらの関数は、bufio
インスタンスをプールに戻す際に、Reset(nil)
を呼び出すように変更されました。これにより、switchReader
やswitchWriter
の参照をクリアするロジックが不要になりました。また、putBufioWriter
から、バッファにデータが残っている場合やエラーが発生している場合に再利用しないというロジックが削除されました。これはReset
メソッドがこれらの状態を適切に処理するためと考えられます。readRequest
およびfinishRequest
での呼び出し変更:response
構造体の初期化やリクエストの終了処理において、newBufioWriterSize
やputBufioWriter
の呼び出しからsw
(switchWriter) の引数が削除されました。
この変更により、コードベースはより簡潔になり、bufio
パッケージの標準機能に依存することで、将来的なメンテナンスも容易になります。
ベンチマーク結果は、この変更がパフォーマンスに良い影響を与えたことを示しています。特に ns/op
(nanoseconds per operation) の値が減少しており、これは各操作にかかる時間が短縮されたことを意味します。allocs
(allocations) の値は変わっておらず、これはオブジェクトプールの利用により、ヒープアロケーションの回数が維持されていることを示しています。bytes
(bytes allocated) の値もほぼ変わっておらず、メモリ使用量に大きな影響がないことを示しています。
コアとなるコードの変更箇所
変更は主に src/pkg/net/http/server.go
ファイルで行われています。
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -110,8 +110,6 @@ type conn struct {
sr liveSwitchReader // where the LimitReader reads from; usually the rwc
lr *io.LimitedReader // io.LimitReader(sr)
buf *bufio.ReadWriter // buffered(lr,rwc), reading from bufio->limitReader->sr->rwc
- bufswr *switchReader // the *switchReader io.Reader source of buf
- bufsww *switchWriter // the *switchWriter io.Writer dest of buf
tlsState *tls.ConnectionState // or nil when not using TLS
mu sync.Mutex // guards the following
@@ -430,34 +428,20 @@ func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) {
}\n \tc.sr = liveSwitchReader{r: c.rwc}\n \tc.lr = io.LimitReader(&c.sr, noLimit).(*io.LimitedReader)\n-\tbr, sr := newBufioReader(c.lr)\n-\tbw, sw := newBufioWriterSize(c.rwc, 4<<10)\n+\tbr := newBufioReader(c.lr)\n+\tbw := newBufioWriterSize(c.rwc, 4<<10)\n \tc.buf = bufio.NewReadWriter(br, bw)\n-\tc.bufswr = sr\n-\tc.bufsww = sw\n \treturn c, nil
}\n \n-// TODO: remove this, if issue 5100 is fixed
-type bufioReaderPair struct {\n-\tbr *bufio.Reader\n-\tsr *switchReader // from which the bufio.Reader is reading\n-}\n-\n-// TODO: remove this, if issue 5100 is fixed
-type bufioWriterPair struct {\n-\tbw *bufio.Writer\n-\tsw *switchWriter // to which the bufio.Writer is writing\n-}\n-\n // TODO: use a sync.Cache instead
var (\n-\tbufioReaderCache = make(chan bufioReaderPair, 4)\n-\tbufioWriterCache2k = make(chan bufioWriterPair, 4)\n-\tbufioWriterCache4k = make(chan bufioWriterPair, 4)\n+\tbufioReaderCache = make(chan *bufio.Reader, 4)\n+\tbufioWriterCache2k = make(chan *bufio.Writer, 4)\n+\tbufioWriterCache4k = make(chan *bufio.Writer, 4)\n )\n \n-func bufioWriterCache(size int) chan bufioWriterPair {\n+func bufioWriterCache(size int) chan *bufio.Writer {\n \tswitch size {\n \tcase 2 << 10:\n \t\treturn bufioWriterCache2k\n@@ -467,55 +451,38 @@ func bufioWriterCache(size int) chan bufioWriterPair {\n \treturn nil\n }\n \n-func newBufioReader(r io.Reader) (*bufio.Reader, *switchReader) {\n+func newBufioReader(r io.Reader) *bufio.Reader {\n \tselect {\n \tcase p := <-bufioReaderCache:\n-\t\tp.sr.Reader = r\n-\t\treturn p.br, p.sr\n+\t\tp.Reset(r)\n+\t\treturn p\n \tdefault:\n-\t\tsr := &switchReader{r}\n-\t\treturn bufio.NewReader(sr), sr\n+\t\treturn bufio.NewReader(r)\n \t}\n }\n \n-func putBufioReader(br *bufio.Reader, sr *switchReader) {\n-\tif n := br.Buffered(); n > 0 {\n-\t\tio.CopyN(ioutil.Discard, br, int64(n))\n-\t}\n-\tbr.Read(nil) // clears br.err\n-\tsr.Reader = nil\n+func putBufioReader(br *bufio.Reader) {\n+\tbr.Reset(nil)\n \tselect {\n-\tcase bufioReaderCache <- bufioReaderPair{br, sr}:\n+\tcase bufioReaderCache <- br:\n \tdefault:\n \t}\n }\n \n-func newBufioWriterSize(w io.Writer, size int) (*bufio.Writer, *switchWriter) {\n+func newBufioWriterSize(w io.Writer, size int) *bufio.Writer {\n \tselect {\n \tcase p := <-bufioWriterCache(size):\n-\t\tp.sw.Writer = w\n-\t\treturn p.bw, p.sw\n+\t\tp.Reset(w)\n+\t\treturn p\n \tdefault:\n-\t\tsw := &switchWriter{w}\n-\t\treturn bufio.NewWriterSize(sw, size), sw\n+\t\treturn bufio.NewWriterSize(w, size)\n \t}\n }\n \n-func putBufioWriter(bw *bufio.Writer, sw *switchWriter) {\n-\tif bw.Buffered() > 0 {\n-\t\t// It must have failed to flush to its target\n-\t\t// earlier. We can't reuse this bufio.Writer.\n-\t\treturn\n-\t}\n-\tif err := bw.Flush(); err != nil {\n-\t\t// Its sticky error field is set, which is returned by\n-\t\t// Flush even when there's no data buffered. This\n-\t\t// bufio Writer is dead to us. Don't reuse it.\n-\t\treturn\n-\t}\n-\tsw.Writer = nil\n+func putBufioWriter(bw *bufio.Writer) {\n+\tbw.Reset(nil)\n \tselect {\n-\tcase bufioWriterCache(bw.Available()) <- bufioWriterPair{bw, sw}:\n+\tcase bufioWriterCache(bw.Available()) <- bw:\n \tdefault:\n \t}\n }\n@@ -621,7 +588,7 @@ func (c *conn) readRequest() (w *response, err error) {\n \t\tcontentLength: -1,\n \t}\n \tw.cw.res = w\n-\tw.w, w.sw = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)\n+\tw.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)\n \treturn w, nil\n }\n \n@@ -1007,7 +974,7 @@ func (w *response) finishRequest() {\n \t}\n \n \tw.w.Flush()\n-\tputBufioWriter(w.w, w.sw)\n+\tputBufioWriter(w.w)\n \tw.cw.close()\n \tw.conn.buf.Flush()\n \n@@ -1040,11 +1007,11 @@ func (c *conn) finalFlush() {\n \n \t\t// Steal the bufio.Reader (~4KB worth of memory) and its associated\n \t\t// reader for a future connection.\n-\t\tputBufioReader(c.buf.Reader, c.bufswr)\n+\t\tputBufioReader(c.buf.Reader)\n \n \t\t// Steal the bufio.Writer (~4KB worth of memory) and its associated\n \t\t// writer for a future connection.\n-\t\tputBufioWriter(c.buf.Writer, c.bufsww)\n+\t\tputBufioWriter(c.buf.Writer)\n \n \t\tc.buf = nil\n \t}\n```
## コアとなるコードの解説
このコミットの主要な変更点は、`net/http` パッケージの `server.go` における `bufio.Reader` と `bufio.Writer` の管理方法の根本的な見直しです。
1. **`conn` 構造体の簡素化**:
* 以前は `conn` 構造体に `bufswr` (`*switchReader`) と `bufsww` (`*switchWriter`) というフィールドがありました。これらは `bufio.Reader` と `bufio.Writer` が内部的に使用する `io.Reader` と `io.Writer` を動的に切り替えるためのカスタムラッパーでした。
* このコミットでは、これらのフィールドが削除されました。これは、`bufio.Reader` と `bufio.Writer` の `Reset` メソッドが導入されたことで、これらのカスタムラッパーが不要になったためです。
2. **`bufioReaderPair` と `bufioWriterPair` の削除**:
* 以前は、`bufio.Reader` や `bufio.Writer` をオブジェクトプールで管理する際に、それらと対応する `switchReader` や `switchWriter` をペアで扱うために `bufioReaderPair` と `bufioWriterPair` というヘルパー構造体が定義されていました。
* `switchReader` と `switchWriter` が不要になったため、これらのペア構造体も削除されました。
3. **キャッシュチャネルの型変更**:
* `bufioReaderCache`、`bufioWriterCache2k`、`bufioWriterCache4k` といった `bufio` インスタンスを再利用するためのチャネルの型が、`bufioReaderPair` や `bufioWriterPair` から直接 `*bufio.Reader` や `*bufio.Writer` に変更されました。これにより、キャッシュの管理がより直接的になりました。
4. **`newBufioReader` と `newBufioWriterSize` 関数の変更**:
* これらの関数は、新しい `bufio.Reader` や `bufio.Writer` インスタンスを取得する(またはプールから再利用する)役割を担っています。
* 変更前は、プールから取得した `bufio` インスタンスに対して、対応する `switchReader` や `switchWriter` の `Reader` や `Writer` フィールドを新しい基盤となる `io.Reader` や `io.Writer` に設定していました。
* 変更後は、プールから取得した `bufio` インスタンスに対して、直接 `p.Reset(r)` や `p.Reset(w)` を呼び出すようになりました。これにより、`bufio` インスタンスの内部状態がリセットされ、新しいI/Oソースに関連付けられます。
5. **`putBufioReader` と `putBufioWriter` 関数の変更**:
* これらの関数は、使用済みの `bufio` インスタンスをプールに戻す役割を担っています。
* 変更前は、プールに戻す前に `switchReader` や `switchWriter` の `Reader` や `Writer` フィールドを `nil` に設定していました。また、`putBufioWriter` では、バッファにデータが残っていたり、エラーが発生していたりする場合には再利用しないというロジックがありました。
* 変更後は、`br.Reset(nil)` や `bw.Reset(nil)` を呼び出すことで、`bufio` インスタンスの内部状態をクリーンアップし、プールに戻す準備をします。`Reset(nil)` は、バッファをクリアし、基盤となるリーダー/ライターへの参照を解除します。これにより、以前の複雑なチェックやクリアロジックが不要になりました。
これらの変更により、`net/http` サーバーは `bufio` パッケージのより新しい、より効率的な機能を利用できるようになり、コードの簡素化とパフォーマンスの向上が実現されました。特に、`Reset` メソッドの利用は、オブジェクトプールの効率を最大化し、ガベージコレクションの負担を軽減する上で重要な役割を果たします。
## 関連リンク
* Go言語の `net/http` パッケージ: [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)
* Go言語の `bufio` パッケージ: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
* Go言語の `io` パッケージ: [https://pkg.go.dev/io](https://pkg.go.dev/io)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード
* `bufio.Reader.Reset` および `bufio.Writer.Reset` のドキュメント
* Go言語のベンチマークに関する一般的な情報