[インデックス 18071] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるパフォーマンス改善を目的としています。具体的には、HTTPヘッダーのソート、テキストプロトコルリーダー、およびバッファリングされたリーダー/ライターといった、頻繁に生成・破棄されるオブジェクトの再利用メカニズムを、従来のチャネルベースのキャッシュから sync.Pool
を使用したオブジェクトプールへと変更しています。これにより、ガベージコレクションの負荷を軽減し、全体的なHTTPサーバーのパフォーマンス向上を図っています。
コミット
commit 93e4a9d84c4d7c955c953523f5c88db7b46405a4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Dec 18 15:52:20 2013 -0800
net/http: use sync.Pool
Update #4720
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/44080043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/93e4a9d84c4d7c955c953523f5c88db7b46405a4
元コミット内容
このコミットの元のメッセージは以下の通りです。
net/http: use sync.Pool
Update #4720
これは、net/http
パッケージが sync.Pool
を利用するように変更されたことを示しており、GoのIssue 4720に関連する更新であることが明記されています。
変更の背景
この変更の背景には、GoのHTTPサーバーが大量のリクエストを処理する際に発生する、一時的なオブジェクトの頻繁な生成とガベージコレクション(GC)によるパフォーマンスオーバーヘッドがありました。特に、HTTPリクエストやレスポンスの処理中に、ヘッダーのソートに使用される headerSorter
、テキストプロトコルを読み取る textproto.Reader
、そしてI/Oバッファリングに使用される bufio.Reader
や bufio.Writer
といったオブジェクトが繰り返し作成・破棄されていました。
Goのガベージコレクタは非常に効率的ですが、それでも大量の一時オブジェクトの割り当てと解放は、GCサイクルを頻繁にトリガーし、アプリケーションの実行を一時停止させる可能性があります。これは特に低レイテンシが求められるHTTPサーバーのようなアプリケーションでは、スループットの低下やレイテンシの増加として現れます。
以前のGoのバージョンでは、これらのオブジェクトを再利用するために、固定サイズのチャネル(chan *T
)を用いた簡易的なオブジェクトプールが実装されていました。しかし、このチャネルベースのプールは、プールのサイズが固定であるため、トラフィックの急増に対応しきれなかったり、プールが枯渇した場合には新しいオブジェクトを割り当てる必要があったりといった限界がありました。また、チャネル操作自体にもオーバーヘッドが存在します。
Issue 4720は、このチャネルベースのキャッシュをより効率的な sync.Pool
に置き換えることを提案しており、このコミットはその提案を実装したものです。sync.Pool
は、一時的なオブジェクトの再利用に特化して設計されており、GCの負荷を軽減し、パフォーマンスを向上させることを目的としています。
前提知識の解説
ガベージコレクション (GC)
Go言語は自動メモリ管理(ガベージコレクション)を採用しています。プログラムが不要になったメモリ領域を自動的に解放し、再利用可能にする仕組みです。これにより、開発者は手動でのメモリ管理から解放されますが、GCが動作する際にはプログラムの実行が一時的に停止(ストップ・ザ・ワールド)することがあり、これがパフォーマンスに影響を与えることがあります。特に、大量の小さなオブジェクトが頻繁に生成・破棄されるようなワークロードでは、GCの頻度が増加し、アプリケーションのスループットやレイテンシに悪影響を及ぼす可能性があります。
オブジェクトプール
オブジェクトプールとは、頻繁に生成・破棄されるオブジェクトを再利用するためのデザインパターンです。オブジェクトを使い終わった後すぐに破棄するのではなく、プールに戻して再利用することで、オブジェクトの生成(メモリ割り当て)と破棄(GCによる解放)のコストを削減します。これにより、GCの頻度を減らし、アプリケーションのパフォーマンスを向上させることができます。
sync.Pool
sync.Pool
はGo言語の標準ライブラリ sync
パッケージで提供される、一時的なオブジェクトを効率的に再利用するためのメカニズムです。主な特徴は以下の通りです。
- 一時的なオブジェクトの再利用:
sync.Pool
は、GCによって回収される可能性のある一時的なオブジェクトをキャッシュするために設計されています。プール内のオブジェクトは、GCの際に回収される可能性があります。これは、プールがメモリ使用量を無制限に増加させないための重要な特性です。 Get()
とPut()
:Get()
メソッドはプールからオブジェクトを取得します。プールが空の場合、New
フィールドに設定された関数が呼び出され、新しいオブジェクトが生成されます。Put()
メソッドはオブジェクトをプールに戻します。
- スレッドセーフ: 複数のゴルーチンから安全にアクセスできるように設計されています。
- GCとの連携:
sync.Pool
はGCと連携して動作します。GCが実行されると、プール内のオブジェクトの一部または全てが回収される可能性があります。これは、プールがメモリを過剰に保持しないようにするためです。そのため、sync.Pool
に格納されるオブジェクトは、その状態がGCによって失われても問題ない、または簡単に再構築できるものであるべきです。
チャネルベースのオブジェクトキャッシュ (旧方式)
このコミット以前は、Goの net/http
パッケージでは、固定サイズのバッファ付きチャネル(例: make(chan *T, N)
)をオブジェクトプールとして利用していました。オブジェクトが必要なときにチャネルから取得し、使い終わったらチャネルに戻すというシンプルな仕組みです。
var myCache = make(chan *MyObject, 8)
func getMyObject() *MyObject {
select {
case obj := <-myCache:
return obj
default:
return new(MyObject) // プールが空なら新規作成
}
}
func putMyObject(obj *MyObject) {
select {
case myCache <- obj:
// プールに戻せた
default:
// プールが満杯なら破棄
}
}
この方式はシンプルですが、以下のような課題がありました。
- 固定サイズ: プールのサイズが固定であるため、トラフィックの変動に対応しにくい。
- GCとの連携: チャネル内のオブジェクトはGCの対象外となるため、メモリ使用量が増加する可能性がある。
- チャネル操作のオーバーヘッド: チャネルの送受信操作には、
sync.Pool
の内部実装と比較してわずかながらオーバーヘッドが存在する。
技術的詳細
このコミットの主要な技術的変更点は、net/http
パッケージ内で使用されていた複数のチャネルベースのオブジェクトキャッシュを、sync.Pool
インスタンスに置き換えたことです。
具体的には、以下のキャッシュが変更されました。
-
headerSorterCache
(src/pkg/net/http/header.go
):- HTTPヘッダーをソートする際に使用される
headerSorter
オブジェクトのプール。 - 変更前:
var headerSorterCache = make(chan *headerSorter, 8)
- 変更後:
var headerSorterPool = sync.Pool{ New: func() interface{} { return new(headerSorter) }, }
Get()
の呼び出しはhs = headerSorterPool.Get().(*headerSorter)
に、Put()
の呼び出しはheaderSorterPool.Put(sorter)
に変更されました。
- HTTPヘッダーをソートする際に使用される
-
textprotoReaderCache
(src/pkg/net/http/request.go
):- HTTPリクエストのテキストプロトコル部分(ヘッダーなど)を読み取る
textproto.Reader
オブジェクトのプール。 - 変更前:
var textprotoReaderCache = make(chan *textproto.Reader, 4)
- 変更後:
var textprotoReaderPool sync.Pool
(New関数はnewTextprotoReader
内で暗黙的に処理される) newTextprotoReader
関数内でtextprotoReaderPool.Get()
を使用し、putTextprotoReader
関数内でtextprotoReaderPool.Put(r)
を使用するように変更されました。
- HTTPリクエストのテキストプロトコル部分(ヘッダーなど)を読み取る
-
bufioReaderCache
,bufioWriterCache2k
,bufioWriterCache4k
(src/pkg/net/http/server.go
):- HTTPサーバーがクライアントとの通信に使用する
bufio.Reader
およびbufio.Writer
オブジェクトのプール。これらはそれぞれ異なるバッファサイズ(2KB, 4KB)を持つライターのために個別のチャネルを持っていました。 - 変更前:
var ( bufioReaderCache = make(chan *bufio.Reader, 4) bufioWriterCache2k = make(chan *bufio.Writer, 4) bufioWriterCache4k = make(chan *bufio.Writer, 4) )
- 変更後:
var ( bufioReaderPool sync.Pool bufioWriter2kPool sync.Pool bufioWriter4kPool sync.Pool )
- 関連する
newBufioReader
,putBufioReader
,newBufioWriterSize
,putBufioWriter
関数もsync.Pool
を利用するように変更されました。特にbufioWriterPool
ヘルパー関数が導入され、サイズに応じた適切なプールを返すようになりました。
- HTTPサーバーがクライアントとの通信に使用する
これらの変更により、オブジェクトの取得と返却のロジックが select
ステートメントによるチャネル操作から、sync.Pool
の Get()
と Put()
メソッドの呼び出しへと簡素化されました。sync.Pool
は内部的にCPUコアごとにローカルなキャッシュを持つことで、ロック競合を減らし、より効率的なオブジェクトの再利用を実現します。また、GCによってプール内のオブジェクトが回収される可能性があるため、メモリ使用量の管理もより適切に行われます。
コアとなるコードの変更箇所
src/pkg/net/http/header.go
--- a/src/pkg/net/http/header.go
+++ b/src/pkg/net/http/header.go
@@ -9,6 +9,7 @@ import (
"net/textproto"
"sort"
"strings"
+ "sync"
"time"
)
@@ -114,18 +115,15 @@ func (s *headerSorter) Len() int { return len(s.kvs) }
func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] }
func (s *headerSorter) Less(i, j int) bool { return s.kvs[i].key < s.kvs[j].key }
-// TODO: convert this to a sync.Cache (issue 4720)
-var headerSorterCache = make(chan *headerSorter, 8)
+var headerSorterPool = sync.Pool{
+ New: func() interface{} { return new(headerSorter) },
+}
// sortedKeyValues returns h's keys sorted in the returned kvs
// slice. The headerSorter used to sort is also returned, for possible
// return to headerSorterCache.
func (h Header) sortedKeyValues(exclude map[string]bool) (kvs []keyValues, hs *headerSorter) {
-\tselect {\n-\tcase hs = <-headerSorterCache:\n-\tdefault:\n-\t\ths = new(headerSorter)\n-\t}\n+\ths = headerSorterPool.Get().(*headerSorter)
if cap(hs.kvs) < len(h) {
hs.kvs = make([]keyValues, 0, len(h))
}
@@ -159,10 +157,7 @@ func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error {
}
}
}
-\tselect {\n-\tcase headerSorterCache <- sorter:\n-\tdefault:\n-\t}\n+\theaderSorterPool.Put(sorter)
return nil
}
src/pkg/net/http/request.go
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -20,6 +20,7 @@ import (
"net/url"
"strconv"
"strings"
+ "sync"
)
const (
@@ -494,25 +495,20 @@ 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)
+var textprotoReaderPool sync.Pool
func newTextprotoReader(br *bufio.Reader) *textproto.Reader {
-\tselect {\n-\tcase r := <-textprotoReaderCache:\n-\t\tr.R = br\n-\t\treturn r\n-\tdefault:\n-\t\treturn textproto.NewReader(br)\n+\tif v := textprotoReaderPool.Get(); v != nil {
+\t\ttr := v.(*textproto.Reader)
+\t\ttr.R = br
+\t\treturn tr
\t}\n+\treturn textproto.NewReader(br)
}
func putTextprotoReader(r *textproto.Reader) {
r.R = nil
-\tselect {\n-\tcase textprotoReaderCache <- r:\n-\tdefault:\n-\t}\n+\ttextprotoReaderPool.Put(r)
}
// ReadRequest reads and parses a request from b.
src/pkg/net/http/server.go
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -435,56 +435,52 @@ func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) {
return c, nil
}
-// TODO: use a sync.Cache instead
var (
-\tbufioReaderCache = make(chan *bufio.Reader, 4)
-\tbufioWriterCache2k = make(chan *bufio.Writer, 4)
-\tbufioWriterCache4k = make(chan *bufio.Writer, 4)
+\tbufioReaderPool sync.Pool
+\tbufioWriter2kPool sync.Pool
+\tbufioWriter4kPool sync.Pool
)
-func bufioWriterCache(size int) chan *bufio.Writer {\n+func bufioWriterPool(size int) *sync.Pool {
switch size {
case 2 << 10:
-\t\treturn bufioWriterCache2k
+\t\treturn &bufioWriter2kPool
case 4 << 10:
-\t\treturn bufioWriterCache4k
+\t\treturn &bufioWriter4kPool
}
return nil
}
func newBufioReader(r io.Reader) *bufio.Reader {
-\tselect {\n-\tcase p := <-bufioReaderCache:\n-\t\tp.Reset(r)\n-\t\treturn p\n-\tdefault:\n-\t\treturn bufio.NewReader(r)\n+\tif v := bufioReaderPool.Get(); v != nil {
+\t\tbr := v.(*bufio.Reader)
+\t\tbr.Reset(r)
+\t\treturn br
\t}\n+\treturn bufio.NewReader(r)
}
func putBufioReader(br *bufio.Reader) {
br.Reset(nil)
-\tselect {\n-\tcase bufioReaderCache <- br:\n-\tdefault:\n-\t}\n+\tbufioReaderPool.Put(br)
}
func newBufioWriterSize(w io.Writer, size int) *bufio.Writer {
-\tselect {\n-\tcase p := <-bufioWriterCache(size):\n-\t\tp.Reset(w)\n-\t\treturn p\n-\tdefault:\n-\t\treturn bufio.NewWriterSize(w, size)\n+\tpool := bufioWriterPool(size)
+\tif pool != nil {
+\t\tif v := pool.Get(); v != nil {
+\t\t\tbw := v.(*bufio.Writer)
+\t\t\tbw.Reset(w)
+\t\t\treturn bw
+\t\t}
\t}\n+\treturn bufio.NewWriterSize(w, size)
}
func putBufioWriter(bw *bufio.Writer) {
bw.Reset(nil)
-\tselect {\n-\tcase bufioWriterCache(bw.Available()) <- bw:\n-\tdefault:\n+\tif pool := bufioWriterPool(bw.Available()); pool != nil {
+\t\tpool.Put(bw)
\t}\n }
コアとなるコードの解説
このコミットでは、既存のチャネルベースのオブジェクトプールを sync.Pool
に置き換えることで、オブジェクトの再利用ロジックを統一し、効率化しています。
-
sync
パッケージのインポート:src/pkg/net/http/header.go
,src/pkg/net/http/request.go
,src/pkg/net/http/server.go
の各ファイルでimport "sync"
が追加されています。これはsync.Pool
を使用するために必須です。
-
sync.Pool
の宣言と初期化:header.go
ではheaderSorterPool
がsync.Pool
型で宣言され、New
フィールドにnew(headerSorter)
を返す関数が設定されています。これは、プールが空のときに新しいheaderSorter
オブジェクトを生成する方法を定義しています。request.go
とserver.go
では、textprotoReaderPool
,bufioReaderPool
,bufioWriter2kPool
,bufioWriter4kPool
が単にsync.Pool
型で宣言されています。これらのプールは、Get()
メソッドが呼び出された際に、対応するnew...
関数(例:textproto.NewReader(br)
)が新しいオブジェクトを生成するように設計されています。sync.Pool
のNew
フィールドが設定されていない場合、Get()
はnil
を返す可能性があるため、呼び出し側でnil
チェックと新規オブジェクトの生成を行う必要があります。このコミットでは、newTextprotoReader
やnewBufioReader
のようなヘルパー関数内でこのロジックがカプセル化されています。
-
オブジェクトの取得 (
Get()
):- 旧来の
select { case obj := <-cache: ... default: ... }
のパターンは、pool.Get()
の呼び出しに置き換えられました。 header.go
のsortedKeyValues
関数では、hs = headerSorterPool.Get().(*headerSorter)
と直接Get()
を呼び出し、型アサーションを行っています。request.go
のnewTextprotoReader
関数やserver.go
のnewBufioReader
,newBufioWriterSize
関数では、if v := pool.Get(); v != nil { ... }
の形式でGet()
の結果をチェックし、プールから取得できた場合はそれを再利用し、できなかった場合は新しいオブジェクトを生成しています。これはsync.Pool
のNew
フィールドが設定されていない場合の一般的なパターンです。
- 旧来の
-
オブジェクトの返却 (
Put()
):- 旧来の
select { case cache <- obj: ... default: ... }
のパターンは、pool.Put(obj)
の呼び出しに置き換えられました。 header.go
のWriteSubset
関数では、headerSorterPool.Put(sorter)
と直接Put()
を呼び出しています。request.go
のputTextprotoReader
関数やserver.go
のputBufioReader
,putBufioWriter
関数でも同様にPut()
を呼び出しています。オブジェクトをプールに戻す前に、Reset(nil)
などでオブジェクトの状態をリセットしている点も重要です。これにより、再利用されるオブジェクトが以前の状態を引きずらないようにしています。
- 旧来の
-
bufioWriterPool
ヘルパー関数:server.go
では、異なるバッファサイズ(2KB, 4KB)を持つbufio.Writer
のプールを管理するために、bufioWriterPool(size int) *sync.Pool
というヘルパー関数が導入されました。これにより、newBufioWriterSize
やputBufioWriter
関数が、バッファサイズに応じて適切なsync.Pool
インスタンスを動的に選択できるようになり、コードの重複が削減されています。
この変更により、net/http
パッケージは、より効率的でスケーラブルなオブジェクト再利用メカニズムを手に入れ、高負荷時のパフォーマンスが向上しました。
関連リンク
- Go Issue 4720: https://github.com/golang/go/issues/4720
- Go CL 44080043: https://golang.org/cl/44080043 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Go言語の
sync.Pool
ドキュメント: https://pkg.go.dev/sync#Pool - Go言語のガベージコレクションに関する情報 (一般的な知識): https://go.dev/doc/gc-guide
- Go言語におけるオブジェクトプーリングの概念 (一般的な知識): https://yourbasic.org/golang/sync-pool-guide/ (これは一般的なガイドであり、特定のコミットの直接的な情報源ではありませんが、
sync.Pool
の理解に役立ちます) - Goのチャネルに関する情報 (一般的な知識): https://go.dev/tour/concurrency/2
- Goの
net/http
パッケージに関する情報 (一般的な知識): https://pkg.go.dev/net/http - Goの
net/textproto
パッケージに関する情報 (一般的な知識): https://pkg.go.dev/net/textproto - Goの
bufio
パッケージに関する情報 (一般的な知識): https://pkg.go.dev/bufio