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

[インデックス 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/opnew ns/opdelta
BenchmarkServerFakeConnWithKeepAliveLite1032410149-1.70%
ベンチマーク名old allocsnew allocsdelta
BenchmarkServerFakeConnWithKeepAliveLite1917-10.53%
ベンチマーク名old bytesnew bytesdelta
BenchmarkServerFakeConnWithKeepAliveLite15591492-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のチャネルは、ゴルーチン間の安全な通信手段としてだけでなく、オブジェクトプールの実装にも非常に適しています。

  • 実装方法:
    1. バッファ付きチャネルの作成: make(chan *Type, capacity) のように、プールするオブジェクトの型と容量を指定してバッファ付きチャネルを作成します。このチャネルがオブジェクトのプールとして機能します。
    2. オブジェクトの取得: obj := <-pool のように、チャネルからオブジェクトを受信します。プールが空の場合、ゴルーチンはオブジェクトが利用可能になるまでブロックされます。
    3. オブジェクトの返却: pool <- obj のように、使用済みのオブジェクトをチャネルに送信してプールに戻します。
  • sync.Pool との比較:
    • sync.Pool: 一時的なオブジェクトの再利用に特化しており、GCによってオブジェクトが破棄される可能性があります。プールのサイズを明示的に制限することはできません。主にGC負荷軽減が目的です。
    • チャネルを用いたプール: チャネルに格納されたオブジェクトはGCの対象外となるため、オブジェクトの保持が保証されます。チャネルのバッファサイズによってプールの最大サイズを制御できます。コネクションプールなど、オブジェクトの生存期間を保証し、数を制限したい場合に適しています。

技術的詳細

このコミットでは、net/http パッケージの ReadRequest 関数内で textproto.Reader を効率的に再利用するためのカスタムプールが導入されています。

  1. textprotoReaderCache チャネルの導入:

    • var textprotoReaderCache = make(chan *textproto.Reader, 4)
    • これは、textproto.Reader インスタンスを格納するためのバッファ付きチャネルです。バッファサイズが 4 であるため、最大4つの textproto.Reader インスタンスをプールに保持できます。
    • コメント // TODO(bradfitz): use a sync.Cache when available が示唆するように、当時は sync.Pool がまだ存在しないか、あるいは現在の要件に完全に合致しなかったため、チャネルベースのカスタムプールが選択されました。
  2. 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 インスタンスを作成し、それを返します。
  3. putTextprotoReader 関数の実装:

    • この関数は、使用済みの textproto.Reader インスタンスをプールに返却するために使用されます。
    • r.R = nil: プールに戻す前に、textproto.Reader が保持している bufio.Reader への参照を nil に設定します。これは、古い bufio.Reader への参照が残り続けることによる意図しない動作やメモリリークを防ぐための重要なクリーンアップステップです。
    • select { case textprotoReaderCache <- r: default: }: textprotoReaderCacher を送信してプールに戻そうとします。
    • default: ブロックがあるため、プールが満杯の場合(バッファサイズ 4 を超える場合)、オブジェクトはプールに戻されずに破棄されます。これにより、プールのサイズが制限され、過剰なメモリ使用を防ぎます。
  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: }: textprotoReaderCacher を送信してプールに戻そうとします。
  • 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言語の 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)