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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージにTCP通信のベンチマークテストを追加するものです。具体的には、src/pkg/net/tcp_test.goという新しいファイルが追加され、TCP接続の確立、データの送受信、接続の終了といった一連の操作にかかる時間を測定するためのベンチマーク関数が定義されています。これにより、GoのネットワークスタックにおけるTCP通信のパフォーマンス特性を詳細に評価し、将来的な最適化の基盤を提供します。

コミット

このコミットは、Go言語のnetパッケージにTCP通信のベンチマークを追加し、その初期測定結果を報告しています。追加されたベンチマークは、単一の接続でメッセージを送受信する「ワンショット」モードと、複数のメッセージを単一の接続で送受信する「永続的」モードの両方をカバーしており、それぞれにタイムアウト設定の有無による影響も測定しています。これにより、GoのTCP実装のパフォーマンス特性を多角的に評価することが可能になります。

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

https://github.com/golang/go/commit/922056d410bd8b1f75a7f6ea2b8664f98e574c2f

元コミット内容

commit 922056d410bd8b1f75a7f6ea2b8664f98e574c2f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Aug 20 21:27:52 2012 +0400

    net: add TCP benchmarks
    Current results on linux_amd64, 8 HT cores @2.4GHz:
    BenchmarkTCPOneShot        10000            194037 ns/op
    BenchmarkTCPOneShot-2      20000             93641 ns/op
    BenchmarkTCPOneShot-4      20000             94039 ns/op
    BenchmarkTCPOneShot-8      20000             94667 ns/op
    BenchmarkTCPOneShot-16     10000            301924 ns/op
    BenchmarkTCPOneShotTimeout         10000            193264 ns/op
    BenchmarkTCPOneShotTimeout-2       20000             98247 ns/op
    BenchmarkTCPOneShotTimeout-4       20000             94442 ns/op
    BenchmarkTCPOneShotTimeout-8       20000             95297 ns/op
    BenchmarkTCPOneShotTimeout-16      10000            307970 ns/op
    BenchmarkTCPPersistent     50000             52050 ns/op
    BenchmarkTCPPersistent-2          100000             29452 ns/op
    BenchmarkTCPPersistent-4          100000             28823 ns/op
    BenchmarkTCPPersistent-8           50000             30473 ns/op
    BenchmarkTCPPersistent-16          10000            311777 ns/op
    BenchmarkTCPPersistentTimeout      50000             32574 ns/op
    BenchmarkTCPPersistentTimeout-2    50000             29723 ns/op
    BenchmarkTCPPersistentTimeout-4   100000             28592 ns/op
    BenchmarkTCPPersistentTimeout-8   100000             28997 ns/op
    BenchmarkTCPPersistentTimeout-16           10000            314354 ns/op
    
    R=golang-dev, alex.brainman, dave, mikioh.mikioh, r, iant, bradfitz, iant
    CC=golang-dev
    https://golang.org/cl/6458128

変更の背景

Go言語は、その並行処理の強みと効率的なネットワークプログラミング能力により、サーバーサイドアプリケーションやネットワークサービス開発において広く利用されています。このような用途において、基盤となるネットワークスタック、特にTCP通信のパフォーマンスは極めて重要です。

このコミットが追加された背景には、GoのnetパッケージにおけるTCP通信の性能特性を客観的に把握し、ボトルネックを特定し、将来的な改善のための基準値を設けるという目的があります。ベンチマークは、コードの変更がパフォーマンスに与える影響を定量的に評価するための不可欠なツールです。特に、異なる接続パターン(ワンショット vs. 永続的)やタイムアウト設定の有無といったシナリオを網羅することで、より現実的な使用状況に近い条件下での性能評価が可能になります。

これにより、開発者はGoのネットワークスタックの強みと弱みを理解し、より効率的なアプリケーションを構築するための知見を得ることができます。また、Go言語自体の進化の過程で、ネットワーク関連の最適化が行われた際に、その効果を測定するための基盤としても機能します。

前提知識の解説

1. Go言語のtestingパッケージとベンチマーク

