[インデックス 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
インターフェースの一般的な利用方法 (一般的な知識として参照)