[インデックス 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
) に関する情報源