[インデックス 15971] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージ内のHTTPサーバーのパフォーマンス最適化に関するものです。具体的には、bufio.Writer の再利用メカニズムを改善し、ガベージコレクション (GC) の回数を減らし、生成されるガベージの量を約半分に削減することを目的としています。これにより、HTTPリクエスト処理の効率が向上し、サーバーのスループットとレイテンシが改善されます。
コミット
393b3b130489b86d44b45f2fa7c53e62516a0aaa
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/393b3b130489b86d44b45f2fa7c53e62516a0aaa
元コミット内容
net/http: server optimization; reduce GCs, generate ~half the garbage
There was another bufio.Writer not being reused, found with
GOGC=off and -test.memprofile.
benchmark old ns/op new ns/op delta
BenchmarkServerFakeConnWithKeepAlive 18270 16046 -12.17%
benchmark old allocs new allocs delta
BenchmarkServerFakeConnWithKeepAlive 38 36 -5.26%
benchmark old bytes new bytes delta
BenchmarkServerFakeConnWithKeepAlive 4598 2488 -45.89%
Update #5100
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/8038047
変更の背景
この変更の背景には、Go言語のHTTPサーバーにおけるパフォーマンスのボトルネック、特にガベージコレクション (GC) のオーバーヘッドとメモリ割り当ての多さがありました。コミットメッセージに記載されているように、GOGC=off と -test.memprofile を使用したプロファイリングによって、bufio.Writer が適切に再利用されていない箇所が特定されました。
bufio.Writer は、I/O操作の効率を高めるためにバッファリングを行う構造体です。HTTPサーバーでは、各リクエストのレスポンスを書き込む際に bufio.Writer が使用されます。もしこの bufio.Writer がリクエストごとに新しく割り当てられ、使用後に破棄される場合、そのたびにメモリ割り当てと解放が発生し、GCの頻度と負荷が増大します。これは、特に高負荷な環境下でサーバーのパフォーマンスに悪影響を及ぼします。
このコミットは、既存の bufio.Reader のキャッシュメカニズムと同様に、bufio.Writer もプールして再利用することで、これらの問題を解決しようとしています。ベンチマーク結果が示すように、この最適化により、処理時間 (ns/op)、メモリ割り当て回数 (allocs)、および割り当てられるバイト数 (bytes) が大幅に削減されています。特に、割り当てられるバイト数が約45%削減されている点は、GC負荷の軽減に大きく貢献します。
前提知識の解説
Go言語のガベージコレクション (GC)
Go言語は自動メモリ管理(ガベージコレクション)を採用しています。プログラムが動的にメモリを割り当てると、GoランタイムのGCが不要になったメモリを自動的に解放します。GCは開発者にとってメモリ管理の手間を省く利点がありますが、GCが実行される際にはプログラムの実行が一時的に停止(ストップ・ザ・ワールド)したり、CPUリソースを消費したりするため、パフォーマンスに影響を与える可能性があります。特に、頻繁なメモリ割り当てと解放はGCの頻度を高め、アプリケーションのレイテンシを悪化させる原因となります。
bufio パッケージ
bufio パッケージは、I/O操作をバッファリングすることで効率化するためのGo標準ライブラリです。
bufio.Reader: 入力ストリームをバッファリングして読み込みます。これにより、小さな読み込み操作が多数発生する代わりに、一度に大きなチャンクを読み込むことでシステムコールを減らし、I/O効率を向上させます。bufio.Writer: 出力ストリームをバッファリングして書き込みます。同様に、小さな書き込み操作をバッファに蓄え、バッファがいっぱいになったりFlushが呼ばれたりしたときにまとめて書き込むことで、I/O効率を向上させます。
sync.Pool (または類似のキャッシュメカニズム)
sync.Pool は、Go言語で一時的なオブジェクトを再利用するためのメカニズムです。オブジェクトをプールに格納し、必要に応じてプールから取得し、使用後にプールに戻すことで、オブジェクトの生成とGCのオーバーヘッドを削減します。このコミットでは、sync.Pool が導入される前のGoのバージョンであるため、chan (チャネル) を使用して手動でオブジェクトプールを実装しています。これは、sync.Pool と同様の目的(オブジェクトの再利用によるGC負荷軽減)を達成するための一般的なパターンでした。
net/http パッケージのサーバー内部
net/http パッケージはGo言語でHTTPサーバーを構築するための主要なパッケージです。HTTPサーバーは、クライアントからのリクエストを受け取り、レスポンスを返す一連の処理を行います。この処理の中で、ネットワーク接続からの読み込み(リクエストボディの読み込みなど)やネットワーク接続への書き込み(レスポンスボディの書き込みなど)が頻繁に発生します。これらのI/O操作の効率は、サーバー全体のパフォーマンスに直結します。
net/http サーバーは、各HTTP接続(またはリクエスト)に対して conn 構造体を生成し、その中で bufio.Reader と bufio.Writer を使用して効率的なI/Oを実現しています。特にKeep-Alive接続の場合、同じ接続が複数のリクエスト-レスポンスサイクルで使用されるため、これらのI/Oバッファを適切に管理し、再利用することが重要になります。
技術的詳細
このコミットの主要な技術的変更点は、net/http サーバーが使用する bufio.Writer の管理方法を改善し、オブジェクトの再利用を促進することです。
-
bufio.Writerのサイズ別キャッシュの導入:- 以前は
bufioWriterCacheという単一のチャネルでbufioWriterPairをキャッシュしていました。 - このコミットでは、
bufioWriterCache2k(2KB) とbufioWriterCache4k(4KB) という2つの異なるサイズのキャッシュチャネルを導入しました。 bufioWriterCache(size int) chan bufioWriterPairというヘルパー関数が追加され、指定されたサイズに基づいて適切なキャッシュチャネルを返すようになりました。これにより、異なるバッファサイズを持つbufio.Writerを効率的にプールできるようになります。
- 以前は
-
newBufioWriterSize関数の導入と利用:- 既存の
newBufioWriter関数がnewBufioWriterSize(w io.Writer, size int)に変更され、bufio.Writerを作成する際にバッファサイズを指定できるようになりました。 - 内部では
bufio.NewWriterSize(sw, size)を呼び出し、指定されたサイズでバッファを作成します。 - オブジェクトの取得時には、
selectステートメントとbufioWriterCache(size)を使用して、指定されたサイズのキャッシュからbufioWriterPairを取得しようとします。キャッシュに利用可能なオブジェクトがない場合は、新しく作成されます。
- 既存の
-
putBufioWriterでのサイズに応じたキャッシュへの返却:putBufioWriter関数が変更され、bufio.Writerをキャッシュに戻す際に、そのbufio.WriterのAvailable()メソッド(バッファの空き容量、つまり元のバッファサイズ)に基づいて適切なサイズのキャッシュ (bufioWriterCache(bw.Available())) に戻すようになりました。これにより、同じサイズのバッファが再利用される可能性が高まります。
-
response構造体へのsw *switchWriterフィールドの追加:response構造体にsw *switchWriterフィールドが追加されました。これは、bufio.Writerに関連付けられたswitchWriterを保持するためのものです。switchWriterは、bufio.Writerがラップしている基盤となるio.Writerを動的に変更できるようにするカスタムのio.Writer実装です。これにより、bufio.Writerをプールから取得した際に、そのswitchWriterの基盤となるio.Writerを現在の接続のnet.Connに設定し直すことができます。
-
conn.readRequest()およびresponse.finishRequest()でのbufio.Writerのライフサイクル管理:conn.readRequest()でresponseオブジェクトを初期化する際に、w.w(bufio.Writer) とw.sw(switchWriter) をnewBufioWriterSizeを使って取得し、response構造体に保持するように変更されました。response.finishRequest()でリクエスト処理が完了した後、w.w.Flush()を呼び出してバッファの内容をフラッシュし、その後putBufioWriter(w.w, w.sw)を呼び出してbufio.WriterとswitchWriterのペアをキャッシュに戻すようになりました。これにより、これらのオブジェクトが次のリクエストで再利用される準備が整います。
これらの変更により、HTTPサーバーはリクエストごとに新しい bufio.Writer を割り当てるのではなく、既存のプールから適切なサイズの bufio.Writer を取得し、使用後にプールに戻すことができるようになりました。これにより、メモリ割り当ての回数が劇的に減少し、結果としてGCの頻度と負荷が軽減され、サーバーの全体的なパフォーマンスが向上します。
コアとなるコードの変更箇所
変更は src/pkg/net/http/server.go ファイルに集中しています。
-
type response structの変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -281,6 +281,7 @@ type response struct { w *bufio.Writer // buffers output in chunks to chunkWriter cw *chunkWriter + sw *switchWriter // of the bufio.Writer, for return to putBufioWriter // handlerHeader is the Header that Handlers get access to, // which may be retained and mutated even after WriteHeader. -
newConn関数内のnewBufioWriterの呼び出し変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -381,7 +382,7 @@ func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) { c.sr = liveSwitchReader{r: c.rwc} c.lr = io.LimitReader(&c.sr, noLimit).(*io.LimitedReader) br, sr := newBufioReader(c.lr) - bw, sw := newBufioWriter(c.rwc) + bw, sw := newBufioWriterSize(c.rwc, 4<<10) c.buf = bufio.NewReadWriter(br, bw) c.bufswr = sr c.bufsww = sw -
bufioWriterCacheの変更とbufioWriterCacheヘルパー関数の追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -402,10 +403,21 @@ type bufioWriterPair struct { // TODO: use a sync.Cache instead var ( - bufioReaderCache = make(chan bufioReaderPair, 4) - bufioWriterCache = make(chan bufioWriterPair, 4) + bufioReaderCache = make(chan bufioReaderPair, 4) + bufioWriterCache2k = make(chan bufioWriterPair, 4) + bufioWriterCache4k = make(chan bufioWriterPair, 4) ) +func bufioWriterCache(size int) chan bufioWriterPair { + switch size { + case 2 << 10: + return bufioWriterCache2k + case 4 << 10: + return bufioWriterCache4k + } + return nil +} + func newBufioReader(r io.Reader) (*bufio.Reader, *switchReader) { select { case p := <-bufioReaderCache: -
newBufioWriterからnewBufioWriterSizeへの変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -429,14 +441,14 @@ func putBufioReader(br *bufio.Reader, sr *switchReader) { } }\n -func newBufioWriter(w io.Writer) (*bufio.Writer, *switchWriter) { +func newBufioWriterSize(w io.Writer, size int) (*bufio.Writer, *switchWriter) { select { -\tcase p := <-bufioWriterCache: +\tcase p := <-bufioWriterCache(size): \tp.sw.Writer = w \treturn p.bw, p.sw \tdefault: \tsw := &switchWriter{w}\n -\t\treturn bufio.NewWriter(sw), sw +\t\treturn bufio.NewWriterSize(sw, size), sw }\n }\n -
putBufioWriterでのキャッシュへの返却ロジックの変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -454,7 +466,7 @@ func putBufioWriter(bw *bufio.Writer, sw *switchWriter) { }\n \tsw.Writer = nil \tselect { -\tcase bufioWriterCache <- bufioWriterPair{bw, sw}: +\tcase bufioWriterCache(bw.Available()) <- bufioWriterPair{bw, sw}: \tdefault: }\n }\n -
conn.readRequestでのresponse初期化時の変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -540,7 +552,7 @@ func (c *conn) readRequest() (w *response, err error) { \tcw: new(chunkWriter),\n }\n \tw.cw.res = w -\tw.w = bufio.NewWriterSize(w.cw, bufferBeforeChunkingSize) +\tw.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize) return w, nil }\n -
response.finishRequestでのputBufioWriterの呼び出し追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -802,6 +814,7 @@ func (w *response) finishRequest() { }\n \n \tw.w.Flush()\n +\tputBufioWriter(w.w, w.sw)\n \tw.cw.close()\n \tw.conn.buf.Flush()\n \n
コアとなるコードの解説
type response struct への sw *switchWriter の追加
response 構造体は、HTTPレスポンスの構築と送信に関連する状態を保持します。以前は w *bufio.Writer しかありませんでしたが、この変更により sw *switchWriter が追加されました。
switchWriter は、bufio.Writer が内部で書き込みを行う実際の io.Writer を動的に切り替えるためのカスタム型です。bufio.Writer をプールから再利用する際、その bufio.Writer が以前の接続で使用していた io.Writer を保持している可能性があります。switchWriter を介することで、bufio.Writer を再利用する際に、その switchWriter.Writer フィールドを現在の接続の net.Conn に設定し直すことができ、バッファリングされた書き込みが正しい接続に対して行われるようにします。この sw フィールドを response 構造体に保持することで、putBufioWriter を呼び出す際に bufio.Writer とその対応する switchWriter のペアを簡単に渡せるようになります。
newConn 関数内の newBufioWriterSize(c.rwc, 4<<10)
newConn は新しいHTTP接続が確立されたときに呼び出され、その接続のための conn オブジェクトを初期化します。ここで、以前は newBufioWriter(c.rwc) を呼び出していましたが、newBufioWriterSize(c.rwc, 4<<10) に変更されました。
4<<10 は 4 * 2^10、つまり 4KB を意味します。これは、この接続の bufio.Writer が 4KB のバッファサイズを持つことを示しています。これにより、bufio.Writer が特定のバッファサイズでプールから取得または新しく作成されるようになり、サイズに応じた効率的な再利用が可能になります。
bufioWriterCache の変更と bufioWriterCache ヘルパー関数の追加
以前は bufioWriterCache という単一のチャネルで bufioWriterPair をキャッシュしていました。これは、すべての bufio.Writer が同じデフォルトバッファサイズ(通常は4KB)で作成されることを前提としていました。
しかし、net/http サーバーの異なる部分では異なるバッファサイズ(例えば、チャンクエンコーディングのための bufferBeforeChunkingSize は2KB)の bufio.Writer が使用される可能性があります。
この変更では、bufioWriterCache2k と bufioWriterCache4k という2つの異なるサイズのキャッシュチャネルを明示的に定義しました。
bufioWriterCache(size int) ヘルパー関数は、引数として渡された size に応じて適切なキャッシュチャネルを返します。これにより、bufio.Writer を取得または返却する際に、そのバッファサイズに基づいて適切なプールが選択されるようになり、サイズの異なる bufio.Writer が混在することなく、より効率的に再利用されるようになります。
newBufioWriterSize(w io.Writer, size int) 関数の導入
この関数は、指定された io.Writer と size を持つ bufio.Writer と switchWriter のペアを返します。
select ステートメントを使用して、まず bufioWriterCache(size) から既存の bufioWriterPair を取得しようとします。成功した場合、取得したペアの switchWriter の基盤となる Writer を新しい w に設定し直して返します。
キャッシュに利用可能なオブジェクトがない場合(default ブロック)、新しく switchWriter と bufio.NewWriterSize(sw, size) を作成して返します。
この関数は、bufio.Writer の生成コストを削減し、GCの負担を軽減するための中心的な役割を担っています。
putBufioWriter(bw *bufio.Writer, sw *switchWriter) でのキャッシュへの返却ロジックの変更
この関数は、使用済みの bufio.Writer と switchWriter のペアをキャッシュに戻す役割を担います。
重要な変更点は、select ステートメントの case 部分が bufioWriterCache <- bufioWriterPair{bw, sw} から bufioWriterCache(bw.Available()) <- bufioWriterPair{bw, sw} に変更されたことです。
bw.Available() は bufio.Writer のバッファの空き容量を返しますが、これは実質的にその bufio.Writer が初期化された際のバッファサイズを示します。これにより、putBufioWriter は、bufio.Writer が作成された際のサイズに基づいて、適切なサイズのキャッシュ(2KBまたは4KB)にオブジェクトを戻すことができます。これにより、プール内のオブジェクトがサイズ別に適切に管理され、再利用の効率が最大化されます。
conn.readRequest() での response 初期化時の変更
conn.readRequest() は、新しいHTTPリクエストを読み込み、それに対応する response オブジェクトを準備します。
以前は w.w = bufio.NewWriterSize(w.cw, bufferBeforeChunkingSize) で bufio.Writer を直接作成していましたが、この変更により w.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize) となりました。
これは、response オブジェクトの w フィールドと新しく追加された sw フィールドの両方に、プールから取得または新しく作成された bufio.Writer と switchWriter のペアを割り当てることを意味します。bufferBeforeChunkingSize は、チャンクエンコーディングを行う前にバッファリングするサイズで、通常は2KBです。
response.finishRequest() での putBufioWriter(w.w, w.sw) の呼び出し追加
response.finishRequest() は、HTTPレスポンスの送信が完了した後に呼び出されます。
以前は w.w.Flush() でバッファをフラッシュするだけでしたが、この変更により putBufioWriter(w.w, w.sw) が追加されました。
これは、使用済みの bufio.Writer と switchWriter のペアをキャッシュに戻すための重要なステップです。これにより、これらのオブジェクトがGCの対象となる代わりに、プールに戻され、次のリクエストで再利用される準備が整います。この明示的な返却が、メモリ割り当てとGC負荷の削減に直接貢献します。
これらの変更は全体として、net/http サーバーが bufio.Writer をより効率的に管理し、オブジェクトの再利用を最大化することで、メモリフットプリントとGCオーバーヘッドを削減し、結果としてサーバーのパフォーマンスを向上させることを目的としています。
関連リンク
- https://golang.org/cl/8038047 (Go Gerrit Code Review)
- Issue 5100: net/http: server optimization; reduce GCs, generate ~half the garbage (GitHub Issue)
参考にした情報源リンク
- Go言語の公式ドキュメント:
bufioパッケージ,net/httpパッケージ - Go言語のガベージコレクションに関する一般的な情報源
- Go言語のプロファイリングツール (
pprof) に関する情報源