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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージのテストファイル src/pkg/net/http/serve_test.go に変更を加えています。具体的には、HTTPサーバーのパフォーマンスベンチマークに関する新しいテストケースが追加されています。

コミット

  • コミットハッシュ: afeaf554aa5dd34b0def3f18a37dd500aebd0695
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Mon Jun 4 08:04:40 2012 -0700

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

https://github.com/golang/go/commit/afeaf554aa5dd34b0def3f18a37dd500aebd0695

元コミット内容

net/http: add new Server benchmark

The new BenchmarkServer avoids profiling the client code
by running it in a child process.

R=rsc
CC=golang-dev
https://golang.org/cl/6260053

変更の背景

Go言語の net/http パッケージは、WebサーバーやHTTPクライアントを構築するための基本的な機能を提供します。パフォーマンスの最適化は、このような基盤となるライブラリにとって非常に重要です。

既存のベンチマーク BenchmarkClientServer は、クライアントとサーバーの両方のコードパスをプロファイリングしていました。しかし、サーバー側のパフォーマンスのみを正確に測定し、最適化するためには、クライアント側のコードがプロファイリング結果に与える影響を排除する必要がありました。

このコミットの目的は、HTTPサーバー自体のパフォーマンスをより純粋に測定するための新しいベンチマーク BenchmarkServer を追加することです。これにより、サーバー側のボトルネックを特定し、改善するためのより正確なプロファイリングデータを得ることが可能になります。特に、クライアントコードがプロファイリング結果に混入することを避けるために、クライアントを別の子プロセスで実行するというアプローチが採用されました。

前提知識の解説

Go言語のベンチマークテスト

Go言語には、標準ライブラリの testing パッケージにベンチマークテストの機能が組み込まれています。

  • ベンチマーク関数は BenchmarkXxx(*testing.B) というシグネチャを持ちます。
  • testing.B 型は、ベンチマークの実行回数 (b.N) やタイマーの制御 (b.StartTimer(), b.StopTimer()) などの機能を提供します。
  • ベンチマークは go test -bench=. のように実行され、go test -bench=. -cpuprofile=cpu.prof のように cpuprofile オプションを指定することでCPUプロファイリングデータを生成できます。

net/http/httptest パッケージ

net/http/httptest パッケージは、HTTPサーバーやクライアントのテストを容易にするためのユーティリティを提供します。

  • httptest.NewServer(handler http.Handler): 実際のネットワークポートをリッスンするテスト用のHTTPサーバーを起動し、そのURLを返します。これにより、実際のHTTPリクエストをサーバーに送信してテストを行うことができます。

os/exec パッケージ

os/exec パッケージは、外部コマンドを実行するための機能を提供します。

  • exec.Command(name string, arg ...string): 指定されたコマンドと引数を持つ Cmd 構造体を返します。
  • cmd.CombinedOutput(): コマンドを実行し、標準出力と標準エラー出力を結合したバイトスライスを返します。
  • cmd.Env: 実行するコマンドの環境変数を設定するためのフィールドです。

プロファイリング

プロファイリングとは、プログラムの実行中にそのパフォーマンス特性(CPU使用率、メモリ使用量など)を測定し、ボトルネックを特定するプロセスです。Go言語には、pprof ツールが標準で提供されており、CPUプロファイルやメモリプロファイルなどを視覚化して分析することができます。

技術的詳細

