[インデックス 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)
:*addr
にdelta
をアトミックに加算し、新しい値を返します。複数のゴルーチンから共有されるカウンターを安全に操作するために使用されます。
runtime
パッケージ
Goランタイムとの相互作用のための関数を提供します。
runtime.GOMAXPROCS(n int)
: 同時に実行できるOSスレッドの最大数を設定または取得します。n < 1
の場合、現在の設定を返します。この値は、Goのスケジューラが同時に実行できるゴルーチンの数を制限します。
技術的詳細
このコミットで追加された並列ベンチマーク benchmarkClientServerParallel
は、以下の技術的アプローチを採用しています。
- テスト用HTTPサーバーの起動:
httptest.NewServer
を使用して、シンプルな "Hello world." を返すHTTPサーバーを起動します。これにより、実際のネットワークスタックを介した通信をシミュレートできます。 - 並行度 (Concurrency) の設定: ベンチマーク関数は
conc
パラメータを受け取ります。これは、runtime.GOMAXPROCS(-1)
(現在の論理CPU数) にconc
を乗算することで、同時に実行されるクライアントゴルーチンの総数numProcs
を決定します。これにより、システムのCPUリソースを最大限に活用し、高負荷状態をシミュレートします。 sync.WaitGroup
によるゴルーチンの同期:numProcs
個のゴルーチンが起動され、それぞれがHTTPリクエストを送信します。sync.WaitGroup
を使用して、すべてのゴルーチンが処理を完了するまでベンチマーク関数が待機するようにします。これにより、すべてのリクエストが完了してからベンチマークの測定が終了することを保証します。atomic.AddInt32
によるリクエスト数の管理:b.N
(ベンチマークのイテレーション回数) は、すべてのゴルーチンで共有されるn
というint32
型の変数で管理されます。各ゴルーチンはatomic.AddInt32(&n, -1)
を呼び出すことで、アトミックにn
をデクリメントします。これにより、複数のゴルーチンが同時にn
を更新しようとしても競合状態が発生せず、正確にb.N
回のリクエストが送信されることが保証されます。- HTTPリクエストの実行と検証: 各ゴルーチンは
http.Get(ts.URL)
を使用してサーバーにリクエストを送信し、ioutil.ReadAll
でレスポンスボディを読み込みます。レスポンスボディが期待される "Hello world.\n" であることを検証し、異なる場合はpanic
を発生させます。エラーが発生した場合はb.Logf
でログを出力しますが、ベンチマークの続行を妨げないようにcontinue
します。 - タイマーの制御:
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"
)
コアとなるコードの解説
BenchmarkClientServerParallel4
と BenchmarkClientServerParallel64
これらは、Goのベンチマークツールによって自動的に検出・実行されるベンチマーク関数です。それぞれ benchmarkClientServerParallel
関数を呼び出し、並行度 (conc
) を 4
と 64
に設定しています。これにより、異なる並行度でのパフォーマンスを簡単に測定できます。
benchmarkClientServerParallel(b *testing.B, conc int)
この関数が並列ベンチマークの主要なロジックを含んでいます。
b.StopTimer()
: ベンチマークのタイマーを一時停止します。これは、サーバーのセットアップやゴルーチンの起動など、測定対象外の初期化処理の時間を計測に含めないためです。ts := httptest.NewServer(...)
: テスト用のHTTPサーバーを起動します。このサーバーは、すべてのリクエストに対して "Hello world.\n" という文字列を返します。defer ts.Close()
: 関数が終了する際に、起動したHTTPサーバーを確実にシャットダウンするためのdefer
ステートメントです。
b.StartTimer()
: 初期化処理が完了し、実際のベンチマーク対象のコードが実行される直前にタイマーを再開します。numProcs := runtime.GOMAXPROCS(-1) * conc
:runtime.GOMAXPROCS(-1)
: 現在のGOMAXPROCS
の値(Goランタイムが同時に実行できるOSスレッドの最大数、通常は論理CPU数)を取得します。- この値に
conc
(並行度) を乗算することで、起動するゴルーチンの総数を決定します。これにより、システムのリソースを考慮した上で、指定された並行度でリクエストを生成するクライアントの数を設定します。
var wg sync.WaitGroup
: 複数のゴルーチンの完了を待つためのWaitGroup
を宣言します。wg.Add(numProcs)
:WaitGroup
のカウンターをnumProcs
に設定します。これは、numProcs
個のゴルーチンが完了するのを待つことを意味します。n := int32(b.N)
: ベンチマークのイテレーション回数b.N
をint32
型の変数n
にコピーします。このn
は、すべてのクライアントゴルーチンで共有され、アトミックにデクリメントされます。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リクエストを送信します。- エラーハンドリング:
Get
やioutil.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減らします。
wg.Wait()
: すべてのクライアントゴルーチンがwg.Done()
を呼び出し、WaitGroup
のカウンターがゼロになるまで、この関数はブロックします。これにより、すべてのリクエストが処理されるまでベンチマークが終了しないことが保証されます。
このコードは、Goの並行処理の強力な機能を活用し、net/http
パッケージの並行パフォーマンスを厳密に測定するための堅牢なフレームワークを提供しています。
関連リンク
- Go言語の
net/http
パッケージ公式ドキュメント: https://pkg.go.dev/net/http - Go言語の
testing
パッケージ公式ドキュメント (ベンチマークに関する記述を含む): https://pkg.go.dev/testing - Go言語の
httptest
パッケージ公式ドキュメント: https://pkg.go.dev/net/http/httptest - Go言語の
sync
パッケージ公式ドキュメント: https://pkg.go.dev/sync - Go言語の
sync/atomic
パッケージ公式ドキュメント: https://pkg.go.dev/sync/atomic - Go言語の
runtime
パッケージ公式ドキュメント: https://pkg.go.dev/runtime
参考にした情報源リンク
- Go言語のベンチマークに関する公式ブログ記事やチュートリアル (一般的な情報源として):
- Go言語の並行処理に関する一般的な情報源:
sync.WaitGroup
の使用例に関する情報源:sync/atomic
の使用例に関する情報源:httptest
の使用例に関する情報源:- https://gobyexample.com/http-servers (直接的ではないが、HTTPサーバーのテストに関連)
- Go言語の
GOMAXPROCS
に関する情報源: