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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージに、並列クライアント/サーバーベンチマークを追加するものです。具体的には、serve_test.go ファイルに、複数のゴルーチンを使用してHTTPクライアントとサーバー間のリクエスト/レスポンス処理のパフォーマンスを測定するための新しいベンチマーク関数が導入されました。これにより、net/http パッケージの並行処理能力とスケーラビリティを評価できるようになります。

コミット

commit 75af013229e9d22da8ed3272f9f936016b085365
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Aug 24 14:19:49 2012 +0400

    net/http: add parallel client/server benchmark
    R=bradfitz@golang.org
    
    R=bradfitz
    CC=bradfitz, dave, dsymonds, gobot, golang-dev
    https://golang.org/cl/6441134

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

https://github.com/golang/go/commit/75af013229e9d22da8ed3272f9f936016b085365

元コミット内容

このコミットは、net/http パッケージに並列クライアント/サーバーベンチマークを追加します。

変更の背景

ソフトウェアのパフォーマンス測定、特に並行処理が関わるシステムにおいては、単一のスレッドやプロセスでのテストだけでは不十分です。複数のクライアントが同時にサーバーにアクセスするシナリオをシミュレートすることで、システムが実際の負荷状況下でどのように振る舞うかを正確に評価できます。

このコミットが作成された2012年当時、Go言語はまだ比較的新しい言語であり、その並行処理モデル(ゴルーチンとチャネル)は大きな注目を集めていました。net/http パッケージはGoのWebアプリケーション開発の基盤となるため、そのパフォーマンス特性、特に高並行環境下での挙動を詳細に理解し、最適化することは非常に重要でした。

このベンチマークの追加は、以下のような目的があったと考えられます。

  • パフォーマンスボトルネックの特定: 多数の同時リクエストが処理される際に、net/http パッケージのどこにボトルネックがあるかを特定するため。
  • スケーラビリティの評価: ゴルーチンとネットワークI/Oがどのように連携し、システムがどれだけ多くの同時接続を効率的に処理できるかを評価するため。
  • 回帰テスト: 将来の変更が net/http の並行パフォーマンスに悪影響を与えないことを確認するための基準として。
  • 最適化の指針: ベンチマーク結果に基づいて、コードの最適化や設計改善の方向性を決定するため。

前提知識の解説

Go言語のベンチマーク

Go言語には、標準の testing パッケージにベンチマーク機能が組み込まれています。Benchmark というプレフィックスを持つ関数を定義し、go test -bench=. コマンドで実行することで、コードのパフォーマンスを測定できます。

  • *testing.B: ベンチマーク関数に渡される構造体で、ベンチマークの制御(タイマーの開始/停止、ループ回数 b.N)を行います。
  • b.N: ベンチマークが実行されるイテレーション回数。go test コマンドが自動的に調整し、統計的に有意な結果が得られるようにします。
  • b.StopTimer() / b.StartTimer(): 測定対象外のセットアップ処理などでタイマーを一時停止/再開するために使用します。

net/http パッケージ

Go言語の標準HTTPクライアントおよびサーバーの実装を提供します。

  • http.HandlerFunc: HTTPリクエストを処理するための関数を http.Handler インターフェースに適合させるためのアダプター。
  • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェース。
  • http.Request: 受信したHTTPリクエストを表す構造体。
  • http.Get(url string): 指定されたURLにGETリクエストを送信し、レスポンスを返します。

httptest パッケージ

HTTPテストを容易にするためのユーティリティを提供します。

  • httptest.NewServer(handler http.Handler): テスト目的でHTTPサーバーを起動し、そのURLを返します。実際のネットワークポートを使用するため、統合テストやベンチマークに適しています。テスト終了時に Close() メソッドを呼び出してサーバーを停止する必要があります。

sync パッケージ

Goの並行処理を同期するためのプリミティブを提供します。

  • sync.WaitGroup: 複数のゴルーチンの完了を待つために使用されます。
    • Add(delta int): カウンターに delta を追加します。
    • Done(): カウンターを1減らします。
    • Wait(): カウンターがゼロになるまでブロックします。

sync/atomic パッケージ

