[インデックス 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
に変更されています。 -
新しい型定義:
switchReader
switchWriter
liveSwitchReader
bufioReaderPair
bufioWriterPair
-
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.