[インデックス 15970] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージのテストファイル src/pkg/net/http/serve_test.go に変更を加えています。具体的には、HTTPサーバーのパフォーマンスを測定するための新しいベンチマークが追加されています。このファイルは、HTTPサーバーの様々な機能や挙動をテストし、その性能を評価するためのコードを含んでいます。
コミット
commit 584a66b785af8c99b4bba3cb31c2b5e22f689438
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Mar 27 13:35:41 2013 -0700
net/http: new server-only, single-connection keep-alive benchmark
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8046043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/584a66b785af8c99b4bba3cb31c2b5e22f689438
元コミット内容
net/http: サーバー専用の、単一接続キープアライブベンチマークを新規追加
変更の背景
Go言語の net/http パッケージは、WebアプリケーションやAPIサーバーを構築する上で非常に重要な基盤を提供しています。その性能は、Goで開発されたサービスの全体的なパフォーマンスに直結するため、継続的な最適化と評価が不可欠です。
このコミットが導入された背景には、特にHTTP/1.1の「Keep-Alive」機能がサーバーのパフォーマンスに与える影響をより正確に測定したいというニーズがありました。Keep-Alive接続は、単一のTCP接続上で複数のHTTPリクエストとレスポンスをやり取りすることを可能にし、接続の確立と切断にかかるオーバーヘッドを削減することで、全体的なスループットを向上させます。
既存のベンチマークでは、Keep-Alive接続におけるサーバーの振る舞いや性能を、単一のクライアント接続が連続してリクエストを送信するシナリオで十分に評価できていなかった可能性があります。この新しいベンチマーク BenchmarkServerFakeConnWithKeepAlive は、まさにこの「サーバー側で、単一の接続を介して連続するKeep-Aliveリクエストを処理する」という特定のシナリオに焦点を当てることで、より現実的かつ詳細な性能データを得ることを目的としています。これにより、net/http サーバーがKeep-Alive接続をどのように効率的に処理しているか、あるいは改善の余地があるかを特定するための重要な指標が提供されます。
前提知識の解説
Go言語のベンチマーク
Go言語には、標準の testing パッケージにベンチマーク機能が組み込まれています。
go test -bench=.: ベンチマークを実行するためのコマンドです。func BenchmarkXxx(b *testing.B): ベンチマーク関数はBenchmarkで始まり、*testing.B型の引数を取ります。b.N: ベンチマーク関数内でループを回す回数を表します。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるように十分な回数実行されます。b.ReportAllocs(): メモリ割り当ての統計情報(ヒープ割り当てのバイト数と回数)をベンチマーク結果に含めるように指示します。これにより、ベンチマーク対象のコードがどれだけメモリを効率的に使用しているかを評価できます。b.ResetTimer(): タイマーをリセットし、それ以前の処理時間を測定対象から除外します。セットアップ処理の時間を測定に含めないために使用されます。b.StopTimer()/b.StartTimer(): タイマーの一時停止と再開を行います。
HTTP Keep-Alive (永続的接続)
HTTP/1.1では、デフォルトでKeep-Alive接続が有効になっています。
- 目的: クライアントとサーバー間で一度確立されたTCP接続を、単一のHTTPリクエスト/レスポンスの交換後も閉じずに再利用することで、その後のリクエスト/レスポンスのオーバーヘッド(TCPハンドシェイク、TLSハンドシェイクなど)を削減し、パフォーマンスを向上させます。
- 動作:
- クライアントがサーバーに接続し、最初のHTTPリクエストを送信します。
- サーバーはレスポンスを返しますが、TCP接続を閉じません。
- クライアントは同じTCP接続を使って次のリクエストを送信できます。
- このプロセスは、どちらかのエンドポイントが接続を閉じるか、タイムアウトするまで繰り返されます。
- メリット: ネットワークの遅延が大きく、多数の小さなリクエストが発生するシナリオで特に効果を発揮します。
net/http パッケージの基本要素
http.Handlerインターフェース: HTTPリクエストを処理するためのインターフェースで、ServeHTTP(ResponseWriter, *Request)メソッドを定義します。http.ResponseWriter: HTTPレスポンスをクライアントに書き込むためのインターフェースです。http.Request: クライアントからのHTTPリクエストを表す構造体です。http.Serve(l net.Listener, handler Handler): 指定されたnet.Listenerからの接続を受け入れ、それぞれの接続でhandlerを使ってHTTPリクエストを処理する関数です。
net.Conn インターフェース
Goの net パッケージで定義されているネットワーク接続を表す汎用インターフェースです。
Read(b []byte) (n int, err error): 接続からデータを読み込みます。Write(b []byte) (n int, err error): 接続にデータを書き込みます。Close() error: 接続を閉じます。LocalAddr() net.Addr: ローカルネットワークアドレスを返します。RemoteAddr() net.Addr: リモートネットワークアドレスを返します。SetDeadline(t time.Time) error: 読み書きのデッドラインを設定します。SetReadDeadline(t time.Time) error: 読み込みのデッドラインを設定します。SetWriteDeadline(t time.Time) error: 書き込みのデッドラインを設定します。
テストにおいては、実際のネットワーク接続ではなく、これらのインターフェースを実装したダミー(モック)オブジェクトを使用することで、外部依存なしに特定のシナリオをシミュレートし、コードの挙動を制御・検証することが可能になります。
技術的詳細
このコミットでは、net/http サーバーのKeep-Alive性能をベンチマークするために、いくつかのカスタム型とロジックが導入されています。
-
noopConn構造体と関連メソッドの導入:- これは
net.Connインターフェースの最小限の実装を提供するダミー接続です。LocalAddr(),RemoteAddr(),SetDeadline(),SetReadDeadline(),SetWriteDeadline()メソッドはすべて何もしない(noop)実装となっています。 - 既存の
testConn構造体からこれらのメソッドが削除され、代わりにnoopConnを埋め込む形にリファクタリングされました。これにより、共通のダミー接続ロジックが再利用され、コードの重複が削減されています。
- これは
-
rwTestConn構造体の導入:- この構造体は、
io.Readerとio.Writerインターフェースを埋め込み、さらにnoopConnを埋め込むことで、読み込みと書き込みの動作を完全に制御できる柔軟なテスト用接続を提供します。 closec chan boolフィールドを持ち、接続が閉じられたときにこのチャネルに値を送信することで、ベンチマークの終了を外部に通知するメカニズムを提供します。これは、http.Serveがゴルーチンで実行された際に、ベンチマーク関数がその完了を待つために使用されます。
- この構造体は、
-
repeatReader構造体の導入:- これは
io.Readerインターフェースを実装するカスタムリーダーです。 content []byte: 繰り返し読み込むバイト列。count int:contentを何回繰り返すか。off int: 現在の読み込みオフセット。Readメソッドは、contentをcount回繰り返して読み込み、countが0になるとio.EOFを返します。- このリーダーは、単一のKeep-Alive接続上で複数のHTTPリクエストをシミュレートするために不可欠です。
b.N回のリクエストを送信するために、HTTPリクエストのバイト列をb.N回繰り返して提供します。
- これは
-
BenchmarkServerFakeConnWithKeepAliveベンチマーク関数の実装:b.ReportAllocs(): メモリ割り当ての統計を報告するように設定します。- リクエストとレスポンスの準備: 標準的なHTTP GETリクエストのバイト列 (
req) と、シンプルなレスポンスボディ (res) を定義します。reqは改行コードをCRLF (\r\n) に変換しています。 rwTestConnの構築:ReaderにはrepeatReaderのインスタンスが設定されます。このrepeatReaderは、reqをb.N回繰り返して提供するように構成されます。これにより、ベンチマークのイテレーション数 (b.N) と同じ数のリクエストが単一の接続上でシミュレートされます。Writerにはioutil.Discardが設定されます。これは、サーバーからのレスポンスボディを破棄することを意味し、レスポンスの書き込みにかかる時間をベンチマークの対象から除外します。ベンチマークの焦点はサーバーがリクエストを処理する能力にあるため、レスポンスの書き込みは重要ではありません。closecチャネルが作成され、接続が閉じられたときに通知を受け取れるようにします。
- ハンドラの定義:
http.HandlerFuncが定義され、リクエストが処理されるたびにhandledカウンタをインクリメントし、簡単なHTMLレスポンスを書き込みます。 - サーバーの起動:
oneConnListener(既存のテストユーティリティで、単一のnet.Connをnet.Listenerとしてラップするもの) を使用して、rwTestConnをリスナーとして設定します。go http.Serve(ln, handler)を使って、http.Serve関数を新しいゴルーチンで実行します。これにより、ベンチマーク関数はサーバーの処理と並行して動作できます。
- ベンチマークの終了待機と検証:
<-conn.closecで、rwTestConnが閉じられるのを待ちます。これは、repeatReaderがすべてのリクエストを読み終え、http.Serveが接続を閉じたことを意味します。if b.N != handledで、ベンチマークのイテレーション数 (b.N) と実際にハンドラが処理したリクエスト数 (handled) が一致するかを検証します。これにより、すべてのシミュレートされたリクエストがサーバーによって正しく処理されたことが保証されます。
このベンチマークは、実際のネットワークI/Oを伴わない「フェイクコネクション」を使用することで、ネットワークの変動や遅延の影響を排除し、純粋にGoの net/http サーバーがKeep-Alive接続上でリクエストを処理する内部的な効率を測定することを可能にしています。
コアとなるコードの変更箇所
src/pkg/net/http/serve_test.go の変更点です。
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -64,10 +64,34 @@ func (a dummyAddr) String() string {
return string(a)
}
+type noopConn struct{}
+
+func (noopConn) LocalAddr() net.Addr { return dummyAddr("local-addr") }
+func (noopConn) RemoteAddr() net.Addr { return dummyAddr("remote-addr") }
+func (noopConn) SetDeadline(t time.Time) error { return nil }
+func (noopConn) SetReadDeadline(t time.Time) error { return nil }
+func (noopConn) SetWriteDeadline(t time.Time) error { return nil }
+
+type rwTestConn struct {
+ io.Reader
+ io.Writer
+ noopConn
+ closec chan bool // if non-nil, send value to it on close
+}
+
+func (c *rwTestConn) Close() error {
+ select {
+ case c.closec <- true:
+ default:
+ }
+ return nil
+}
+
type testConn struct {
readBuf bytes.Buffer
writeBuf bytes.Buffer
closec chan bool // if non-nil, send value to it on close
+ noopConn
}
func (c *testConn) Read(b []byte) (int, error) {
@@ -86,26 +110,6 @@ func (c *testConn) Close() error {
return nil
}
--func (c *testConn) LocalAddr() net.Addr {
-- return dummyAddr("local-addr")
--}
--
--func (c *testConn) RemoteAddr() net.Addr {
-- return dummyAddr("remote-addr")
--}
--
--func (c *testConn) SetDeadline(t time.Time) error {
-- return nil
--}
--
--func (c *testConn) SetReadDeadline(t time.Time) error {
-- return nil
--}
--
--func (c *testConn) SetWriteDeadline(t time.Time) error {
-- return nil
--}
--
func TestConsumingBodyOnNextConn(t *testing.T) {
conn := new(testConn)
for i := 0; i < 2; i++ {
@@ -1653,3 +1657,56 @@ Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
<-conn.closec
}\n
}\n+\n+// repeatReader reads content count times, then EOFs.\n+type repeatReader struct {\n+\tcontent []byte\n+\tcount int\n+\toff int\n+}\n+\n+func (r *repeatReader) Read(p []byte) (n int, err error) {\n+\tif r.count <= 0 {\n+\t\treturn 0, io.EOF\n+\t}\n+\tn = copy(p, r.content[r.off:])\n+\tr.off += n\n+\tif r.off == len(r.content) {\n+\t\tr.count--\n+\t\tr.off = 0\n+\t}\n+\treturn\n+}\n+\n+func BenchmarkServerFakeConnWithKeepAlive(b *testing.B) {\n+\tb.ReportAllocs()\n+\n+\treq := []byte(strings.Replace(`GET / HTTP/1.1\n+Host: golang.org\n+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n+User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17\n+Accept-Encoding: gzip,deflate,sdch\n+Accept-Language: en-US,en;q=0.8\n+Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\n+\n+`, \"\\n\", \"\\r\\n\", -1))\n+\tres := []byte(\"Hello world!\\n\")\n+\n+\tconn := &rwTestConn{\n+\t\tReader: &repeatReader{content: req, count: b.N},\n+\t\tWriter: ioutil.Discard,\n+\t\tclosec: make(chan bool, 1),\n+\t}\n+\thandled := 0\n+\thandler := HandlerFunc(func(rw ResponseWriter, r *Request) {\n+\t\thandled++\n+\t\trw.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n+\t\trw.Write(res)\n+\t})\n+\tln := &oneConnListener{conn: conn}\n+\tgo Serve(ln, handler)\n+\t<-conn.closec\n+\tif b.N != handled {\n+\t\tb.Errorf(\"b.N=%d but handled %d\", b.N, handled)\n+\t}\n+}\n```
## コアとなるコードの解説
### `noopConn` 構造体
```go
type noopConn struct{}
func (noopConn) LocalAddr() net.Addr { return dummyAddr("local-addr") }
func (noopConn) RemoteAddr() net.Addr { return dummyAddr("remote-addr") }
func (noopConn) SetDeadline(t time.Time) error { return nil }
func (noopConn) SetReadDeadline(t time.Time) error { return nil }
func (noopConn) SetWriteDeadline(t time.Time) error { return nil }
noopConn は、net.Conn インターフェースの一部メソッドを何もしない(no-operation)で実装する空の構造体です。これは、テスト用の接続において、アドレス取得やデッドライン設定といった機能が不要な場合に、基盤となるダミー実装として利用されます。これにより、testConn のような他のテスト用接続構造体は、これらの共通のダミーメソッドを個別に実装する代わりに noopConn を埋め込むことで、コードの重複を避け、簡潔さを保つことができます。
rwTestConn 構造体
type rwTestConn struct {
io.Reader
io.Writer
noopConn
closec chan bool // if non-nil, send value to it on close
}
func (c *rwTestConn) Close() error {
select {
case c.closec <- true:
default:
}
return nil
}
rwTestConn は、読み込み (io.Reader) と書き込み (io.Writer) の動作を外部から完全に制御できるテスト用のネットワーク接続をシミュレートする構造体です。
io.Readerとio.Writerを埋め込むことで、この構造体自体がReadとWriteメソッドを持つようになり、実際の読み書きのデータソースとシンクを自由に設定できます。noopConnを埋め込むことで、net.Connインターフェースの残りのメソッド(LocalAddrなど)も提供されます。closec chan boolは、この接続が閉じられたときに、そのイベントを外部に通知するためのチャネルです。Close()メソッドが呼び出されると、このチャネルにtrueが送信されます。これは、ベンチマーク関数がサーバーの処理完了を待つための同期メカニズムとして使用されます。
repeatReader 構造体
type repeatReader struct {
content []byte
count int
off int
}
func (r *repeatReader) Read(p []byte) (n int, err error) {
if r.count <= 0 {
return 0, io.EOF
}
n = copy(p, r.content[r.off:])
r.off += n
if r.off == len(r.content) {
r.count--
r.off = 0
}
return
}
repeatReader は、特定のバイト列 (content) を指定された回数 (count) だけ繰り返し読み込ませるための io.Reader の実装です。
Readメソッドが呼び出されるたびに、contentの残りの部分をpにコピーします。contentの最後まで読み込んだ場合 (r.off == len(r.content))、countをデクリメントし、r.offをリセットしてcontentの先頭から再度読み込みを開始します。countが0以下になると、それ以上読み込むデータがないことを示すio.EOFを返します。 このリーダーは、HTTP Keep-Alive接続において、単一のTCP接続上で複数のHTTPリクエストが連続して送信されるシナリオをシミュレートするために使用されます。
BenchmarkServerFakeConnWithKeepAlive 関数
func BenchmarkServerFakeConnWithKeepAlive(b *testing.B) {
b.ReportAllocs()
req := []byte(strings.Replace(`GET / HTTP/1.1
Host: golang.org
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
`, "\n", "\r\n", -1))
res := []byte("Hello world!\n")
conn := &rwTestConn{
Reader: &repeatReader{content: req, count: b.N},
Writer: ioutil.Discard,
closec: make(chan bool, 1),
}
handled := 0
handler := HandlerFunc(func(rw ResponseWriter, r *Request) {
handled++
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.Write(res)
})
ln := &oneConnListener{conn: conn}
go Serve(ln, handler)
<-conn.closec
if b.N != handled {
b.Errorf("b.N=%d but handled %d", b.N, handled)
}
}
この関数は、サーバーが単一のKeep-Alive接続上で複数のリクエストを処理する性能を測定するベンチマークです。
b.ReportAllocs(): メモリ割り当ての統計をベンチマーク結果に含めるように設定します。reqとresの定義: シミュレートするHTTPリクエストと、サーバーが返すダミーのHTTPレスポンスボディをバイト列として定義します。reqは改行コードをCRLFに変換しています。rwTestConnの初期化:Readerには、reqをb.N回繰り返すrepeatReaderが設定されます。これにより、ベンチマークのイテレーション数 (b.N) と同じ数のリクエストが、この仮想的な接続を通じてサーバーに送信されるようにシミュレートされます。Writerにはioutil.Discardが設定されます。これは、サーバーがレスポンスを書き込んでも、そのデータがどこにも保存されずに破棄されることを意味します。これにより、レスポンスの書き込みにかかるI/O時間がベンチマークの測定対象から除外され、純粋なリクエスト処理の性能が測定されます。closecチャネルが作成され、接続が閉じられたときに通知を受け取れるようにします。
handledカウンタとhandlerの定義:handledは、サーバーが処理したリクエストの数をカウントするための変数です。handlerは、http.HandlerFuncとして定義され、リクエストが来るたびにhandledをインクリメントし、簡単なHTTPレスポンスを書き込みます。
- サーバーの起動:
oneConnListener(既存のテストヘルパー) を使用して、作成したconn(仮想接続) をnet.Listenerとしてラップします。go Serve(ln, handler)を使って、http.Serve関数を新しいゴルーチンで実行します。これにより、サーバーはバックグラウンドでリクエストの処理を開始します。
- ベンチマークの終了待機と検証:
<-conn.closecで、rwTestConnが閉じられるのを待ちます。repeatReaderがすべてのb.N回のリクエストを読み終えると、http.Serveは接続を閉じ、rwTestConn.Close()が呼び出されてclosecに値が送信されます。if b.N != handledで、ベンチマークのイテレーション数 (b.N) と、実際にハンドラが処理したリクエスト数 (handled) が一致するかを検証します。これにより、シミュレートされたすべてのリクエストがサーバーによって正しく処理されたことが保証されます。
このベンチマークは、ネットワークI/Oのオーバーヘッドを排除し、Goの net/http サーバーがKeep-Alive接続上でリクエストを効率的に処理する能力を、隔離された環境で高精度に測定することを可能にします。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
net/httpパッケージのドキュメント: https://pkg.go.dev/net/httptestingパッケージのドキュメント: https://pkg.go.dev/testing- HTTP Keep-Alive (MDN Web Docs): https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Keep-Alive
参考にした情報源リンク
- Go言語のベンチマークに関する公式ブログ記事やチュートリアル (一般的な知識として参照)
- HTTP/1.1 の永続的接続に関するRFC (RFC 2616, RFC 7230など) (一般的な知識として参照)
net.Connインターフェースの一般的な利用方法 (一般的な知識として参照)