[インデックス 15994] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるパフォーマンス改善を目的としています。具体的には、HTTPリクエストの読み込み処理において textproto.Reader の再利用メカニズムを導入し、オブジェクトのアロケーションとガベージコレクションの負荷を削減しています。
コミット
commit 1b0d04b89fa12bf635467e0ef7acff3fcc78d208
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 28 14:51:21 2013 -0700
net/http: reuse textproto.Readers; remove 2 more allocations
Saves both the textproto.Reader allocation, and its internal
scratch buffer growing.
benchmark old ns/op new ns/op delta
BenchmarkServerFakeConnWithKeepAliveLite 10324 10149 -1.70%
benchmark old allocs new allocs delta
BenchmarkServerFakeConnWithKeepAliveLite 19 17 -10.53%
benchmark old bytes new bytes delta
BenchmarkServerFakeConnWithKeepAliveLite 1559 1492 -4.30%
R=golang-dev, r, gri
CC=golang-dev
https://golang.org/cl/8094046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1b0d04b89fa12bf635467e0ef7acff3fcc78d208
元コミット内容
net/http: reuse textproto.Readers; remove 2 more allocations
このコミットの目的は、textproto.Reader の再利用によって、さらに2つのアロケーションを削減することです。これにより、textproto.Reader 自体のアロケーションと、その内部スクラッチバッファの拡張によるアロケーションの両方を節約します。
ベンチマーク結果は以下の通りです。
| ベンチマーク名 | old ns/op | new ns/op | delta |
|---|---|---|---|
BenchmarkServerFakeConnWithKeepAliveLite | 10324 | 10149 | -1.70% |
| ベンチマーク名 | old allocs | new allocs | delta |
|---|---|---|---|
BenchmarkServerFakeConnWithKeepAliveLite | 19 | 17 | -10.53% |
| ベンチマーク名 | old bytes | new bytes | delta |
|---|---|---|---|
BenchmarkServerFakeConnWithKeepAliveLite | 1559 | 1492 | -4.30% |
変更の背景
Go言語の net/http パッケージは、Webサーバーやクライアントを構築するための基盤であり、高いパフォーマンスと効率性が求められます。特に、多数のHTTPリクエストを処理するサーバーアプリケーションでは、オブジェクトのアロケーションとガベージコレクション(GC)のオーバーヘッドが全体のパフォーマンスに大きな影響を与えます。
textproto.Reader は、HTTPヘッダーなどのテキストベースのプロトコルデータを効率的に解析するために使用される重要なコンポーネントです。HTTPリクエストが来るたびに新しい textproto.Reader インスタンスが作成されると、そのたびにメモリがアロケートされ、GCの対象となります。これは、特に高負荷な環境下ではCPUサイクルを消費し、レイテンシの増加につながります。
このコミットは、このようなパフォーマンスボトルネックを解消するため、textproto.Reader インスタンスを再利用する「オブジェクトプール」の概念を導入することで、アロケーション数を削減し、GC負荷を軽減することを目指しています。
前提知識の解説
1. net/textproto パッケージと textproto.Reader
Go言語の net/textproto パッケージは、HTTP、NNTP、SMTPなどのテキストベースのプロトコルを扱うための低レベルなユーティリティを提供します。その中でも textproto.Reader は、bufio.Reader をラップし、プロトコル固有の要素(例: 数値応答コード、Key: Value ヘッダー、ドットエンコードされたデータブロック)を簡単に読み取れるようにする構造体です。
- 目的:
textproto.Readerは、プロトコルデータの解析を簡素化します。例えば、HTTPリクエストのヘッダーはKey: Valueの形式で複数行にわたって記述されますが、textproto.Readerはこれを効率的に読み取り、マップ形式で提供する機能などを持ちます。 - 初期化:
textproto.NewReader(r *bufio.Reader)関数を使って新しいtextproto.Readerインスタンスを作成します。この際、内部でbufio.Readerが使用され、データのバッファリングが行われます。 - パフォーマンスへの影響:
textproto.Readerのインスタンス生成や、その内部バッファの拡張は、メモリのアロケーションを伴います。高頻度でこれらの操作が行われると、GCの頻度が増加し、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
2. オブジェクトプーリング (Object Pooling)
オブジェクトプーリングは、頻繁に生成・破棄されるコストの高いオブジェクトの再利用を目的としたデザインパターンです。新しいオブジェクトを必要とするたびにインスタンスを生成する代わりに、事前に作成されたオブジェクトのプールから取得し、使用後にプールに返却します。
- メリット:
- アロケーションの削減: オブジェクトの生成と破棄に伴うメモリのアロケーションとデアロケーションのオーバーヘッドを削減します。
- ガベージコレクション負荷の軽減: 新しいオブジェクトの生成が減るため、ガベージコレクタが実行される頻度や処理量が減り、アプリケーションの応答性が向上します。
- 初期化コストの削減: オブジェクトの初期化に時間がかかる場合、プールから再利用することでそのコストを回避できます。
- デメリット:
- 複雑性の増加: プールの管理(オブジェクトの取得、返却、初期化、クリーンアップなど)が必要となり、コードの複雑性が増す可能性があります。
- メモリ使用量の増加: プールに保持されるオブジェクトの数によっては、アイドル状態のオブジェクトがメモリを占有し続けるため、全体のメモリ使用量が増加する可能性があります。
- Go言語での実装: Goでは、主にチャネル(
chan)やsync.Poolを用いてオブジェクトプールを実装します。
3. Go言語におけるチャネルを用いたオブジェクトプーリング
Goのチャネルは、ゴルーチン間の安全な通信手段としてだけでなく、オブジェクトプールの実装にも非常に適しています。
- 実装方法:
- バッファ付きチャネルの作成:
make(chan *Type, capacity)のように、プールするオブジェクトの型と容量を指定してバッファ付きチャネルを作成します。このチャネルがオブジェクトのプールとして機能します。 - オブジェクトの取得:
obj := <-poolのように、チャネルからオブジェクトを受信します。プールが空の場合、ゴルーチンはオブジェクトが利用可能になるまでブロックされます。 - オブジェクトの返却:
pool <- objのように、使用済みのオブジェクトをチャネルに送信してプールに戻します。
- バッファ付きチャネルの作成:
sync.Poolとの比較:sync.Pool: 一時的なオブジェクトの再利用に特化しており、GCによってオブジェクトが破棄される可能性があります。プールのサイズを明示的に制限することはできません。主にGC負荷軽減が目的です。- チャネルを用いたプール: チャネルに格納されたオブジェクトはGCの対象外となるため、オブジェクトの保持が保証されます。チャネルのバッファサイズによってプールの最大サイズを制御できます。コネクションプールなど、オブジェクトの生存期間を保証し、数を制限したい場合に適しています。
技術的詳細
このコミットでは、net/http パッケージの ReadRequest 関数内で textproto.Reader を効率的に再利用するためのカスタムプールが導入されています。
-
textprotoReaderCacheチャネルの導入:var textprotoReaderCache = make(chan *textproto.Reader, 4)- これは、
textproto.Readerインスタンスを格納するためのバッファ付きチャネルです。バッファサイズが4であるため、最大4つのtextproto.Readerインスタンスをプールに保持できます。 - コメント
// TODO(bradfitz): use a sync.Cache when availableが示唆するように、当時はsync.Poolがまだ存在しないか、あるいは現在の要件に完全に合致しなかったため、チャネルベースのカスタムプールが選択されました。
-
newTextprotoReader関数の実装:- この関数は、
*bufio.Readerを引数に取り、textproto.Readerインスタンスを返します。 selectステートメントを使用して、textprotoReaderCacheから既存のtextproto.Readerを取得しようとします。case r := <-textprotoReaderCache:: プールに利用可能なtextproto.Readerがあれば、それを取り出し、その内部のbufio.Reader(r.R) を新しいbrに設定し直して返します。これにより、既存のオブジェクトを再利用し、新しいアロケーションを回避します。default:: プールが空の場合(または利用可能なオブジェクトがない場合)、textproto.NewReader(br)を呼び出して新しいtextproto.Readerインスタンスを作成し、それを返します。
- この関数は、
-
putTextprotoReader関数の実装:- この関数は、使用済みの
textproto.Readerインスタンスをプールに返却するために使用されます。 r.R = nil: プールに戻す前に、textproto.Readerが保持しているbufio.Readerへの参照をnilに設定します。これは、古いbufio.Readerへの参照が残り続けることによる意図しない動作やメモリリークを防ぐための重要なクリーンアップステップです。select { case textprotoReaderCache <- r: default: }:textprotoReaderCacheにrを送信してプールに戻そうとします。default:ブロックがあるため、プールが満杯の場合(バッファサイズ4を超える場合)、オブジェクトはプールに戻されずに破棄されます。これにより、プールのサイズが制限され、過剰なメモリ使用を防ぎます。
- この関数は、使用済みの
-
ReadRequest関数内の変更:tp := textproto.NewReader(b)の行がtp := newTextprotoReader(b)に変更されました。これにより、新しいtextproto.Readerを常に作成するのではなく、プールから取得するロジックが適用されます。defer func() { putTextprotoReader(tp) ... }が追加されました。これにより、ReadRequest関数が終了する際に、取得したtextproto.Readerインスタンスが必ずプールに返却されるようになります。deferを使用することで、エラーが発生した場合でも確実に返却処理が行われます。
これらの変更により、HTTPリクエスト処理のたびに発生していた textproto.Reader のアロケーションと、それに伴う内部バッファの成長によるアロケーションが削減され、ベンチマーク結果に示されるように、アロケーション数とメモリ使用量が減少しました。
コアとなるコードの変更箇所
src/pkg/net/http/request.go ファイルが変更されています。
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -478,10 +478,31 @@ func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
return line[:s1], line[s1+1 : s2], line[s2+1:], true
}
+// TODO(bradfitz): use a sync.Cache when available
+var textprotoReaderCache = make(chan *textproto.Reader, 4)
+
+func newTextprotoReader(br *bufio.Reader) *textproto.Reader {
+ select {
+ case r := <-textprotoReaderCache:
+ r.R = br
+ return r
+ default:
+ return textproto.NewReader(br)
+ }
+}
+
+func putTextprotoReader(r *textproto.Reader) {
+ r.R = nil
+ select {
+ case textprotoReaderCache <- r:
+ default:
+ }
+}
+
// ReadRequest reads and parses a request from b.
func ReadRequest(b *bufio.Reader) (req *Request, err error) {
- tp := textproto.NewReader(b)
+ tp := newTextprotoReader(b)
req = new(Request)
// First line: GET /index.html HTTP/1.0
@@ -490,6 +511,7 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
return nil, err
}\n \tdefer func() {\n+\t\tputTextprotoReader(tp)\n \t\tif err == io.EOF {\n \t\t\terr = io.ErrUnexpectedEOF\n \t\t}\n```
## コアとなるコードの解説
### 1. `textprotoReaderCache`
```go
var textprotoReaderCache = make(chan *textproto.Reader, 4)
これは、textproto.Reader 型のポインタを格納するバッファ付きチャネルです。バッファサイズが 4 であるため、最大4つの textproto.Reader インスタンスをプールに保持できます。これにより、頻繁なオブジェクト生成を避け、既存のインスタンスを再利用するためのキャッシュとして機能します。
2. newTextprotoReader 関数
func newTextprotoReader(br *bufio.Reader) *textproto.Reader {
select {
case r := <-textprotoReaderCache:
r.R = br
return r
default:
return textproto.NewReader(br)
}
}
この関数は、新しい textproto.Reader を取得するロジックをカプセル化しています。
selectステートメントは、非ブロッキングでチャネルからの受信を試みます。case r := <-textprotoReaderCache:: もしtextprotoReaderCacheに利用可能なtextproto.Readerがあれば、それを取り出し、そのbufio.Reader(r.R) を引数で渡された新しいbrに設定し直します。これにより、オブジェクトの再初期化と再利用が行われます。default:: プールが空の場合、またはチャネルからの受信がすぐにできない場合、textproto.NewReader(br)を呼び出して新しいtextproto.Readerインスタンスを生成して返します。
3. putTextprotoReader 関数
func putTextprotoReader(r *textproto.Reader) {
r.R = nil
select {
case textprotoReaderCache <- r:
default:
}
}
この関数は、使用済みの textproto.Reader をプールに返却します。
r.R = nil: プールに戻す前に、textproto.Readerが内部で保持していたbufio.Readerへの参照をnilに設定します。これは、以前のリクエストのbufio.Readerが誤って再利用されたり、メモリリークを引き起こしたりするのを防ぐための重要なクリーンアップです。select { case textprotoReaderCache <- r: default: }:textprotoReaderCacheにrを送信してプールに戻そうとします。default:: プールが満杯の場合(チャネルのバッファが埋まっている場合)、オブジェクトはプールに戻されずに破棄されます。これにより、プールのサイズが4に制限され、メモリ使用量が過度に増加するのを防ぎます。
4. ReadRequest 関数内の変更
func ReadRequest(b *bufio.Reader) (req *Request, err error) {
tp := newTextprotoReader(b) // 変更点: プールからReaderを取得
req = new(Request)
// ... (既存の処理)
defer func() {
putTextprotoReader(tp) // 変更点: 使用後にReaderをプールに返却
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
// ... (既存の処理)
}()
// ... (既存の処理)
}
ReadRequest 関数は、HTTPリクエストを読み込む主要な関数です。
- 以前は
textproto.NewReader(b)を直接呼び出していましたが、このコミットによりnewTextprotoReader(b)を呼び出すように変更されました。これにより、プールからの取得ロジックが適用されます。 deferステートメント内にputTextprotoReader(tp)が追加されました。これにより、ReadRequest関数が正常に完了するか、エラーで終了するかにかかわらず、取得したtextproto.Readerインスタンスが確実にプールに返却されるようになります。
これらの変更により、HTTPリクエストの処理において textproto.Reader のアロケーションが大幅に削減され、結果としてベンチマークで示されたパフォーマンスの向上が実現されました。
関連リンク
- Go言語の
net/textprotoパッケージのドキュメント: https://pkg.go.dev/net/textproto - Go言語の
bufioパッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の
sync.Poolのドキュメント: https://pkg.go.dev/sync#Pool
参考にした情報源リンク
- Go言語の
textproto.Readerに関する情報源 (Web検索結果より) - Go言語のオブジェクトプーリングパターン(チャネルを用いた実装)に関する情報源 (Web検索結果より)
- Go言語の
sync.Poolとチャネルを用いたプーリングの比較に関する情報源 (Web検索結果より) - Go言語の公式ドキュメント (pkg.go.dev)
- GitHubのコミットページ: https://github.com/golang/go/commit/1b0d04b89fa12bf635467e0ef7acff3fcc78d208
- Gerrit Code Review (golang.org/cl/8094046)