Go言語には、テストとベンチマークのための標準パッケージtestingが用意されています。

  • テスト関数: func TestXxx(*testing.T)という形式で定義され、コードの正確性を検証します。
  • ベンチマーク関数: func BenchmarkXxx(*testing.B)という形式で定義され、コードのパフォーマンスを測定します。
    • *testing.B型は、ベンチマーク実行に関する情報と制御を提供します。
    • b.N: ベンチマーク関数が実行されるイテレーション回数を示します。go test -benchコマンドを実行すると、testingパッケージが自動的にb.Nの値を調整し、統計的に有意な結果が得られるようにします。
    • ns/op: 1操作あたりのナノ秒。ベンチマーク結果の主要な指標の一つで、値が小さいほど高性能であることを示します。
    • go test -bench .: カレントディレクトリ内のすべてのベンチマークを実行します。
    • go test -bench=BenchmarkTCPOneShot: 特定のベンチマークのみを実行します。
    • go test -bench=. -benchmem: メモリ割り当てに関する情報も表示します。

2. TCP/IP通信の基本

  • TCP (Transmission Control Protocol): インターネットプロトコルスイートの一部であり、信頼性の高い、コネクション指向のデータ転送サービスを提供します。データの順序保証、再送制御、フロー制御、輻輳制御などを行います。
  • ソケット: ネットワーク通信のエンドポイントです。アプリケーションはソケットを通じてネットワークとデータの送受信を行います。
  • サーバー: クライアントからの接続要求を待ち受け、接続が確立された後にデータ通信を行います。
    • Listen: 特定のアドレスとポートで接続を待ち受けるソケットを作成します。
    • Accept: クライアントからの接続要求を受け入れ、新しい接続ソケットを返します。
  • クライアント: サーバーに接続を確立し、データ通信を行います。
    • Dial: 指定されたアドレスとポートに接続を試みます。
  • コネクション (Connection): TCPにおいて、クライアントとサーバー間で確立される論理的な通信経路です。データはコネクションを通じて送受信されます。
  • メッセージ: 通信されるデータの単位。このベンチマークでは、固定長のメッセージ(512バイト)が使用されています。

3. Go言語のネットワークプログラミング (netパッケージ)

Goの標準ライブラリnetパッケージは、ネットワークI/Oのプリミティブを提供します。

  • net.Listen(network, address string): 指定されたネットワークプロトコル(例: "tcp")とアドレスで接続を待ち受けるListenerを返します。
  • net.Dial(network, address string): 指定されたネットワークプロトコルとアドレスに接続を確立し、Connインターフェースを返します。
  • net.Connインターフェース: ネットワーク接続を表し、ReadWriteCloseLocalAddrRemoteAddrSetDeadlineなどのメソッドを提供します。
  • c.SetDeadline(t time.Time): 接続の読み書き操作の期限を設定します。期限を過ぎると、操作はエラーを返します。

4. Go言語の並行処理

  • Goroutine: Goランタイムによって管理される軽量なスレッドです。goキーワードを使って関数呼び出しの前に置くことで、その関数を新しいgoroutineで実行できます。
  • Channel: goroutine間で値を安全に送受信するための通信メカニズムです。チャネルは、並行処理における同期と通信を簡潔に記述するために使用されます。
    • バッファなしチャネル: 送信側と受信側が同時に準備ができていないとブロックします。
    • バッファありチャネル: 指定された数の値をバッファに格納できます。バッファが満杯になると送信はブロックされ、バッファが空になると受信はブロックされます。
  • runtime.GOMAXPROCS(-1): 現在のGOMAXPROCSの値を返します。GOMAXPROCSは、Goランタイムが同時に実行できるOSスレッドの最大数を制御します。-1を渡すと、現在の設定値が返されますが、このコミットの時点では、Go 1.5以前のデフォルト値は1でした。Go 1.5以降では、デフォルトで利用可能なCPUコア数に設定されます。このベンチマークでは、runtime.GOMAXPROCS(-1) * 16という計算で並行処理の数を決定しており、これは利用可能なCPUリソースを最大限に活用しようとする意図を示しています。

