Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Readerbufio.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 インスタンスに置き換えたことです。

具体的には、以下のキャッシュが変更されました。

  1. 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) に変更されました。
  2. 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) を使用するように変更されました。
  3. 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 ヘルパー関数が導入され、サイズに応じた適切なプールを返すようになりました。

これらの変更により、オブジェクトの取得と返却のロジックが select ステートメントによるチャネル操作から、sync.PoolGet()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 に置き換えることで、オブジェクトの再利用ロジックを統一し、効率化しています。

  1. sync パッケージのインポート:

    • src/pkg/net/http/header.go, src/pkg/net/http/request.go, src/pkg/net/http/server.go の各ファイルで import "sync" が追加されています。これは sync.Pool を使用するために必須です。
  2. sync.Pool の宣言と初期化:

    • header.go では headerSorterPoolsync.Pool 型で宣言され、New フィールドに new(headerSorter) を返す関数が設定されています。これは、プールが空のときに新しい headerSorter オブジェクトを生成する方法を定義しています。
    • request.goserver.go では、textprotoReaderPool, bufioReaderPool, bufioWriter2kPool, bufioWriter4kPool が単に sync.Pool 型で宣言されています。これらのプールは、Get() メソッドが呼び出された際に、対応する new... 関数(例: textproto.NewReader(br))が新しいオブジェクトを生成するように設計されています。sync.PoolNew フィールドが設定されていない場合、Get()nil を返す可能性があるため、呼び出し側で nil チェックと新規オブジェクトの生成を行う必要があります。このコミットでは、newTextprotoReadernewBufioReader のようなヘルパー関数内でこのロジックがカプセル化されています。
  3. オブジェクトの取得 (Get()):

    • 旧来の select { case obj := <-cache: ... default: ... } のパターンは、pool.Get() の呼び出しに置き換えられました。
    • header.gosortedKeyValues 関数では、hs = headerSorterPool.Get().(*headerSorter) と直接 Get() を呼び出し、型アサーションを行っています。
    • request.gonewTextprotoReader 関数や server.gonewBufioReader, newBufioWriterSize 関数では、if v := pool.Get(); v != nil { ... } の形式で Get() の結果をチェックし、プールから取得できた場合はそれを再利用し、できなかった場合は新しいオブジェクトを生成しています。これは sync.PoolNew フィールドが設定されていない場合の一般的なパターンです。
  4. オブジェクトの返却 (Put()):

    • 旧来の select { case cache <- obj: ... default: ... } のパターンは、pool.Put(obj) の呼び出しに置き換えられました。
    • header.goWriteSubset 関数では、headerSorterPool.Put(sorter) と直接 Put() を呼び出しています。
    • request.goputTextprotoReader 関数や server.goputBufioReader, putBufioWriter 関数でも同様に Put() を呼び出しています。オブジェクトをプールに戻す前に、Reset(nil) などでオブジェクトの状態をリセットしている点も重要です。これにより、再利用されるオブジェクトが以前の状態を引きずらないようにしています。
  5. bufioWriterPool ヘルパー関数:

    • server.go では、異なるバッファサイズ(2KB, 4KB)を持つ bufio.Writer のプールを管理するために、bufioWriterPool(size int) *sync.Pool というヘルパー関数が導入されました。これにより、newBufioWriterSizeputBufioWriter 関数が、バッファサイズに応じて適切な sync.Pool インスタンスを動的に選択できるようになり、コードの重複が削減されています。

この変更により、net/http パッケージは、より効率的でスケーラブルなオブジェクト再利用メカニズムを手に入れ、高負荷時のパフォーマンスが向上しました。

関連リンク

参考にした情報源リンク