このコミットで追加された BenchmarkServer は、以下の技術的なアプローチでサーバーのプロファイリングからクライアントコードの影響を排除しています。

  1. 子プロセスでのクライアント実行:

    • メインのベンチマークプロセス(親プロセス)は、HTTPサーバーを起動し、そのURLを子プロセスに渡します。
    • クライアントコードは、親プロセスと同じ実行可能ファイル(os.Args[0] で参照される)を exec.Command を使って子プロセスとして起動することで実行されます。
    • 子プロセスは、特定の環境変数(TEST_BENCH_SERVER_URLTEST_BENCH_CLIENT_N)が設定されているかどうかをチェックし、それらが設定されていればクライアントとして動作します。
  2. 環境変数による通信:

    • 親プロセスは、b.N (ベンチマークの実行回数) と httptest.NewServer で起動したサーバーのURLを環境変数として子プロセスに渡します。
    • fmt.Sprintf("TEST_BENCH_CLIENT_N=%d", b.N)fmt.Sprintf("TEST_BENCH_SERVER_URL=%s", ts.URL) のように環境変数を設定し、cmd.Env に追加しています。
  3. プロファイリングの分離:

    • 親プロセスで go test -cpuprofile=http.prof のようにプロファイリングを実行すると、そのプロファイルは親プロセス(サーバー側)のCPU使用率を主に記録します。
    • クライアントコードは別の子プロセスで実行されるため、そのCPU使用率は親プロセスのプロファイルには含まれません。これにより、サーバーコードの純粋なプロファイリングが可能になります。
  4. ベンチマークの制御:

    • b.StopTimer()b.StartTimer() を使用して、サーバーのセットアップ(httptest.NewServer の起動など)にかかる時間をベンチマークの測定から除外しています。
    • 子プロセスでのクライアントの実行が完了した後、親プロセスは子プロセスの終了を待ち、エラーがないかを確認します。

この手法により、go tool pprof で生成されるプロファイルデータは、net/http パッケージのサーバー側の処理に集中したものとなり、より的確なパフォーマンス分析と最適化が可能になります。

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

src/pkg/net/http/serve_test.go に以下のコードが追加されています。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -20,7 +20,9 @@ import (
 	"net/http/httputil"
 	"net/url"
 	"os"
+	"os/exec"
 	"reflect"
+	"strconv"
 	"strings"
 	"syscall"
 	"testing"
@@ -1262,3 +1264,56 @@ func BenchmarkClientServer(b *testing.B) {
 
 	b.StopTimer()
 }
+
+// A benchmark for profiling the server without the HTTP client code.
+// The client code runs in a subprocess.
+//
+// For use like:
+//   $ go test -c
+//   $ ./http.test -test.run=XX -test.bench=Benchmarktime=15 -test.cpuprofile=http.prof
+//   $ go tool pprof http.test http.prof
+//   (pprof) web
+func BenchmarkServer(b *testing.B) {
+	// Child process mode;
+	if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" {
+		n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N"))
+		if err != nil {
+			panic(err)
+		}
+		for i := 0; i < n; i++ {
+			res, err := Get(url)
+			if err != nil {
+				log.Panicf("Get:", err)
+			}
+			all, err := ioutil.ReadAll(res.Body)
+			if err != nil {
+				log.Panicf("ReadAll:", err)
+			}
+			body := string(all)
+			if body != "Hello world.\n" {
+				log.Panicf("Got body:", body)
+			}
+		}
+		os.Exit(0)
+		return
+	}
+
+	var res = []byte("Hello world.\n")
+	b.StopTimer()
+	ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
+		rw.Header().Set("Content-Type", "text/html; charset=utf-8")
+		rw.Write(res)
+	}))
+	defer ts.Close()
+	b.StartTimer()
+
+	cmd := exec.Command(os.Args[0], "-test.run=XXXX", "-test.bench=BenchmarkServer")
+	cmd.Env = append([]string{
+		fmt.Sprintf("TEST_BENCH_CLIENT_N=%d", b.N),
+		fmt.Sprintf("TEST_BENCH_SERVER_URL=%s", ts.URL),
+	}, os.Environ()...)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		b.Errorf("Test failure: %v, with output: %s", err, out)
+	}
+}

コアとなるコードの解説