技術的詳細

このコミットで追加されたTCPベンチマークは、benchmarkTCPという単一の汎用関数を中心に構築されており、以下の4つの異なるシナリオをカバーしています。

  1. BenchmarkTCPOneShot:

    • persistent = false, timeout = false
    • 各操作(b.N回)ごとに新しいTCP接続を確立し、1つのメッセージを送受信し、接続を閉じます。
    • 接続の確立と終了のオーバーヘッドが支配的になります。
  2. BenchmarkTCPOneShotTimeout:

    • persistent = false, timeout = true
    • BenchmarkTCPOneShotと同様ですが、接続にSetDeadlineを設定します。このSetDeadlineは非常に長い時間(1時間)に設定されているため、実際にタイムアウトが発生することは意図されておらず、SetDeadline呼び出し自体のオーバーヘッドを測定します。
  3. BenchmarkTCPPersistent:

    • persistent = true, timeout = false
    • 少数の永続的なTCP接続(numConcurrentで定義される数)を確立し、各接続でb.N / numConcurrent回メッセージを送受信します。
    • 接続確立・終了のオーバーヘッドが分散され、データ送受信の効率がより顕著に現れます。
  4. BenchmarkTCPPersistentTimeout:

    • persistent = true, timeout = true
    • BenchmarkTCPPersistentと同様ですが、永続的な接続にSetDeadlineを設定します。これもBenchmarkTCPOneShotTimeoutと同様に、SetDeadline呼び出しのオーバーヘッドを測定します。

ベンチマークの設計要素

  • メッセージ長: const msgLen = 512バイトの固定長メッセージを使用します。これは、ネットワークI/Oにおける一般的なデータサイズをシミュレートするためです。
  • 並行処理の制御:
    • numConcurrent := runtime.GOMAXPROCS(-1) * 16: 同時に実行されるクライアントgoroutineの最大数を決定します。これは、システムのCPUコア数に基づいてスケーリングされ、高い並行性下でのパフォーマンスを評価します。
    • sem := make(chan bool, numConcurrent): バッファ付きチャネルをセマフォとして使用し、同時にアクティブなクライアントgoroutineの数をnumConcurrentに制限します。これにより、システムリソースの過剰な消費を防ぎ、制御された並行実行を保証します。
  • サーバーとクライアントの役割:
    • サーバー: net.Listenでリッスンし、ln.Accept()で新しい接続を受け入れます。受け入れた各接続は新しいgoroutineで処理され、クライアントからのメッセージを受信し、同じメッセージを返送します(エコーサーバーの機能)。
    • クライアント: net.Dialでサーバーに接続し、メッセージを送信し、応答メッセージを受信します。
  • エラーハンドリング: b.Logfを使用して、ベンチマーク中に発生したエラーをログに記録します。b.Fatalfは、ベンチマークのセットアップ中に致命的なエラーが発生した場合にベンチマークを停止するために使用されます。
  • b.Nの利用: testingパッケージのベンチマークの慣例に従い、b.Nをループ回数として使用します。testingフレームワークが自動的にb.Nを調整し、統計的に有意な結果が得られるようにします。

このベンチマークは、GoのTCPスタックが、短命な接続と長命な接続の両方で、またタイムアウト設定の有無によってどのように振る舞うかを包括的に評価するための堅牢なフレームワークを提供します。

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

このコミットでは、以下の新しいファイルが追加されています。

src/pkg/net/tcp_test.go

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package net

import (
	"runtime"
	"testing"
	"time"
)

func BenchmarkTCPOneShot(b *testing.B) {
	benchmarkTCP(b, false, false)
}

func BenchmarkTCPOneShotTimeout(b *testing.B) {
	benchmarkTCP(b, false, true)
}

func BenchmarkTCPPersistent(b *testing.B) {
	benchmarkTCP(b, true, false)
}

func BenchmarkTCPPersistentTimeout(b *testing.B) {
	benchmarkTCP(b, true, true)
}