低レベルのアトミック操作を提供します。

  • atomic.AddInt32(addr *int32, delta int32): *addrdelta をアトミックに加算し、新しい値を返します。複数のゴルーチンから共有されるカウンターを安全に操作するために使用されます。

runtime パッケージ

Goランタイムとの相互作用のための関数を提供します。

  • runtime.GOMAXPROCS(n int): 同時に実行できるOSスレッドの最大数を設定または取得します。n < 1 の場合、現在の設定を返します。この値は、Goのスケジューラが同時に実行できるゴルーチンの数を制限します。

技術的詳細

このコミットで追加された並列ベンチマーク benchmarkClientServerParallel は、以下の技術的アプローチを採用しています。

  1. テスト用HTTPサーバーの起動: httptest.NewServer を使用して、シンプルな "Hello world." を返すHTTPサーバーを起動します。これにより、実際のネットワークスタックを介した通信をシミュレートできます。
  2. 並行度 (Concurrency) の設定: ベンチマーク関数は conc パラメータを受け取ります。これは、runtime.GOMAXPROCS(-1) (現在の論理CPU数) に conc を乗算することで、同時に実行されるクライアントゴルーチンの総数 numProcs を決定します。これにより、システムのCPUリソースを最大限に活用し、高負荷状態をシミュレートします。
  3. sync.WaitGroup によるゴルーチンの同期: numProcs 個のゴルーチンが起動され、それぞれがHTTPリクエストを送信します。sync.WaitGroup を使用して、すべてのゴルーチンが処理を完了するまでベンチマーク関数が待機するようにします。これにより、すべてのリクエストが完了してからベンチマークの測定が終了することを保証します。
  4. atomic.AddInt32 によるリクエスト数の管理: b.N (ベンチマークのイテレーション回数) は、すべてのゴルーチンで共有される n という int32 型の変数で管理されます。各ゴルーチンは atomic.AddInt32(&n, -1) を呼び出すことで、アトミックに n をデクリメントします。これにより、複数のゴルーチンが同時に n を更新しようとしても競合状態が発生せず、正確に b.N 回のリクエストが送信されることが保証されます。
  5. HTTPリクエストの実行と検証: 各ゴルーチンは http.Get(ts.URL) を使用してサーバーにリクエストを送信し、ioutil.ReadAll でレスポンスボディを読み込みます。レスポンスボディが期待される "Hello world.\n" であることを検証し、異なる場合は panic を発生させます。エラーが発生した場合は b.Logf でログを出力しますが、ベンチマークの続行を妨げないように continue します。
  6. タイマーの制御: b.StopTimer()b.StartTimer() を使用して、サーバーのセットアップやゴルーチンの起動などのオーバーヘッドがベンチマークの測定時間に含まれないようにします。

この設計により、Goの並行処理モデルを最大限に活用し、実際のWebサーバーが直面するような高並行リクエスト環境下での net/http パッケージのパフォーマンスを正確に測定することが可能になります。

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

src/pkg/net/http/serve_test.go ファイルに以下のコードが追加されました。

func BenchmarkClientServerParallel4(b *testing.B) {
	benchmarkClientServerParallel(b, 4)
}

func BenchmarkClientServerParallel64(b *testing.B) {
	benchmarkClientServerParallel(b, 64)
}

