[インデックス 18088] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージ内の Hijack
機能に関するベンチマークテストを追加するものです。具体的には、serve_test.go
ファイルに BenchmarkServerHijack
という新しいベンチマーク関数が追加されています。このベンチマークの主な目的は、Hijack
処理におけるメモリ割り当て(allocations)のパフォーマンスを測定することにあります。
コミット
commit cbf6ff3b90f5b70e9e7f6aafc1744efbb4761377
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Dec 19 13:24:42 2013 -0800
net/http: add Hihack benchmark
Notably, to show allocs. Currently: 11766 B/op, 21 allocs/op,
at least one alloc of which is in the benchmark loop itself.
R=golang-dev, jnewlin
CC=golang-dev
https://golang.org/cl/40370057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cbf6ff3b90f5b70e9e7f6aafc1744efbb4761377
元コミット内容
net/http: add Hihack benchmark
Notably, to show allocs. Currently: 11766 B/op, 21 allocs/op,
at least one alloc of which is in the benchmark loop itself.
変更の背景
このコミットの背景には、Goの net/http
パッケージにおける Hijack
機能のパフォーマンス、特にメモリ割り当ての効率性を評価したいという意図があります。Hijack
は、HTTPハンドラが基盤となるTCP接続を直接制御できるようにする強力な機能です。これは、WebSocketのようなプロトコルや、カスタムのストリーミングプロトコルを実装する際に非常に有用です。
しかし、このような低レベルの操作は、不注意に実装されると、不要なメモリ割り当てを引き起こし、ガベージコレクションの負荷を増やし、結果としてアプリケーションのパフォーマンスを低下させる可能性があります。コミットメッセージに「Notably, to show allocs. Currently: 11766 B/op, 21 allocs/op, at least one alloc of which is in the benchmark loop itself.」とあるように、このベンチマークは Hijack
処理がどれだけのメモリを割り当てているかを明らかにし、将来的な最適化の機会を特定するために追加されました。
Goの標準ライブラリは、高いパフォーマンスと効率性を追求しており、特にネットワークI/Oのようなクリティカルな部分では、メモリ割り当ての削減が重要な目標となります。このベンチマークの追加は、その目標達成に向けた一歩と言えます。
前提知識の解説
Go言語のベンチマーク
Go言語には、標準でベンチマークテストを記述・実行するためのフレームワークが testing
パッケージに組み込まれています。ベンチマーク関数は BenchmarkXxx(*testing.B)
というシグネチャを持ち、go test -bench=.
コマンドで実行されます。
*testing.B
: ベンチマークのコンテキストを提供する構造体です。b.N
: ベンチマーク関数が実行されるイテレーション回数を示します。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。b.ReportAllocs()
: このメソッドを呼び出すことで、ベンチマーク実行中に発生したメモリ割り当ての回数(allocs/op)とバイト数(B/op)をレポートに含めることができます。これは、パフォーマンス最適化、特にガベージコレクションの負荷軽減において非常に重要な指標となります。
net/http
パッケージと Hijacker
インターフェース
net/http
パッケージは、GoでHTTPクライアントおよびサーバーを実装するための主要なパッケージです。
http.ResponseWriter
: HTTPハンドラがクライアントに応答を書き込むために使用するインターフェースです。http.Hijacker
インターフェース:http.ResponseWriter
が実装できるオプションのインターフェースです。このインターフェースはHijack()
メソッドを定義しています。Hijack() (net.Conn, *bufio.ReadWriter, error)
: このメソッドを呼び出すと、HTTPサーバーは基盤となるTCP接続の制御をハンドラに引き渡します。これにより、ハンドラはHTTPプロトコルに縛られずに、生のTCP接続上で任意のプロトコル(例: WebSocket)を実装できます。Hijack()
が呼び出されると、net/http
パッケージはそれ以上その接続を処理せず、ハンドラが接続のライフサイクル(読み書き、クローズなど)を完全に管理する責任を負います。
メモリ割り当てとガベージコレクション
Goはガベージコレクタを持つ言語であり、開発者は手動でメモリを解放する必要がありません。しかし、不要なメモリ割り当て(アロケーション)が頻繁に発生すると、ガベージコレクタがより頻繁に実行され、アプリケーションの実行が一時停止(ストップ・ザ・ワールド)する時間が長くなり、全体的なパフォーマンスが低下する可能性があります。特に、ホットパス(頻繁に実行されるコードパス)でのアロケーションは、パフォーマンスに大きな影響を与えます。ベンチマークでアロケーション数を測定することは、このようなパフォーマンスボトルネックを特定し、最適化するための第一歩となります。
技術的詳細
このコミットで追加された BenchmarkServerHijack
ベンチマークは、net/http
サーバーが Hijack
機能を処理する際のオーバーヘッドを測定するように設計されています。
ベンチマークのセットアップは以下の要素を含んでいます。
b.ReportAllocs()
: これにより、ベンチマーク結果にメモリ割り当ての統計が含まれるようになります。これがこのベンチマークの主要な目的の一つです。- リクエストの準備:
reqBytes
ヘルパー関数(既存)を使用して、シンプルなGET / HTTP/1.1
リクエストのバイト列を準備します。これは、サーバーが処理を開始するための入力となります。
- ハンドラの定義:
http.HandlerFunc
を使用して、Hijack
を呼び出すだけのシンプルなHTTPハンドラを定義します。- ハンドラは
w.(Hijacker).Hijack()
を呼び出して接続をハイジャックし、すぐにconn.Close()
で接続を閉じます。これは、Hijack
処理自体のオーバーヘッドを分離して測定するためです。
- テスト接続とリスナーのモック:
rwTestConn
というテスト用の接続構造体(既存)を使用します。これはnet.Conn
インターフェースを実装しており、実際のネットワークI/Oを行わずに、メモリ内でリクエストとレスポンスをシミュレートできます。Writer: ioutil.Discard
: レスポンスの書き込みは破棄されます。closec: make(chan bool, 1)
: 接続が閉じられたことを通知するためのチャネルです。
oneConnListener
というテスト用のリスナー構造体(既存)を使用します。これは一度に一つの接続だけを提供するように設計されており、ベンチマークループ内で新しい接続をシミュレートするために使用されます。
- ベンチマークループ:
for i := 0; i < b.N; i++
ループ内で、b.N
回のイテレーションを実行します。- 各イテレーションで、
conn.Reader
を新しいリクエストバイト列でリセットし、ln.conn
を現在のテスト接続に設定します。 go Serve(ln, h)
を呼び出して、新しいゴルーチンでHTTPサーバーを起動します。これにより、サーバーはモックされたリスナーとハンドラを使用してリクエストを処理します。<-conn.closec
で、ハンドラが接続を閉じるのを待ちます。これにより、各イテレーションでHijack
処理が完了するのを確実に待機します。
このベンチマークは、実際のネットワークI/Oを伴わないため、Hijack
機能の内部的なロジックとメモリ管理に焦点を当てた測定が可能です。コミットメッセージにあるように、このベンチマークの実行結果は、Hijack
処理が1回あたり約11KBのメモリを割り当て、21回の割り当て操作を行っていることを示しており、そのうち少なくとも1回はベンチマークループ内で発生していることが指摘されています。これは、将来的にこれらの割り当てを減らすためのターゲットとなります。
コアとなるコードの変更箇所
変更は src/pkg/net/http/serve_test.go
ファイルに集中しており、以下の25行が追加されています。
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2390,3 +2390,28 @@ Host: golang.org
b.Errorf("b.N=%d but handled %d", b.N, handled)
}\n
}\n
+\n+func BenchmarkServerHijack(b *testing.B) {\n+ b.ReportAllocs()\n+ req := reqBytes(`GET / HTTP/1.1\n+Host: golang.org\n+`)\n+ h := HandlerFunc(func(w ResponseWriter, r *Request) {\n+ conn, _, err := w.(Hijacker).Hijack()\n+ if err != nil {\n+ panic(err)\n+ }\n+ conn.Close()\n+ })\n+ conn := &rwTestConn{\n+ Writer: ioutil.Discard,\n+ closec: make(chan bool, 1),\n+ }\n+ ln := &oneConnListener{conn: conn}\n+ for i := 0; i < b.N; i++ {\n+ conn.Reader = bytes.NewReader(req)\n+ ln.conn = conn\n+ go Serve(ln, h)\n+ <-conn.closec\n+ }\n+}\n
コアとなるコードの解説
追加された BenchmarkServerHijack
関数は、net/http
パッケージの Hijack
機能のパフォーマンスを測定するためのベンチマークです。
-
b.ReportAllocs()
:- この行は、ベンチマークの実行結果にメモリ割り当ての統計(バイト数と回数)を含めるようにGoのテストフレームワークに指示します。これにより、
Hijack
処理がどれだけのメモリを消費しているかを定量的に把握できます。
- この行は、ベンチマークの実行結果にメモリ割り当ての統計(バイト数と回数)を含めるようにGoのテストフレームワークに指示します。これにより、
-
req := reqBytes(...)
:- HTTPリクエストの生バイト列を定義しています。これは、サーバーが処理する仮想的なクライアントリクエストを表します。シンプルなGETリクエストが使用されています。
-
h := HandlerFunc(func(w ResponseWriter, r *Request) { ... })
:http.HandlerFunc
型の匿名関数として、HTTPハンドラを定義しています。- このハンドラの主要なロジックは
w.(Hijacker).Hijack()
の呼び出しです。これは、ResponseWriter
がHijacker
インターフェースを実装していることをアサートし、そのHijack
メソッドを呼び出して基盤となる接続の制御を奪います。 if err != nil { panic(err) }
は、エラーが発生した場合にパニックを引き起こします。ベンチマークでは通常、エラーは発生しないと想定されますが、堅牢性のために含まれています。conn.Close()
:Hijack
された接続は、ハンドラが責任を持って閉じる必要があります。このベンチマークでは、Hijack
のオーバーヘッドのみを測定するため、すぐに接続を閉じています。
-
conn := &rwTestConn{ ... }
:rwTestConn
は、net.Conn
インターフェースを実装するテスト用の構造体です。これにより、実際のネットワークソケットを使用せずに、メモリ内で接続の読み書きをシミュレートできます。Writer: ioutil.Discard
: 書き込み操作はすべて破棄されます。closec: make(chan bool, 1)
: 接続が閉じられたことをハンドラが通知するためのチャネルです。これにより、ベンチマークループが各イテレーションの完了を待つことができます。
-
ln := &oneConnListener{conn: conn}
:oneConnListener
は、net.Listener
インターフェースを実装するテスト用の構造体です。これは、Accept()
メソッドが常に単一の事前定義された接続(conn
)を返すように設計されています。
-
for i := 0; i < b.N; i++ { ... }
:- Goのベンチマークループです。
b.N
はテストフレームワークによって動的に調整され、統計的に有意な結果が得られるように十分な回数実行されます。 conn.Reader = bytes.NewReader(req)
: 各イテレーションの開始時に、rwTestConn
のリーダーを新しいリクエストバイト列でリセットします。これにより、各ベンチマーク実行が独立したリクエストとして扱われます。ln.conn = conn
:oneConnListener
が現在のconn
インスタンスを返すように設定します。go Serve(ln, h)
: 新しいゴルーチンでhttp.Serve
関数を呼び出します。Serve
は、指定されたリスナー(ln
)からの接続を受け入れ、指定されたハンドラ(h
)で処理します。これにより、Hijack
処理がトリガーされます。<-conn.closec
:Hijack
ハンドラがconn.Close()
を呼び出してclosec
チャネルに値を送信するまで、ベンチマークループはここでブロックされます。これにより、各イテレーションが完全に完了するのを待ち、正確な時間と割り当ての測定を保証します。
- Goのベンチマークループです。
このベンチマークは、Hijack
機能のメモリフットプリントと実行時間を効率的に測定するための、クリーンで分離されたテスト環境を提供しています。
関連リンク
- Go言語の
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語のベンチマークに関する公式ブログ記事やドキュメント(一般的な情報源として)
参考にした情報源リンク
- https://github.com/golang/go/commit/cbf6ff3b90f5b70e9e7f6aafc1744efbb4761377
- https://golang.org/cl/40370057 (Go Code Review - Gerrit)
- Go言語の公式ドキュメントおよびソースコード
- Go言語のベンチマークに関する一般的な知識
- HTTPプロトコルとTCP接続に関する一般的な知識