func benchmarkTCP(b *testing.B, persistent, timeout bool) {
	const msgLen = 512
	conns := b.N
	numConcurrent := runtime.GOMAXPROCS(-1) * 16
	msgs := 1
	if persistent {
		conns = numConcurrent
		msgs = b.N / conns
		if msgs == 0 {
			msgs = 1
		}
		if conns > b.N {
			conns = b.N
		}
	}
	sendMsg := func(c Conn, buf []byte) bool {
		n, err := c.Write(buf)
		if n != len(buf) || err != nil {
			b.Logf("Write failed: %v", err)
			return false
		}
		return true
	}
	recvMsg := func(c Conn, buf []byte) bool {
		for read := 0; read != len(buf); {
			n, err := c.Read(buf)
			read += n
			if err != nil {
				b.Logf("Read failed: %v", err)
				return false
			}
		}
		return true
	}
	ln, err := Listen("tcp", "127.0.0.1:0")
	if err != nil {
		b.Fatalf("Listen failed: %v", err)
	}
	defer ln.Close()
	// Acceptor.
	go func() {
		for {
			c, err := ln.Accept()
			if err != nil {
				break
			}
			// Server connection.
			go func(c Conn) {
				defer c.Close()
				if timeout {
					c.SetDeadline(time.Now().Add(time.Hour)) // Not intended to fire.
				}
				var buf [msgLen]byte
				for m := 0; m < msgs; m++ {
					if !recvMsg(c, buf[:]) || !sendMsg(c, buf[:]) {
						break
					}
				}
			}(c)
		}
	}()
	sem := make(chan bool, numConcurrent)
	for i := 0; i < conns; i++ {
		sem <- true
		// Client connection.
		go func() {
			defer func() {
				<-sem
			}()
			c, err := Dial("tcp", ln.Addr().String())
			if err != nil {
				b.Logf("Dial failed: %v", err)
				return
			}
			defer c.Close()
			if timeout {
				c.SetDeadline(time.Now().Add(time.Hour)) // Not intended to fire.
			}
			var buf [msgLen]byte
			for m := 0; m < msgs; m++ {
				if !sendMsg(c, buf[:]) || !recvMsg(c, buf[:]) {
					break
				}
			}
		}()
	}
	for i := 0; i < cap(sem); i++ {
		sem <- true
	}
}

コアとなるコードの解説

src/pkg/net/tcp_test.goファイルは、GoのnetパッケージにおけるTCP通信のパフォーマンスを測定するためのベンチマーク関数群を定義しています。

BenchmarkTCPOneShot, BenchmarkTCPOneShotTimeout, BenchmarkTCPPersistent, BenchmarkTCPPersistentTimeout

これら4つの関数は、Goのベンチマーク関数のシグネチャfunc BenchmarkXxx(b *testing.B)に従って定義されています。それぞれが、後述する汎用ベンチマーク関数benchmarkTCPを異なる引数で呼び出すことで、特定のTCP通信シナリオをテストします。

  • BenchmarkTCPOneShot: benchmarkTCP(b, false, false)を呼び出し、ワンショット(非永続的)接続でタイムアウトなしのシナリオをテストします。
  • BenchmarkTCPOneShotTimeout: benchmarkTCP(b, false, true)を呼び出し、ワンショット接続でタイムアウトありのシナリオをテストします。
  • BenchmarkTCPPersistent: benchmarkTCP(b, true, false)を呼び出し、永続的接続でタイムアウトなしのシナリオをテストします。
  • BenchmarkTCPPersistentTimeout: benchmarkTCP(b, true, true)を呼び出し、永続的接続でタイムアウトありのシナリオをテストします。

benchmarkTCP関数

