[インデックス 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
インターフェース: ネットワーク接続を表し、Read
、Write
、Close
、LocalAddr
、RemoteAddr
、SetDeadline
などのメソッドを提供します。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つの異なるシナリオをカバーしています。
-
BenchmarkTCPOneShot
:persistent = false
,timeout = false
- 各操作(
b.N
回)ごとに新しいTCP接続を確立し、1つのメッセージを送受信し、接続を閉じます。 - 接続の確立と終了のオーバーヘッドが支配的になります。
-
BenchmarkTCPOneShotTimeout
:persistent = false
,timeout = true
BenchmarkTCPOneShot
と同様ですが、接続にSetDeadline
を設定します。このSetDeadline
は非常に長い時間(1時間)に設定されているため、実際にタイムアウトが発生することは意図されておらず、SetDeadline
呼び出し自体のオーバーヘッドを測定します。
-
BenchmarkTCPPersistent
:persistent = true
,timeout = false
- 少数の永続的なTCP接続(
numConcurrent
で定義される数)を確立し、各接続でb.N / numConcurrent
回メッセージを送受信します。 - 接続確立・終了のオーバーヘッドが分散され、データ送受信の効率がより顕著に現れます。
-
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
: 確立する接続の総数。persistent
がfalse
の場合はb.N
(ベンチマークのイテレーション回数)に等しく、true
の場合はnumConcurrent
に等しくなります。numConcurrent := runtime.GOMAXPROCS(-1) * 16
: 同時に実行されるクライアントgoroutineの最大数を決定します。これは、利用可能なCPUコア数に16を掛けた値で、高い並行性下でのテストを意図しています。msgs
: 各接続で送受信するメッセージの数。persistent
がtrue
の場合はb.N / conns
となり、false
の場合は1となります。
-
ヘルパー関数:
sendMsg(c Conn, buf []byte) bool
: 指定されたConn
にbuf
の内容を書き込みます。書き込みが成功したか、エラーが発生したかを返します。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)) }
:timeout
がtrue
の場合、接続に非常に長い期限を設定します。これは、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)) }
: サーバー側と同様に、timeout
がtrue
の場合に期限を設定します。for m := 0; m < msgs; m++ { ... }
:msgs
回、メッセージをサーバーに送信し、応答メッセージを受信します。
-
セマフォの解放:
for i := 0; i < cap(sem); i++ { sem <- true }
: ベンチマークの最後に、セマフォに残っている可能性のあるすべてのトークンを解放します。これにより、ベンチマークが正常に終了し、リソースが適切にクリーンアップされます。
このコードは、Goの並行処理機能(goroutineとチャネル)を効果的に利用して、複数のTCP接続を同時に処理し、様々なシナリオでのパフォーマンスを測定する堅牢なベンチマーク環境を構築しています。
関連リンク
- Go言語
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語におけるベンチマークの書き方 (公式ブログ): https://go.dev/blog/benchmarking
参考にした情報源リンク
- Go言語の公式ドキュメント
- TCP/IPに関する一般的な知識
- Go言語の並行処理に関する一般的な知識