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

[インデックス 16057] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおいて、HTTPサーバーのハンドラ性能を測定するための新しいベンチマークを追加するものです。特に、Content-Type ヘッダと Content-Length ヘッダの有無が、レスポンスの処理性能にどのような影響を与えるかを詳細に評価することを目的としています。

コミット

net/http: new server Handler benchmarks

For all the Content-Type & Content-Length cases.

R=golang-dev, pabuhr
CC=golang-dev
https://golang.org/cl/8280046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6ca1fa625c2377071163399f1579a440e7d29502

元コミット内容

commit 6ca1fa625c2377071163399f1579a440e7d29502
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Apr 2 15:42:06 2013 -0700

    net/http: new server Handler benchmarks
    
    For all the Content-Type & Content-Length cases.
    
    R=golang-dev, pabuhr
    CC=golang-dev
    https://golang.org/cl/8280046
---
 src/pkg/net/http/serve_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)

diff --git a/src/pkg/net/http/serve_test.go b/src/pkg/net/http/serve_test.go
index a040f2738b..102f489427 100644
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1937,3 +1937,64 @@ Host: golang.org
 	\tb.Errorf(\"b.N=%d but handled %d\", b.N, handled)\n \t}\n }\n+\n+const someResponse = \"<html>some response</html>\"\n+\n+// A Reponse that\'s just no bigger than 2KB, the buffer-before-chunking threshold.\n+var response = bytes.Repeat([]byte(someResponse), 2<<10/len(someResponse))\n+\n+// Both Content-Type and Content-Length set. Should be no buffering.\n+func BenchmarkServerHandlerTypeLen(b *testing.B) {\n+\tbenchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tw.Header().Set(\"Content-Type\", \"text/html\")\n+\t\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(response)))\n+\t\tw.Write(response)\n+\t}))\n+}\n+\n+// A Content-Type is set, but no length. No sniffing, but will count the Content-Length.\n+func BenchmarkServerHandlerNoLen(b *testing.B) {\n+\tbenchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tw.Header().Set(\"Content-Type\", \"text/html\")\n+\t\tw.Write(response)\n+\t}))\n+}\n+\n+// A Content-Length is set, but the Content-Type will be sniffed.\n+func BenchmarkServerHandlerNoType(b *testing.B) {\n+\tbenchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(response)))\n+\t\tw.Write(response)\n+\t}))\n+}\n+\n+// Neither a Content-Type or Content-Length, so sniffed and counted.\n+func BenchmarkServerHandlerNoHeader(b *testing.B) {\n+\tbenchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {\n+\t\tw.Write(response)\n+\t}))\n+}\n+\n+func benchmarkHandler(b *testing.B, h Handler) {\n+\tb.ReportAllocs()\n+\treq := []byte(strings.Replace(`GET / HTTP/1.1\n+Host: golang.org\n+\n+`, \"\\n\", \"\\r\\n\", -1))\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\th.ServeHTTP(rw, r)\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```

## 変更の背景

このコミットの背景には、Go言語の `net/http` パッケージが提供するHTTPサーバーの性能特性をより深く理解し、最適化の機会を特定するという目的があります。特に、HTTPレスポンスを送信する際に `Content-Type` ヘッダと `Content-Length` ヘッダがどのように設定されているかによって、サーバー内部の処理(例えば、コンテンツタイプの推測、バッファリング、チャンク転送エンコーディングの適用など)が変化し、それが性能に影響を与える可能性があります。

開発者は、これらのヘッダの組み合わせがサーバーのハンドラ処理に与えるオーバーヘッドを定量的に測定し、潜在的なボトルネックを特定したいと考えていました。これにより、`net/http` パッケージの堅牢性と効率性をさらに向上させることが可能になります。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語およびHTTPの基本的な概念を理解しておく必要があります。

*   **Go言語の `net/http` パッケージ**: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。
    *   `http.Handler` インターフェース: HTTPリクエストを処理するためのインターフェースで、`ServeHTTP(ResponseWriter, *Request)` メソッドを実装する必要があります。
    *   `http.ResponseWriter`: HTTPレスポンスをクライアントに書き込むためのインターフェースです。ヘッダの設定やボディの書き込みを行います。
    *   `http.Request`: クライアントからのHTTPリクエストを表す構造体です。リクエストメソッド、URL、ヘッダ、ボディなどの情報を含みます。
    *   `http.Serve`: HTTP接続を受け入れ、各接続に対して新しいゴルーチンを起動してリクエストを処理する関数です。
*   **HTTPヘッダ**: HTTPメッセージのメタデータを提供するキーと値のペアです。
    *   `Content-Type`: レスポンスボディのメディアタイプ(例: `text/html`, `application/json`)を示します。このヘッダがない場合、ブラウザやクライアントはコンテンツを「スニッフィング」(内容から推測)しようとすることがあります。
    *   `Content-Length`: レスポンスボディのバイト単位のサイズを示します。このヘッダが設定されていると、クライアントはレスポンスボディの終わりを正確に知ることができます。設定されていない場合、サーバーは通常、チャンク転送エンコーディング(Chunked Transfer Encoding)を使用してレスポンスを送信します。
*   **HTTPチャンク転送エンコーディング (Chunked Transfer Encoding)**: `Content-Length` ヘッダが不明な場合や、動的にコンテンツを生成する場合に、HTTP/1.1でレスポンスボディを送信するためのメカニズムです。ボディは複数の「チャンク」に分割され、各チャンクの前にそのサイズが記述されます。これにより、サーバーはレスポンス全体のサイズを知ることなく、データをストリームとして送信できます。
*   **Go言語のベンチマークテスト**: Go言語の `testing` パッケージには、コードの性能を測定するためのベンチマーク機能が組み込まれています。
    *   `func BenchmarkXxx(b *testing.B)`: ベンチマーク関数は `Benchmark` で始まり、`*testing.B` 型の引数を取ります。
    *   `b.N`: ベンチマーク関数が実行されるイテレーション回数です。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。
    *   `b.ReportAllocs()`: メモリ割り当ての統計を報告するように設定します。
*   **`bytes.Repeat`**: 指定されたバイトスライスをN回繰り返して新しいバイトスライスを生成する関数です。
*   **`strconv.Itoa`**: 整数を文字列に変換する関数です。
*   **`io.Reader` と `io.Writer`**: Go言語におけるI/O操作の基本的なインターフェースです。
*   **`ioutil.Discard`**: `io.Writer` の実装で、書き込まれたデータをすべて破棄します。ベンチマークにおいて、書き込み先の実装によるオーバーヘッドを排除し、純粋な処理時間を測定するのに役立ちます。

## 技術的詳細

このコミットで追加されたベンチマークは、`net/http` サーバーがHTTPレスポンスを処理する際の、`Content-Type` と `Content-Length` ヘッダの組み合わせによる性能特性を評価することに焦点を当てています。

HTTPサーバーがレスポンスを送信する際、これらのヘッダの有無は、サーバーが内部的にどのようにレスポンスボディを準備し、送信するかに影響を与えます。

1.  **`Content-Type` ヘッダの有無**:
    *   **設定されている場合**: サーバーは指定されたメディアタイプでレスポンスを送信します。クライアントはコンテンツタイプを推測する必要がありません。
    *   **設定されていない場合**: サーバーはレスポンスボディの最初の数バイトを「スニッフィング」して、適切な `Content-Type` を推測しようとすることがあります。このスニッフィング処理にはわずかなオーバーヘッドが発生する可能性があります。
2.  **`Content-Length` ヘッダの有無**:
    *   **設定されている場合**: サーバーはレスポンスボディの正確なサイズをクライアントに通知します。これにより、クライアントはレスポンス全体の受信を効率的に管理できます。サーバーは通常、ボディ全体をバッファリングしてから送信します。
    *   **設定されていない場合**: サーバーは通常、チャンク転送エンコーディングを使用してレスポンスを送信します。これは、レスポンスボディのサイズが事前に不明な場合や、動的に生成される場合に便利ですが、各チャンクのサイズ情報を追加するオーバーヘッドが発生します。また、サーバーはレスポンスボディをバッファリングせずにストリームとして送信できます。

このコミットでは、以下の4つのシナリオをベンチマークで測定しています。

*   **`Content-Type` と `Content-Length` の両方が設定されている場合**: 最も効率的なケースが期待されます。サーバーはスニッフィングやチャンク転送の必要がなく、直接レスポンスを送信できます。
*   **`Content-Type` は設定されているが、`Content-Length` が設定されていない場合**: コンテンツスニッフィングは不要ですが、チャンク転送エンコーディングが使用される可能性があります。
*   **`Content-Length` は設定されているが、`Content-Type` が設定されていない場合**: コンテンツスニッフィングが発生し、そのオーバーヘッドが測定されます。`Content-Length` があるため、チャンク転送は不要です。
*   **`Content-Type` と `Content-Length` の両方が設定されていない場合**: 最もオーバーヘッドが大きいケースが期待されます。コンテンツスニッフィングとチャンク転送エンコーディングの両方が発生する可能性があります。

ベンチマークは、約2KBの固定サイズのレスポンスボディを使用しています。これは、`net/http` サーバーがチャンク転送を開始する前の内部バッファリング閾値(通常は2KB)を考慮したサイズです。これにより、バッファリングの挙動がベンチマーク結果に与える影響を適切に評価できます。

`benchmarkHandler` ヘルパー関数は、実際のHTTPサーバーの動作をシミュレートするために、カスタムの `io.Reader` (`repeatReader`) と `io.Writer` (`ioutil.Discard`) を使用したテスト用のコネクション (`rwTestConn`) を作成します。これにより、ネットワークI/Oの実際のオーバーヘッドを排除し、純粋なハンドラ処理の性能を測定することが可能になります。

## コアとなるコードの変更箇所

変更は `src/pkg/net/http/serve_test.go` ファイルに追加されています。

具体的には、以下の新しいベンチマーク関数とヘルパー関数が追加されました。

*   `const someResponse = "<html>some response</html>"`
*   `var response = bytes.Repeat([]byte(someResponse), 2<<10/len(someResponse))`
*   `func BenchmarkServerHandlerTypeLen(b *testing.B)`
*   `func BenchmarkServerHandlerNoLen(b *testing.B)`
*   `func BenchmarkServerHandlerNoType(b *testing.B)`
*   `func BenchmarkServerHandlerNoHeader(b *testing.B)`
*   `func benchmarkHandler(b *testing.B, h Handler)`

## コアとなるコードの解説

### `someResponse` と `response` 変数

```go
const someResponse = "<html>some response</html>"

// A Reponse that's just no bigger than 2KB, the buffer-before-chunking threshold.
var response = bytes.Repeat([]byte(someResponse), 2<<10/len(someResponse))

someResponse は、レスポンスボディの基本的な文字列を定義しています。 response 変数は、someResponse を繰り返し結合して作成されるバイトスライスです。そのサイズは 2<<10 (2KB) を len(someResponse) で割った回数だけ繰り返すことで、約2KBになるように調整されています。この2KBというサイズは、net/http サーバーがレスポンスボディをチャンク転送する前に内部的にバッファリングする閾値(通常2KB)を意識したものです。これにより、バッファリングの有無がベンチマーク結果に与える影響を適切に評価できます。

ベンチマーク関数群

すべてのベンチマーク関数は、benchmarkHandler ヘルパー関数を呼び出し、それぞれ異なる http.Handler の実装を渡しています。これらのハンドラは、http.ResponseWriter のヘッダを操作し、response 変数を書き込むことで、特定の Content-TypeContent-Length の組み合わせをシミュレートします。

  1. BenchmarkServerHandlerTypeLen:

    func BenchmarkServerHandlerTypeLen(b *testing.B) {
    	benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {
    		w.Header().Set("Content-Type", "text/html")
    		w.Header().Set("Content-Length", strconv.Itoa(len(response)))
    		w.Write(response)
    	}))
    }
    

    このベンチマークでは、Content-Type (text/html) と Content-Length (レスポンスボディの正確な長さ) の両方が明示的に設定されます。これは、サーバーがレスポンスを送信する際に、コンテンツスニッフィングやチャンク転送エンコーディングの処理が不要となる、最も効率的なシナリオを測定します。

  2. BenchmarkServerHandlerNoLen:

    func BenchmarkServerHandlerNoLen(b *testing.B) {
    	benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {
    		w.Header().Set("Content-Type", "text/html")
    		w.Write(response)
    	}))
    }
    

    このベンチマークでは、Content-Type は設定されますが、Content-Length は設定されません。サーバーはコンテンツスニッフィングを行う必要はありませんが、レスポンスボディのサイズが不明なため、チャンク転送エンコーディングが適用される可能性があります。これにより、チャンク転送のオーバーヘッドが測定されます。

  3. BenchmarkServerHandlerNoType:

    func BenchmarkServerHandlerNoType(b *testing.B) {
    	benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {
    		w.Header().Set("Content-Length", strconv.Itoa(len(response)))
    		w.Write(response)
    	}))
    }
    

    このベンチマークでは、Content-Length は設定されますが、Content-Type は設定されません。サーバーはレスポンスボディのサイズを知っていますが、Content-Type を推測するためにコンテンツスニッフィングを行う必要があります。これにより、コンテンツスニッフィングのオーバーヘッドが測定されます。

  4. BenchmarkServerHandlerNoHeader:

    func BenchmarkServerHandlerNoHeader(b *testing.B) {
    	benchmarkHandler(b, HandlerFunc(func(w ResponseWriter, r *Request) {
    		w.Write(response)
    	}))
    }
    

    このベンチマークでは、Content-TypeContent-Length のどちらも設定されません。サーバーはコンテンツスニッフィングを行い、さらにチャンク転送エンコーディングを使用する可能性があります。これは、最も多くの内部処理が発生する可能性のあるシナリオを測定します。

benchmarkHandler ヘルパー関数

func benchmarkHandler(b *testing.B, h Handler) {
	b.ReportAllocs()
	req := []byte(strings.Replace(`GET / HTTP/1.1
Host: golang.org

`, "\n", "\r\n", -1))
	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++
		h.ServeHTTP(rw, r)
	})
	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)
	}
}

この関数は、すべてのベンチマーク関数から呼び出される共通のヘルパーです。

  • b.ReportAllocs(): ベンチマーク実行中に発生したメモリ割り当ての統計を報告するように設定します。これにより、各シナリオでのメモリ使用量の違いも評価できます。
  • req 変数: 繰り返し送信されるHTTPリクエストのバイトスライスを定義しています。これは単純な GET / HTTP/1.1 リクエストです。
  • rwTestConn 構造体: net.Conn インターフェースを模倣したテスト用のコネクションです。
    • Reader: &repeatReader{content: req, count: b.N}: リクエストボディを b.N 回繰り返して読み込むカスタム io.Reader です。これにより、ベンチマークのイテレーション回数分だけリクエストが供給されます。
    • Writer: ioutil.Discard: レスポンスボディの書き込み先を ioutil.Discard に設定します。これにより、実際のネットワークI/OやディスクI/Oのオーバーヘッドを排除し、純粋なハンドラ処理の性能を測定できます。
    • closec: make(chan bool, 1): コネクションがクローズされたことを通知するためのチャネルです。
  • handled 変数: 実際にハンドラが処理したリクエストの数をカウントします。
  • handler 変数: 渡された h (ベンチマーク対象のハンドラ) をラップする http.HandlerFunc です。このラッパーは、リクエストが処理されるたびに handled カウンタをインクリメントします。
  • ln := &oneConnListener{conn: conn}: net.Listener インターフェースを模倣したテスト用のリスナーです。これは単一の rwTestConn を受け入れます。
  • go Serve(ln, handler): 新しいゴルーチンで http.Serve 関数を起動します。http.Serve は、指定されたリスナー (ln) から接続を受け入れ、各接続に対して handler を呼び出してリクエストを処理します。
  • <-conn.closec: rwTestConn がクローズされるまでブロックします。これにより、すべてのリクエストが処理されるのを待ちます。
  • if b.N != handled: 最終的に、ベンチマークのイテレーション回数 (b.N) と実際に処理されたリクエスト数 (handled) が一致するかどうかを確認します。一致しない場合はエラーを報告します。

この benchmarkHandler 関数は、net/http サーバーの内部動作を可能な限り忠実にシミュレートしつつ、外部要因(実際のネットワーク遅延など)を排除することで、ハンドラ処理の純粋な性能を測定するための隔離された環境を提供しています。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント: net/http パッケージ
  • Go言語公式ドキュメント: testing パッケージ
  • HTTP/1.1 RFC (RFC 2616, 特に Content-Type, Content-Length, Chunked Transfer Encodingに関するセクション)
  • Go言語のベンチマークに関する一般的な記事やチュートリアル