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

[インデックス 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 機能を処理する際のオーバーヘッドを測定するように設計されています。

ベンチマークのセットアップは以下の要素を含んでいます。

  1. b.ReportAllocs(): これにより、ベンチマーク結果にメモリ割り当ての統計が含まれるようになります。これがこのベンチマークの主要な目的の一つです。
  2. リクエストの準備:
    • reqBytes ヘルパー関数(既存)を使用して、シンプルな GET / HTTP/1.1 リクエストのバイト列を準備します。これは、サーバーが処理を開始するための入力となります。
  3. ハンドラの定義:
    • http.HandlerFunc を使用して、Hijack を呼び出すだけのシンプルなHTTPハンドラを定義します。
    • ハンドラは w.(Hijacker).Hijack() を呼び出して接続をハイジャックし、すぐに conn.Close() で接続を閉じます。これは、Hijack 処理自体のオーバーヘッドを分離して測定するためです。
  4. テスト接続とリスナーのモック:
    • rwTestConn というテスト用の接続構造体(既存)を使用します。これは net.Conn インターフェースを実装しており、実際のネットワークI/Oを行わずに、メモリ内でリクエストとレスポンスをシミュレートできます。
      • Writer: ioutil.Discard: レスポンスの書き込みは破棄されます。
      • closec: make(chan bool, 1): 接続が閉じられたことを通知するためのチャネルです。
    • oneConnListener というテスト用のリスナー構造体(既存)を使用します。これは一度に一つの接続だけを提供するように設計されており、ベンチマークループ内で新しい接続をシミュレートするために使用されます。
  5. ベンチマークループ:
    • 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 機能のパフォーマンスを測定するためのベンチマークです。

  1. b.ReportAllocs():

    • この行は、ベンチマークの実行結果にメモリ割り当ての統計(バイト数と回数)を含めるようにGoのテストフレームワークに指示します。これにより、Hijack 処理がどれだけのメモリを消費しているかを定量的に把握できます。
  2. req := reqBytes(...):

    • HTTPリクエストの生バイト列を定義しています。これは、サーバーが処理する仮想的なクライアントリクエストを表します。シンプルなGETリクエストが使用されています。
  3. h := HandlerFunc(func(w ResponseWriter, r *Request) { ... }):

    • http.HandlerFunc 型の匿名関数として、HTTPハンドラを定義しています。
    • このハンドラの主要なロジックは w.(Hijacker).Hijack() の呼び出しです。これは、ResponseWriterHijacker インターフェースを実装していることをアサートし、その Hijack メソッドを呼び出して基盤となる接続の制御を奪います。
    • if err != nil { panic(err) } は、エラーが発生した場合にパニックを引き起こします。ベンチマークでは通常、エラーは発生しないと想定されますが、堅牢性のために含まれています。
    • conn.Close(): Hijack された接続は、ハンドラが責任を持って閉じる必要があります。このベンチマークでは、Hijack のオーバーヘッドのみを測定するため、すぐに接続を閉じています。
  4. conn := &rwTestConn{ ... }:

    • rwTestConn は、net.Conn インターフェースを実装するテスト用の構造体です。これにより、実際のネットワークソケットを使用せずに、メモリ内で接続の読み書きをシミュレートできます。
    • Writer: ioutil.Discard: 書き込み操作はすべて破棄されます。
    • closec: make(chan bool, 1): 接続が閉じられたことをハンドラが通知するためのチャネルです。これにより、ベンチマークループが各イテレーションの完了を待つことができます。
  5. ln := &oneConnListener{conn: conn}:

    • oneConnListener は、net.Listener インターフェースを実装するテスト用の構造体です。これは、Accept() メソッドが常に単一の事前定義された接続(conn)を返すように設計されています。
  6. 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 チャネルに値を送信するまで、ベンチマークループはここでブロックされます。これにより、各イテレーションが完全に完了するのを待ち、正確な時間と割り当ての測定を保証します。

このベンチマークは、Hijack 機能のメモリフットプリントと実行時間を効率的に測定するための、クリーンで分離されたテスト環境を提供しています。

関連リンク

  • Go言語の net/http パッケージドキュメント: https://pkg.go.dev/net/http
  • Go言語の testing パッケージドキュメント: https://pkg.go.dev/testing
  • Go言語のベンチマークに関する公式ブログ記事やドキュメント(一般的な情報源として)

参考にした情報源リンク