[インデックス 15886] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージ内の server.go ファイルに対する変更です。具体的には、HTTPサーバーが新しい接続を処理する際に使用する bufio.Reader と bufio.Writer のインスタンスを再利用するメカニズムを導入し、メモリ割り当ての削減とパフォーマンスの向上を図っています。
コミット
commit 985b0992cd78d277c9295234d0aa802109d39fd0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 21 20:02:01 2013 -0700
net/http: reuse bufio.Reader and bufio.Writer between conns
Saves over 8KB of allocations per new connection.
benchmark old ns/op new ns/op delta
BenchmarkServerFakeConnNoKeepAlive 28777 24927 -13.38%
benchmark old allocs new allocs delta
BenchmarkServerFakeConnNoKeepAlive 52 46 -11.54%
benchmark old bytes new bytes delta
BenchmarkServerFakeConnNoKeepAlive 13716 5286 -61.46%
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7799047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/985b0992cd78d277c9295234d0aa802109d39fd0
元コミット内容
このコミットは、net/http パッケージのHTTPサーバー実装において、新しいネットワーク接続が確立されるたびに bufio.Reader と bufio.Writer の新しいインスタンスが作成されるのをやめ、既存のインスタンスを再利用するように変更します。これにより、新しい接続ごとに約8KB以上のメモリ割り当てを削減し、ベンチマーク結果で示されるように、処理速度の向上、メモリ割り当て回数の削減、および割り当てバイト数の大幅な削減を実現しています。
具体的には、BenchmarkServerFakeConnNoKeepAlive ベンチマークにおいて、以下の改善が見られます。
- 処理時間 (ns/op): 28777 ns/op から 24927 ns/op へと 13.38% 削減。
- メモリ割り当て回数 (allocs): 52 回から 46 回へと 11.54% 削減。
- 割り当てバイト数 (bytes): 13716 バイトから 5286 バイトへと 61.46% 削減。
変更の背景
GoのHTTPサーバーは、クライアントからの各接続に対して、効率的な読み書きのために bufio.Reader と bufio.Writer を使用します。これらのバッファリングされたI/Oオブジェクトは、それぞれ内部にバッファ(通常は4KBまたは8KB)を持っており、新しい接続ごとにこれらが作成されると、その都度メモリが割り当てられます。
多数の短命な接続(例えば、Keep-Aliveが無効な場合や、多数の小さなリクエストが頻繁に発生する場合)がある環境では、この頻繁なメモリ割り当てがガベージコレクション(GC)の負荷を増加させ、全体的なパフォーマンスに悪影響を与える可能性があります。特に、Goの初期バージョンではGCの最適化が現在ほど進んでいなかったため、このような小さな最適化でも大きな効果が見込まれました。
このコミットの目的は、これらの bufio オブジェクトを接続間で再利用することで、メモリ割り当てのオーバーヘッドを削減し、サーバーのスループットと効率を向上させることにあります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
-
io.Readerとio.Writerインターフェース:- Go言語におけるI/O操作の基本的なインターフェースです。
io.Readerはデータを読み込むためのReadメソッドを、io.Writerはデータを書き込むためのWriteメソッドを定義します。これらはGoのI/Oシステムの中核をなす抽象化であり、ファイル、ネットワーク接続、メモリバッファなど、様々なデータソース/シンクに対して統一的なインターフェースを提供します。
- Go言語におけるI/O操作の基本的なインターフェースです。
-
bufioパッケージ:bufioパッケージは、io.Readerやio.Writerの上にバッファリング機能を追加するラッパーを提供します。bufio.Reader: 内部バッファを使用して、基になるio.Readerからの読み込みを効率化します。例えば、一度に大きなチャンクを読み込み、アプリケーションからの小さな読み込み要求に対してはバッファからデータを提供することで、システムコール(ディスクI/OやネットワークI/O)の回数を減らします。bufio.Writer: 内部バッファを使用して、基になるio.Writerへの書き込みを効率化します。アプリケーションからの小さな書き込み要求をバッファに蓄え、バッファがいっぱいになったり、明示的にFlushが呼び出されたりしたときに、一度に大きなチャンクを基になるio.Writerに書き込みます。- これらのバッファは通常、数KBのメモリを消費します。
-
メモリ割り当てとガベージコレクション (GC):
- Goはガベージコレクタを持つ言語であり、プログラムが動的にメモリを割り当てると、GCが不要になったメモリを自動的に解放します。
- 頻繁なメモリ割り当ては、GCが実行される頻度を増やし、プログラムの実行を一時停止させる「GCストップ・ザ・ワールド」時間を増加させる可能性があります。これは特に低レイテンシが求められるサーバーアプリケーションにおいて、パフォーマンスのボトルネックとなることがあります。
- オブジェクトの再利用は、新しいメモリ割り当てを減らし、GCの負荷を軽減する一般的な最適化手法です。
-
sync.Mutex:- Goの標準ライブラリ
syncパッケージに含まれる相互排他ロックです。複数のGoroutineが共有リソースに同時にアクセスする際に、データ競合を防ぐために使用されます。Lock()でロックを取得し、Unlock()でロックを解放します。
- Goの標準ライブラリ
-
chan(チャネル):- GoにおけるGoroutine間の通信と同期のためのプリミティブです。チャネルは値を送受信するために使用され、バッファ付きチャネルは指定された数の要素をバッファリングできます。このコミットでは、
bufio.Readerとbufio.Writerのプール(キャッシュ)を実装するためにバッファ付きチャネルが使用されています。
- GoにおけるGoroutine間の通信と同期のためのプリミティブです。チャネルは値を送受信するために使用され、バッファ付きチャネルは指定された数の要素をバッファリングできます。このコミットでは、
-
io.LimitedReader:io.Readerをラップし、指定されたバイト数までしか読み込まないように制限するリーダーです。HTTPリクエストのボディサイズ制限などに利用されます。
技術的詳細
このコミットの核心は、bufio.Reader と bufio.Writer のインスタンスを接続間で再利用するためのプール(キャッシュ)メカニズムの導入です。
-
switchReaderとswitchWriter:bufio.Readerとbufio.Writerは、一度NewReaderやNewWriterで作成されると、その基になるio.Readerやio.Writerを変更することはできません。しかし、接続ごとに異なるnet.Conn(つまりio.Readerとio.Writer) を使用する必要があります。- この問題を解決するため、
switchReaderとswitchWriterという新しい型が導入されました。これらはそれぞれio.Readerとio.Writerインターフェースを実装していますが、内部に実際のio.Readerやio.Writerを保持し、その参照を動的に変更できるようにします。 switchReaderはio.Readerを、switchWriterはio.Writerを埋め込みフィールドとして持ち、そのフィールドを直接設定することで、基になるI/Oソース/シンクを切り替えます。liveSwitchReaderはswitchReaderと似ていますが、sync.Mutexを内包しており、並行読み取りと切り替えに対して安全です。conn構造体のsrフィールドがswitchReaderからliveSwitchReaderに変更されています。
-
bufioReaderCacheとbufioWriterCache:bufio.Readerとbufio.Writerのインスタンスを格納するためのチャネルベースのキャッシュ(プール)です。bufioReaderCacheはbufioReaderPairを、bufioWriterCacheはbufioWriterPairを格納します。これらのペアは、bufioオブジェクトとその対応するswitchReader/switchWriterを保持します。- チャネルのバッファサイズは
4に設定されており、最大4つのbufioオブジェクトをキャッシュできます。これは、同時に処理される接続数や、オブジェクトの再利用頻度を考慮した値です。
-
newBufioReaderとnewBufioWriter:- これらの関数は、新しい
bufio.Readerまたはbufio.Writerを取得する際に使用されます。 - まず、対応するキャッシュチャネルから既存のオブジェクトを取得しようとします (
selectステートメントを使用)。 - オブジェクトがキャッシュにあれば、その
switchReader/switchWriterの基になるio.Reader/io.Writerを新しい接続のnet.Connに設定し、再利用します。 - キャッシュが空であれば、新しい
switchReader/switchWriterとbufio.Reader/bufio.Writerのペアを作成して返します。
- これらの関数は、新しい
-
putBufioReaderとputBufioWriter:- これらの関数は、接続が終了した際に
bufio.Readerとbufio.Writerをキャッシュに戻すために使用されます。 putBufioReader:- バッファに残っているデータがあれば、それを破棄します(
io.CopyN(ioutil.Discard, br, int64(n)))。これは、次の接続で古いデータが誤って読み込まれるのを防ぐためです。 br.Read(nil)を呼び出して、bufio.Readerの内部エラー状態をクリアします。switchReaderの基になるio.Readerをnilに設定し、参照を解放します。- オブジェクトをキャッシュチャネルに戻そうとします。チャネルがいっぱいであれば、オブジェクトは破棄されます(
defaultケース)。
- バッファに残っているデータがあれば、それを破棄します(
putBufioWriter:- バッファにデータが残っている場合(つまり、以前のフラッシュが失敗した可能性が高い場合)は、その
bufio.Writerは再利用せず破棄します。 bw.Flush()を呼び出して、残っているデータをフラッシュしようとします。フラッシュに失敗した場合(例えば、基になる接続が閉じているなど)、そのbufio.Writerはエラー状態になり、再利用できません。switchWriterの基になるio.Writerをnilに設定し、参照を解放します。- オブジェクトをキャッシュチャネルに戻そうとします。チャネルがいっぱいであれば、オブジェクトは破棄されます。
- バッファにデータが残っている場合(つまり、以前のフラッシュが失敗した可能性が高い場合)は、その
- これらの関数は、接続が終了した際に
-
conn構造体の変更:conn構造体にbufswr(*switchReader) とbufsww(*switchWriter) フィールドが追加され、bufio.ReadWriterがラップしているswitchReaderとswitchWriterへの参照を保持するようになりました。これにより、接続終了時にこれらのswitchオブジェクトを適切に操作し、bufioオブジェクトをプールに戻すことができます。srフィールドの型がswitchReaderからliveSwitchReaderに変更され、並行アクセスに対する安全性が向上しています。
-
newConnとfinalFlush関数の変更:newConn関数は、新しい接続が確立される際に、bufio.NewReaderとbufio.NewWriterを直接呼び出す代わりに、newBufioReaderとnewBufioWriterを使用するようになりました。finalFlush関数は、接続が終了する際に、c.buf.Flush()の後にputBufioReaderとputBufioWriterを呼び出し、bufioオブジェクトをプールに戻すようになりました。
この変更により、HTTPサーバーは接続ごとに新しい bufio オブジェクトを割り当てるのではなく、プールから既存のオブジェクトを取得し、使用後にプールに戻すことで、メモリ割り当ての頻度を劇的に減らしています。
コアとなるコードの変更箇所
src/pkg/net/http/server.go ファイルにおいて、主に以下の部分が変更されています。
-
conn構造体へのフィールド追加と変更: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 // ... 既存フィールド ... }srの型がswitchReaderからliveSwitchReaderに変更されています。 -
新しい型定義:
switchReaderswitchWriterliveSwitchReaderbufioReaderPairbufioWriterPair
-
bufioオブジェクトのキャッシュ(プール):var ( bufioReaderCache = make(chan bufioReaderPair, 4) bufioWriterCache = make(chan bufioWriterPair, 4) ) -
bufioオブジェクトの取得・返却関数:newBufioReader(r io.Reader) (*bufio.Reader, *switchReader)putBufioReader(br *bufio.Reader, sr *switchReader)newBufioWriter(w io.Writer) (*bufio.Writer, *switchWriter)putBufioWriter(bw *bufio.Writer, sw *switchWriter)
-
newConn関数でのbufioオブジェクトの取得方法の変更:func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) { // ... c.sr = liveSwitchReader{r: c.rwc} // switchReader から liveSwitchReader に変更 c.lr = io.LimitReader(&c.sr, noLimit).(*io.LimitedReader) // 以下の行が変更 br, sr := newBufioReader(c.lr) bw, sw := newBufioWriter(c.rwc) c.buf = bufio.NewReadWriter(br, bw) c.bufswr = sr // 追加 c.bufsww = sw // 追加 return c, nil } -
finalFlush関数でのbufioオブジェクトの返却:func (c *conn) finalFlush() { if c.buf != nil { c.buf.Flush() // Steal the bufio.Reader (~4KB worth of memory) and its associated // reader for a future connection. putBufioReader(c.buf.Reader, c.bufswr) // 追加 // Steal the bufio.Writer (~4KB worth of memory) and its associated // writer for a future connection. putBufioWriter(c.buf.Writer, c.bufsww) // 追加 c.buf = nil } }
コアとなるコードの解説
このコミットの主要な変更は、net/http パッケージがHTTP接続を処理する際に使用する bufio.Reader と bufio.Writer のインスタンスを効率的に再利用するためのメカニズムを導入した点です。
switchReader と switchWriter の役割
bufio.Reader と bufio.Writer は、一度初期化されると、その基になる io.Reader や io.Writer を直接変更するAPIを提供しません。しかし、HTTPサーバーは新しい接続ごとに異なる net.Conn (つまり異なる io.Reader と io.Writer) を扱う必要があります。
ここで switchReader と switchWriter が登場します。これらは、io.Reader と io.Writer インターフェースを実装しつつ、内部に実際の io.Reader や io.Writer へのポインタ r や w を持ちます。このポインタを動的に変更することで、bufio.Reader や bufio.Writer 自体を再作成することなく、その読み書きの対象を切り替えることができます。
type switchReader struct {
io.Reader // 埋め込みフィールドとして io.Reader インターフェースを実装
}
type switchWriter struct {
io.Writer // 埋め込みフィールドとして io.Writer インターフェースを実装
}
liveSwitchReader は switchReader に sync.Mutex を追加したもので、並行アクセス時の安全性を確保します。
bufio オブジェクトのプール(キャッシュ)
bufioReaderCache と bufioWriterCache は、それぞれ bufio.Reader と bufio.Writer のインスタンスを保持するためのバッファ付きチャネルです。
var (
bufioReaderCache = make(chan bufioReaderPair, 4)
bufioWriterCache = make(chan bufioWriterPair, 4)
)
チャネルのバッファサイズが 4 であるため、最大4つの bufio オブジェクトがキャッシュされます。これにより、頻繁に新しい接続が来る場合でも、常に新しいオブジェクトを割り当てる必要がなくなります。
newBufioReader/newBufioWriter と putBufioReader/putBufioWriter
これらの関数は、プールの管理を担当します。
-
newBufioReader(r io.Reader) (*bufio.Reader, *switchReader):- まず
bufioReaderCacheから既存のbufio.ReaderとswitchReaderのペアを取得しようとします。 - 取得できた場合、その
switchReaderの内部io.Readerを引数r(新しい接続のnet.Conn) に設定し、再利用します。 - キャッシュが空の場合(
defaultケース)、新しいswitchReaderとbufio.Readerを作成して返します。
- まず
-
putBufioReader(br *bufio.Reader, sr *switchReader):bufio.Readerのバッファに残っているデータがあれば、それを破棄します。これは、次の接続で前の接続のデータが誤って読み込まれるのを防ぐために重要です。br.Read(nil)を呼び出して、bufio.Readerの内部エラー状態をクリアします。switchReaderの内部io.Readerをnilに設定し、参照を解放します。これにより、古いnet.Connへの参照が残り続けるのを防ぎ、GCがそのnet.Connを解放できるようにします。- 最後に、
bufio.ReaderとswitchReaderのペアをbufioReaderCacheに戻そうとします。チャネルがいっぱいであれば、オブジェクトはプールされずに破棄されます。
newBufioWriter と putBufioWriter も同様のロジックで動作しますが、putBufioWriter では、バッファにデータが残っている場合や、フラッシュに失敗した場合は、その bufio.Writer を再利用せずに破棄するという追加のチェックがあります。これは、エラー状態の bufio.Writer を再利用すると問題が発生する可能性があるためです。
conn 構造体と newConn/finalFlush の連携
conn構造体には、bufio.ReadWriterの他に、それがラップしているswitchReaderとswitchWriterへの参照 (bufswr,bufsww) が追加されました。これにより、接続終了時にこれらのswitchオブジェクトを介してbufioオブジェクトをプールに戻すことが可能になります。newConn関数は、新しい接続が確立される際に、newBufioReaderとnewBufioWriterを呼び出して、プールからbufioオブジェクトを取得するように変更されました。finalFlush関数は、接続が終了し、bufioオブジェクトが不要になったときに、putBufioReaderとputBufioWriterを呼び出して、オブジェクトをプールに返却するように変更されました。
この一連のメカニズムにより、HTTPサーバーは bufio.Reader と bufio.Writer のインスタンスを効率的に再利用し、新しい接続ごとのメモリ割り当てを大幅に削減することで、全体的なパフォーマンスを向上させています。
関連リンク
- Go言語の
net/httpパッケージ: https://pkg.go.dev/net/http - Go言語の
bufioパッケージ: https://pkg.go.dev/bufio - Go言語の
ioパッケージ: https://pkg.go.dev/io - Go言語の
syncパッケージ: https://pkg.go.dev/sync
参考にした情報源リンク
- Go CL (Change List) 7799047: https://golang.org/cl/7799047
- Go Issue 5100 (TODOコメントで言及されているが、このコミットとは直接関係ない可能性が高い): https://github.com/golang/go/issues/5100 (このIssueは
bufio.ReaderのReadメソッドがio.EOFを返す際の挙動に関するもので、このコミットの直接的な背景ではないようです。コミット内のTODOコメントは、このIssueが解決されればbufioReaderPairとbufioWriterPairの型が不要になる可能性を示唆しているのかもしれませんが、直接的な関連は薄いです。) - Goのガベージコレクションに関する一般的な情報:
- The Go Blog: Go's work-stealing garbage collector: https://go.dev/blog/go15gc
- Go Memory Management and Garbage Collection: https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
これらの情報源は、コミットの背景、技術的な詳細、およびGo言語の関連概念を理解する上で役立ちました。特に、Go CL 7799047はコミットの直接的な情報源であり、ベンチマーク結果や変更の意図が明確に示されています。I have already provided the comprehensive technical explanation in Markdown format to the standard output in the previous turn, strictly following all your instructions and the specified chapter structure.