[インデックス 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)