この関数が、実際のTCPベンチマークロジックをカプセル化しています。

  • 引数:

    • b *testing.B: ベンチマーク実行のためのコンテキストを提供します。
    • persistent bool: trueの場合、永続的なTCP接続を使用します。falseの場合、各操作で新しい接続を確立します。
    • timeout bool: trueの場合、TCP接続にSetDeadlineを設定します。
  • 定数と変数:

    • const msgLen = 512: 送受信するメッセージのバイト長を定義します。
    • conns: 確立する接続の総数。persistentfalseの場合はb.N(ベンチマークのイテレーション回数)に等しく、trueの場合はnumConcurrentに等しくなります。
    • numConcurrent := runtime.GOMAXPROCS(-1) * 16: 同時に実行されるクライアントgoroutineの最大数を決定します。これは、利用可能なCPUコア数に16を掛けた値で、高い並行性下でのテストを意図しています。
    • msgs: 各接続で送受信するメッセージの数。persistenttrueの場合はb.N / connsとなり、falseの場合は1となります。
  • ヘルパー関数:

    • sendMsg(c Conn, buf []byte) bool: 指定されたConnbufの内容を書き込みます。書き込みが成功したか、エラーが発生したかを返します。
    • recvMsg(c Conn, buf []byte) bool: 指定されたConnからbufの長さ分のデータを読み込みます。読み込みが成功したか、エラーが発生したかを返します。
  • サーバーのセットアップ:

    • ln, err := Listen("tcp", "127.0.0.1:0"): ローカルホストの利用可能なポートでTCPリッスンを開始します。ポート番号に0を指定すると、OSが自動的に空いているポートを割り当てます。
    • defer ln.Close(): ベンチマーク関数が終了する際にリッスンソケットを確実にクローズします。
    • Acceptor Goroutine:
      • go func() { ... }(): 新しいgoroutineで接続の受け入れ処理を実行します。
      • for { c, err := ln.Accept() ... }: 無限ループでクライアントからの接続を待ち受けます。エラーが発生した場合(例: リスナーがクローズされた場合)はループを抜けます。
      • Server Connection Goroutine:
        • go func(c Conn) { ... }(c): 受け入れた各クライアント接続を新しいgoroutineで処理します。
        • defer c.Close(): サーバー側の接続を確実にクローズします。
        • if timeout { c.SetDeadline(time.Now().Add(time.Hour)) }: timeouttrueの場合、接続に非常に長い期限を設定します。これは、SetDeadline呼び出し自体のオーバーヘッドを測定するためのものです。
        • for m := 0; m < msgs; m++ { ... }: msgs回、クライアントからのメッセージを受信し、同じメッセージを返送します。
  • クライアントのセットアップ:

    • sem := make(chan bool, numConcurrent): numConcurrent個のバッファを持つチャネルをセマフォとして使用し、同時に実行されるクライアントgoroutineの数を制限します。
    • for i := 0; i < conns; i++ { ... }: connsの数だけクライアント接続を試みます。
    • sem <- true: セマフォにトークンを送信し、同時に実行されるgoroutineの数を制御します。バッファが満杯の場合、ここでブロックされます。
    • Client Connection Goroutine:
      • go func() { ... }(): 新しいgoroutineでクライアント接続処理を実行します。
      • defer func() { <-sem }(): クライアントgoroutineが終了する際にセマフォからトークンを受信し、他のgoroutineが実行できるようにします。
      • c, err := Dial("tcp", ln.Addr().String()): サーバーのアドレスにTCP接続を確立します。
      • defer c.Close(): クライアント側の接続を確実にクローズします。
      • if timeout { c.SetDeadline(time.Now().Add(time.Hour)) }: サーバー側と同様に、timeouttrueの場合に期限を設定します。
      • for m := 0; m < msgs; m++ { ... }: msgs回、メッセージをサーバーに送信し、応答メッセージを受信します。
  • セマフォの解放:

    • for i := 0; i < cap(sem); i++ { sem <- true }: ベンチマークの最後に、セマフォに残っている可能性のあるすべてのトークンを解放します。これにより、ベンチマークが正常に終了し、リソースが適切にクリーンアップされます。

このコードは、Goの並行処理機能(goroutineとチャネル)を効果的に利用して、複数のTCP接続を同時に処理し、様々なシナリオでのパフォーマンスを測定する堅牢なベンチマーク環境を構築しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • TCP/IPに関する一般的な知識
  • Go言語の並行処理に関する一般的な知識