func benchmarkClientServerParallel(b *testing.B, conc int) {
	b.StopTimer()
	ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
		fmt.Fprintf(rw, "Hello world.\n")
	}))
	defer ts.Close()
	b.StartTimer()

	numProcs := runtime.GOMAXPROCS(-1) * conc
	var wg sync.WaitGroup
	wg.Add(numProcs)
	n := int32(b.N)
	for p := 0; p < numProcs; p++ {
		go func() {
			for atomic.AddInt32(&n, -1) >= 0 {
				res, err := Get(ts.URL)
				if err != nil {
					b.Logf("Get: %v", err)
					continue
				}
				all, err := ioutil.ReadAll(res.Body)
				if err != nil {
					b.Logf("ReadAll: %v", err)
					continue
				}
				body := string(all)
				if body != "Hello world.\\n" {
					panic("Got body: " + body)
				}
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

また、必要なパッケージがインポートされています。

import (
	"runtime"
	"sync"
	"sync/atomic"
)

コアとなるコードの解説

BenchmarkClientServerParallel4BenchmarkClientServerParallel64

これらは、Goのベンチマークツールによって自動的に検出・実行されるベンチマーク関数です。それぞれ benchmarkClientServerParallel 関数を呼び出し、並行度 (conc) を 464 に設定しています。これにより、異なる並行度でのパフォーマンスを簡単に測定できます。

benchmarkClientServerParallel(b *testing.B, conc int)

この関数が並列ベンチマークの主要なロジックを含んでいます。

  1. b.StopTimer(): ベンチマークのタイマーを一時停止します。これは、サーバーのセットアップやゴルーチンの起動など、測定対象外の初期化処理の時間を計測に含めないためです。
  2. ts := httptest.NewServer(...): テスト用のHTTPサーバーを起動します。このサーバーは、すべてのリクエストに対して "Hello world.\n" という文字列を返します。
    • defer ts.Close(): 関数が終了する際に、起動したHTTPサーバーを確実にシャットダウンするための defer ステートメントです。
  3. b.StartTimer(): 初期化処理が完了し、実際のベンチマーク対象のコードが実行される直前にタイマーを再開します。
  4. numProcs := runtime.GOMAXPROCS(-1) * conc:
    • runtime.GOMAXPROCS(-1): 現在の GOMAXPROCS の値(Goランタイムが同時に実行できるOSスレッドの最大数、通常は論理CPU数)を取得します。
    • この値に conc (並行度) を乗算することで、起動するゴルーチンの総数を決定します。これにより、システムのリソースを考慮した上で、指定された並行度でリクエストを生成するクライアントの数を設定します。
  5. var wg sync.WaitGroup: 複数のゴルーチンの完了を待つための WaitGroup を宣言します。
  6. wg.Add(numProcs): WaitGroup のカウンターを numProcs に設定します。これは、numProcs 個のゴルーチンが完了するのを待つことを意味します。
  7. n := int32(b.N): ベンチマークのイテレーション回数 b.Nint32 型の変数 n にコピーします。この n は、すべてのクライアントゴルーチンで共有され、アトミックにデクリメントされます。
  8. for p := 0; p < numProcs; p++ { go func() { ... } }: numProcs 個のゴルーチンを起動します。各ゴルーチンは以下の処理をループで実行します。
    • for atomic.AddInt32(&n, -1) >= 0:
      • atomic.AddInt32(&n, -1): 共有変数 n の値をアトミックに1減らし、その結果の値を返します。これにより、複数のゴルーチンが同時に n を更新しようとしても、競合状態が発生せず、正確なカウントが保証されます。
      • ループは、n が0以上である限り(つまり、まだ処理すべきリクエストが残っている限り)続行されます。
    • res, err := Get(ts.URL): テストサーバーのURLに対してHTTP GETリクエストを送信します。
    • エラーハンドリング: Getioutil.ReadAll でエラーが発生した場合、b.Logf でログを出力し、現在のリクエストの処理をスキップして次のリクエストに進みます。これにより、一時的なネットワーク問題などがベンチマーク全体を停止させるのを防ぎます。
    • all, err := ioutil.ReadAll(res.Body): レスポンスボディをすべて読み込みます。
    • body := string(all): 読み込んだバイトスライスを文字列に変換します。
    • if body != "Hello world.\\n" { panic("Got body: " + body) }: レスポンスボディが期待される文字列と異なる場合、panic を発生させます。これは、テストサーバーが正しく動作していないか、通信中にデータが破損したことを示します。
    • wg.Done(): ゴルーチンがすべてのリクエストの処理を完了したら、WaitGroup のカウンターを1減らします。
  9. wg.Wait(): すべてのクライアントゴルーチンが wg.Done() を呼び出し、WaitGroup のカウンターがゼロになるまで、この関数はブロックします。これにより、すべてのリクエストが処理されるまでベンチマークが終了しないことが保証されます。

このコードは、Goの並行処理の強力な機能を活用し、net/http パッケージの並行パフォーマンスを厳密に測定するための堅牢なフレームワークを提供しています。

関連リンク

参考にした情報源リンク