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

[インデックス 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ハンドシェイクなど)を削減し、パフォーマンスを向上させます。
  • 動作:
    1. クライアントがサーバーに接続し、最初のHTTPリクエストを送信します。
    2. サーバーはレスポンスを返しますが、TCP接続を閉じません。
    3. クライアントは同じTCP接続を使って次のリクエストを送信できます。
    4. このプロセスは、どちらかのエンドポイントが接続を閉じるか、タイムアウトするまで繰り返されます。
  • メリット: ネットワークの遅延が大きく、多数の小さなリクエストが発生するシナリオで特に効果を発揮します。

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性能をベンチマークするために、いくつかのカスタム型とロジックが導入されています。

  1. noopConn 構造体と関連メソッドの導入:

    • これは net.Conn インターフェースの最小限の実装を提供するダミー接続です。LocalAddr(), RemoteAddr(), SetDeadline(), SetReadDeadline(), SetWriteDeadline() メソッドはすべて何もしない(noop)実装となっています。
    • 既存の testConn 構造体からこれらのメソッドが削除され、代わりに noopConn を埋め込む形にリファクタリングされました。これにより、共通のダミー接続ロジックが再利用され、コードの重複が削減されています。
  2. rwTestConn 構造体の導入:

    • この構造体は、io.Readerio.Writer インターフェースを埋め込み、さらに noopConn を埋め込むことで、読み込みと書き込みの動作を完全に制御できる柔軟なテスト用接続を提供します。
    • closec chan bool フィールドを持ち、接続が閉じられたときにこのチャネルに値を送信することで、ベンチマークの終了を外部に通知するメカニズムを提供します。これは、http.Serve がゴルーチンで実行された際に、ベンチマーク関数がその完了を待つために使用されます。
  3. repeatReader 構造体の導入:

    • これは io.Reader インターフェースを実装するカスタムリーダーです。
    • content []byte: 繰り返し読み込むバイト列。
    • count int: content を何回繰り返すか。
    • off int: 現在の読み込みオフセット。
    • Read メソッドは、contentcount 回繰り返して読み込み、count が0になると io.EOF を返します。
    • このリーダーは、単一のKeep-Alive接続上で複数のHTTPリクエストをシミュレートするために不可欠です。b.N 回のリクエストを送信するために、HTTPリクエストのバイト列を b.N 回繰り返して提供します。
  4. BenchmarkServerFakeConnWithKeepAlive ベンチマーク関数の実装:

    • b.ReportAllocs(): メモリ割り当ての統計を報告するように設定します。
    • リクエストとレスポンスの準備: 標準的なHTTP GETリクエストのバイト列 (req) と、シンプルなレスポンスボディ (res) を定義します。req は改行コードをCRLF (\r\n) に変換しています。
    • rwTestConn の構築:
      • Reader には repeatReader のインスタンスが設定されます。この repeatReader は、reqb.N 回繰り返して提供するように構成されます。これにより、ベンチマークのイテレーション数 (b.N) と同じ数のリクエストが単一の接続上でシミュレートされます。
      • Writer には ioutil.Discard が設定されます。これは、サーバーからのレスポンスボディを破棄することを意味し、レスポンスの書き込みにかかる時間をベンチマークの対象から除外します。ベンチマークの焦点はサーバーがリクエストを処理する能力にあるため、レスポンスの書き込みは重要ではありません。
      • closec チャネルが作成され、接続が閉じられたときに通知を受け取れるようにします。
    • ハンドラの定義: http.HandlerFunc が定義され、リクエストが処理されるたびに handled カウンタをインクリメントし、簡単なHTMLレスポンスを書き込みます。
    • サーバーの起動:
      • oneConnListener (既存のテストユーティリティで、単一の net.Connnet.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.Readerio.Writer を埋め込むことで、この構造体自体が ReadWrite メソッドを持つようになり、実際の読み書きのデータソースとシンクを自由に設定できます。
  • 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接続上で複数のリクエストを処理する性能を測定するベンチマークです。

  1. b.ReportAllocs(): メモリ割り当ての統計をベンチマーク結果に含めるように設定します。
  2. reqres の定義: シミュレートするHTTPリクエストと、サーバーが返すダミーのHTTPレスポンスボディをバイト列として定義します。req は改行コードをCRLFに変換しています。
  3. rwTestConn の初期化:
    • Reader には、reqb.N 回繰り返す repeatReader が設定されます。これにより、ベンチマークのイテレーション数 (b.N) と同じ数のリクエストが、この仮想的な接続を通じてサーバーに送信されるようにシミュレートされます。
    • Writer には ioutil.Discard が設定されます。これは、サーバーがレスポンスを書き込んでも、そのデータがどこにも保存されずに破棄されることを意味します。これにより、レスポンスの書き込みにかかるI/O時間がベンチマークの測定対象から除外され、純粋なリクエスト処理の性能が測定されます。
    • closec チャネルが作成され、接続が閉じられたときに通知を受け取れるようにします。
  4. handled カウンタと handler の定義:
    • handled は、サーバーが処理したリクエストの数をカウントするための変数です。
    • handler は、http.HandlerFunc として定義され、リクエストが来るたびに handled をインクリメントし、簡単なHTTPレスポンスを書き込みます。
  5. サーバーの起動:
    • oneConnListener (既存のテストヘルパー) を使用して、作成した conn (仮想接続) を net.Listener としてラップします。
    • go Serve(ln, handler) を使って、http.Serve 関数を新しいゴルーチンで実行します。これにより、サーバーはバックグラウンドでリクエストの処理を開始します。
  6. ベンチマークの終了待機と検証:
    • <-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言語のベンチマークに関する公式ブログ記事やチュートリアル (一般的な知識として参照)
  • HTTP/1.1 の永続的接続に関するRFC (RFC 2616, RFC 7230など) (一般的な知識として参照)
  • net.Conn インターフェースの一般的な利用方法 (一般的な知識として参照)