新しく追加された BenchmarkServer 関数は、大きく分けて2つの実行パスを持っています。

  1. 子プロセス(クライアント)としての実行パス:

    if url := os.Getenv("TEST_BENCH_SERVER_URL"); url != "" {
        n, err := strconv.Atoi(os.Getenv("TEST_BENCH_CLIENT_N"))
        // ... (エラーハンドリング)
        for i := 0; i < n; i++ {
            res, err := Get(url) // HTTP GETリクエストを送信
            // ... (レスポンスの読み込みと検証)
        }
        os.Exit(0) // 処理が完了したら子プロセスを終了
        return
    }
    

    このブロックは、環境変数 TEST_BENCH_SERVER_URL が設定されている場合に実行されます。これは、このプロセスが親プロセスによって起動された「クライアント」であることを示します。

    • os.Getenv("TEST_BENCH_SERVER_URL") からサーバーのURLを取得します。
    • os.Getenv("TEST_BENCH_CLIENT_N") からリクエストの実行回数 n を取得します。
    • for ループ内で Get(url) を呼び出し、指定されたURLに対してHTTP GETリクエストを n 回送信します。
    • レスポンスボディが期待通り ("Hello world.\n") であることを検証します。
    • 処理が完了したら os.Exit(0) で子プロセスを正常終了させます。
  2. 親プロセス(サーバーとベンチマーク制御)としての実行パス:

    var res = []byte("Hello world.\n")
    b.StopTimer() // セットアップ時間をベンチマークから除外
    ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
        rw.Header().Set("Content-Type", "text/html; charset=utf-8")
        rw.Write(res) // "Hello world.\n" を返すハンドラ
    }))
    defer ts.Close() // ベンチマーク終了時にサーバーをクローズ
    b.StartTimer() // ベンチマーク測定開始
    
    cmd := exec.Command(os.Args[0], "-test.run=XXXX", "-test.bench=BenchmarkServer")
    cmd.Env = append([]string{
        fmt.Sprintf("TEST_BENCH_CLIENT_N=%d", b.N),
        fmt.Sprintf("TEST_BENCH_SERVER_URL=%s", ts.URL),
    }, os.Environ()...) // 環境変数を設定
    out, err := cmd.CombinedOutput() // 子プロセスを実行し、出力を取得
    if err != nil {
        b.Errorf("Test failure: %v, with output: %s", err, out) // エラーハンドリング
    }
    

    このブロックは、TEST_BENCH_SERVER_URL 環境変数が設定されていない場合に実行されます。これは、このプロセスが「親」であり、ベンチマークを制御する役割を担っていることを示します。

    • httptest.NewServer を使用して、シンプルなHTTPサーバーを起動します。このサーバーは、すべてのリクエストに対して "Hello world.\n" というテキストを返します。
    • b.StopTimer()b.StartTimer() を使って、サーバーの起動にかかる時間をベンチマークの測定対象から除外します。
    • exec.Command(os.Args[0], ...) を使用して、自分自身(現在のテスト実行可能ファイル)を子プロセスとして起動します。-test.run=XXXX は、子プロセスがテストを実行しないようにするためのダミーの引数です。-test.bench=BenchmarkServer は、子プロセスが BenchmarkServer 関数内のクライアントロジックを実行するように指示します。
    • cmd.Env に、子プロセスがクライアントとして動作するために必要な環境変数(リクエスト回数 b.N とサーバーURL ts.URL)を設定します。既存の環境変数も引き継ぎます。
    • cmd.CombinedOutput() を呼び出して子プロセスを実行し、その標準出力と標準エラー出力を取得します。
    • 子プロセスの実行中にエラーが発生した場合、b.Errorf を使ってベンチマークエラーとして報告します。

この巧妙な設計により、親プロセスはサーバーのプロファイリングに集中し、クライアントの負荷生成は独立した子プロセスに任せることで、より正確なサーバーパフォーマンスの測定を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (testing, net/http/httptest, os/exec パッケージ)
  • Go言語のプロファイリングに関するドキュメント (go tool pprof)
  • Go言語のベンチマークに関する一般的な情報