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