[インデックス 13969] ファイルの概要
このコミットは、Go言語の標準ライブラリである net
パッケージにおける、ファイルディスクリプタ (FD) のポーリング処理の効率化を目的としています。具体的には、ネットワークI/Oを処理する pollServer
のインスタンスを複数化し、FDをこれらの pollServer
に分散させることで、マルチコア環境における競合(コンテンション)を軽減し、ネットワーク性能を向上させています。
コミット
commit 7014bc64b1be8f85fff75ec13f8597b6a6aed366
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date: Wed Sep 26 15:32:59 2012 -0400
net: spread fd over several pollservers.
Lighten contention without preventing further improvements on pollservers.
Connections are spread over Min(GOMAXPROCS, NumCPU, 8) pollserver instances.
Median of 10 runs, 4 cores @ 3.4GHz, amd/linux-3.2:
BenchmarkTCPOneShot 171917 ns/op 175194 ns/op 1.91%
BenchmarkTCPOneShot-2 101413 ns/op 109462 ns/op 7.94%
BenchmarkTCPOneShot-4 91796 ns/op 35712 ns/op -61.10%
BenchmarkTCPOneShot-6 90938 ns/op 30607 ns/op -66.34%
BenchmarkTCPOneShot-8 90374 ns/op 29150 ns/op -67.75%
BenchmarkTCPOneShot-16 101089 ns/op 111526 ns/op 10.32%
BenchmarkTCPOneShotTimeout 174986 ns/op 178606 ns/op 2.07%
BenchmarkTCPOneShotTimeout-2 101585 ns/op 110678 ns/op 8.95%
BenchmarkTCPOneShotTimeout-4 91547 ns/op 35931 ns/op -60.75%
BenchmarkTCPOneShotTimeout-6 91496 ns/op 31019 ns/op -66.10%
BenchmarkTCPOneShotTimeout-8 90670 ns/op 29531 ns/op -67.43%
BenchmarkTCPOneShotTimeout-16 101013 ns/op 106026 ns/op 4.96%
BenchmarkTCPPersistent 51731 ns/op 53324 ns/op 3.08%
BenchmarkTCPPersistent-2 32888 ns/op 30678 ns/op -6.72%
BenchmarkTCPPersistent-4 25751 ns/op 15595 ns/op -39.44%
BenchmarkTCPPersistent-6 26737 ns/op 9805 ns/op -63.33%
BenchmarkTCPPersistent-8 26850 ns/op 9730 ns/op -63.76%
BenchmarkTCPPersistent-16 104449 ns/op 102838 ns/op -1.54%
BenchmarkTCPPersistentTimeout 51806 ns/op 53281 ns/op 2.85%
BenchmarkTCPPersistentTimeout-2 32956 ns/op 30895 ns/op -6.25%
BenchmarkTCPPersistentTimeout-4 25994 ns/op 18111 ns/op -30.33%
BenchmarkTCPPersistentTimeout-6 26679 ns/op 9846 ns/op -63.09%
BenchmarkTCPPersistentTimeout-8 26810 ns/op 9727 ns/op -63.72%
BenchmarkTCPPersistentTimeout-16 101652 ns/op 104410 ns/op 2.71%
R=rsc, dvyukov, dave, mikioh.mikioh, bradfitz, remyoudompheng
CC=golang-dev
https://golang.org/cl/6496054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7014bc64b1be8f85fff75ec13f8597b6a6aed366
元コミット内容
このコミットは、Go言語のネットワークI/O処理におけるボトルネックを解消するためのものです。以前は、すべてのネットワークファイルディスクリプタ (FD) が単一の pollServer
インスタンスを共有していました。これにより、特にマルチコアプロセッサを搭載したシステムで多数の同時接続を処理する際に、この単一の pollServer
が競合のホットスポットとなり、性能が低下する問題がありました。
コミットの目的は、この単一の pollServer
への競合を軽減し、Goの並行処理能力をより効果的に活用することです。
変更の背景
Go言語は、その軽量なゴルーチンと効率的なスケジューラによって、高い並行性を実現するように設計されています。しかし、ネットワークI/Oのようなシステムコールを伴う処理では、OSのI/O多重化メカニズム(Linuxのepoll、FreeBSD/macOSのkqueueなど)を利用します。Goの net
パッケージ内部では、これらのOSレベルのI/Oイベントを抽象化し、ゴルーチンに通知する役割を pollServer
が担っていました。
従来の設計では、すべてのネットワークFDが単一のグローバルな pollServer
インスタンスに登録されていました。これにより、複数のゴルーチンが同時にネットワークI/Oを待機する際、この単一の pollServer
の内部ロックやデータ構造へのアクセスで競合が発生し、スケーラビリティが制限されていました。特に、CPUコア数が増えるにつれて、このボトルネックは顕著になり、Goの並行処理の利点が十分に活かされない状況でした。
このコミットは、この競合を緩和し、マルチコア環境でのネットワーク性能を向上させることを目的としています。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。ネットワーク通信では、ソケットがFDとして扱われます。
2. ノンブロッキングI/O
通常のI/O操作は、データが利用可能になるまで(または書き込みバッファが空くまで)処理がブロックされます。ノンブロッキングI/Oでは、操作がすぐに戻り、データが利用可能でない場合はエラー(例: EAGAIN
または EWOULDBLOCK
)を返します。これにより、アプリケーションはI/O操作の完了を待つ間に他の処理を実行できます。
3. I/O多重化 (I/O Multiplexing)
複数のFDに対して、どれか一つでもI/O準備ができたときに通知を受け取るメカニズムです。select
, poll
, epoll
(Linux), kqueue
(FreeBSD/macOS) などがあります。これにより、単一のスレッドで多数の同時接続を効率的に処理できます。Goの pollServer
は、これらのOS固有のI/O多重化メカニズムを抽象化しています。
4. pollServer
(Go言語 net
パッケージ内部)
Goの net
パッケージ内部で使用されるコンポーネントで、ノンブロッキングI/O操作を管理し、I/Oイベントをゴルーチンに通知する役割を担います。具体的には、ネットワークFDをOSのI/O多重化メカニズム(例: epoll)に登録し、I/O準備ができたFDを監視します。I/O準備ができたFDがあると、対応するゴルーチンを再開させます。
5. 競合 (Contention)
並行プログラミングにおいて、複数のスレッドやゴルーチンが同時に共有リソース(例: ロック、データ構造)にアクセスしようとするときに発生する状況です。競合が高いと、スレッドはリソースの解放を待つ必要があり、プログラムの実行速度が低下します。
6. GOMAXPROCS
Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数、または runtime.GOMAXPROCS()
関数です。この値は、GoスケジューラがゴルーチンをOSスレッドにマッピングする方法に影響を与えます。
7. runtime.NumCPU()
システム上の論理CPUの数を返します。
8. sync.Once
Goの sync
パッケージが提供する型で、特定の関数がプログラムの実行中に一度だけ実行されることを保証します。これは、リソースの初期化など、一度だけ実行されるべき処理に非常に有用です。
技術的詳細
このコミットの核心は、GoのネットワークI/O処理における pollServer
のアーキテクチャを、単一インスタンスから複数インスタンスへと変更した点にあります。
-
単一
pollServer
の問題点: 以前のGoのnet
パッケージでは、すべてのネットワークFDが単一のグローバルなpollServer
インスタンスに登録されていました。このpollServer
は、内部でロックを使用して共有データ構造(I/Oイベントを待機しているFDのリストなど)を保護していました。マルチコア環境で多数のゴルーチンが同時にネットワークI/Oを待機すると、この単一のpollServer
のロックに対する競合が激しくなり、性能上のボトルネックとなっていました。 -
複数
pollServer
の導入: このコミットでは、pollServer
のインスタンスを複数作成し、ネットワークFDをこれらのインスタンスに分散させる戦略を採用しました。これにより、I/O待機処理が複数のpollServer
に分散され、単一のロックに対する競合が大幅に軽減されます。 -
分散戦略:
- インスタンス数の決定: 作成される
pollServer
のインスタンス数は、Min(GOMAXPROCS, NumCPU, 8)
で決定されます。GOMAXPROCS
: Goスケジューラが利用できるOSスレッド数を超えてpollServer
を増やしても、Goの並行実行能力が向上するわけではないため、この値が考慮されます。NumCPU
: 物理的なCPUコア数も考慮されます。8
: コミットメッセージに「No improvement then.」とあるように、経験的に8を超えるインスタンス数ではそれ以上の性能向上が見られないため、上限が設けられています。これは、過剰なインスタンス化が管理オーバーヘッドを増大させる可能性があるためです。
- FDの割り当て: 各ネットワークFDは、そのFDの整数値と
pollServer
のインスタンス数 (pollN
) を用いたモジュロ演算 (fd % pollN
) によって、特定のpollServer
インスタンスに割り当てられます。このシンプルなハッシュ戦略により、FDが複数のpollServer
に均等に分散されることが期待されます。
- インスタンス数の決定: 作成される
-
遅延初期化と
sync.Once
: 各pollServer
インスタンスは、必要になったときに初めて初期化されます。これはsync.Once
を使用して実現されており、各pollServer
が一度だけ安全に初期化されることを保証します。これにより、不要なpollServer
の起動を避け、リソースの効率的な利用が図られています。 -
性能ベンチマーク: コミットメッセージに含まれるベンチマーク結果は、この変更が特にマルチコア環境で顕著な性能向上をもたらしたことを明確に示しています。
BenchmarkTCPOneShot-4
,-6
,-8
など、CPUコア数が増えるにつれてns/op
(操作あたりのナノ秒) が大幅に減少しており、これは処理速度が向上したことを意味します。例えば、BenchmarkTCPOneShot-8
では、操作あたりの時間が約67.75%削減されています。- これは、複数の
pollServer
が並行してI/Oイベントを処理できるようになった結果、全体のスループットが向上したことを裏付けています。
この変更は、Goのネットワークスタックの基盤を強化し、高負荷なネットワークアプリケーションにおけるスケーラビリティと応答性を大幅に改善しました。
コアとなるコードの変更箇所
主な変更は src/pkg/net/fd_unix.go
に集中しています。
-
netFD
構造体へのpollServer
フィールドの追加:type netFD struct { // ... 既存のフィールド ... // wait server pollServer *pollServer }
これにより、各ネットワークFDが自身が属する
pollServer
インスタンスへの参照を持つようになります。 -
グローバルな
pollserver
変数の削除と複数インスタンスの導入:// 削除: var pollserver *pollServer // 削除: var onceStartServer sync.Once var pollMaxN int var pollservers []*pollServer var startServersOnce []func()
単一のグローバル変数
pollserver
が削除され、pollservers
というpollServer
のスライスが導入されました。 -
init()
関数の変更:func init() { pollMaxN = runtime.NumCPU() if pollMaxN > 8 { pollMaxN = 8 // No improvement then. } pollservers = make([]*pollServer, pollMaxN) startServersOnce = make([]func(), pollMaxN) for i := 0; i < pollMaxN; i++ { k := i once := new(sync.Once) startServersOnce[i] = func() { once.Do(func() { startServer(k) }) } } }
pollMaxN
の計算、pollservers
スライスの初期化、そして各pollServer
の遅延初期化を保証するためのsync.Once
を含むstartServersOnce
スライスの設定が行われます。 -
startServer
関数の変更:// 変更前: func startServer() { ... pollserver = p ... } func startServer(k int) { p, err := newPollServer() if err != nil { panic(err) // 以前は print だった } pollservers[k] = p }
k
を引数に取り、指定されたインデックスのpollServer
を初期化してpollservers
スライスに格納します。エラー処理もpanic
に変更されています。 -
server
関数の新規追加:func server(fd int) *pollServer { pollN := runtime.GOMAXPROCS(0) if pollN > pollMaxN { pollN = pollMaxN } k := fd % pollN startServersOnce[k]() return pollservers[k] }
この関数が、与えられたFDに基づいてどの
pollServer
を使用するかを決定し、そのpollServer
を返します。 -
newFD
関数でのpollServer
の割り当て:func newFD(fd, family, sotype int, net string) (*netFD, error) { // 変更前: onceStartServer.Do(startServer) // ... netfd.pollServer = server(fd) // 新しい割り当て return netfd, nil }
newFD
が作成される際に、server(fd)
関数を呼び出して適切なpollServer
をnetfd.pollServer
に設定します。 -
netFD
メソッドでのpollServer
の利用変更:connect
,Close
,Read
,Write
など、pollServer
を利用するすべてのnetFD
メソッドで、グローバルなpollserver
ではなく、fd.pollServer
を介してアクセスするように変更されています。 例:// 変更前: if err = pollserver.WaitWrite(fd); err != nil { // 変更後: if err = fd.pollServer.WaitWrite(fd); err != nil {
同様の変更が
src/pkg/net/sendfile_freebsd.go
とsrc/pkg/net/sendfile_linux.go
にも適用されています。
コアとなるコードの解説
このコミットの核となるロジックは、init()
関数と新しく追加された server(fd int) *pollServer
関数にあります。
init()
関数
func init() {
// 1. pollServerの最大数を決定
// runtime.NumCPU() はシステム上の論理CPU数を返す。
// GOMAXPROCSは考慮されないが、後続のserver関数で利用される。
pollMaxN = runtime.NumCPU()
// 経験的に8を超えるインスタンスでは性能向上が見られないため、上限を8に設定。
if pollMaxN > 8 {
pollMaxN = 8 // No improvement then.
}
// 2. pollServerインスタンスを格納するスライスを初期化
// pollMaxNの数だけnilポインタで初期化される。
pollservers = make([]*pollServer, pollMaxN)
// 3. 各pollServerの遅延初期化を制御するためのsync.Once関数スライスを初期化
startServersOnce = make([]func(), pollMaxN)
for i := 0; i < pollMaxN; i++ {
k := i // クロージャ内でiの現在の値をキャプチャするために必要
once := new(sync.Once) // 各pollServerごとに新しいsync.Onceインスタンスを作成
// startServersOnce[k] に、k番目のpollServerを一度だけ起動する関数を割り当てる
startServersOnce[i] = func() { once.Do(func() { startServer(k) }) }
}
}
この init()
関数は、プログラム起動時に一度だけ実行され、pollServer
のインスタンスを管理するためのグローバルなデータ構造を準備します。pollMaxN
を決定し、pollservers
スライスと、各 pollServer
を一度だけ起動するための sync.Once
を含む startServersOnce
スライスを初期化します。
server(fd int) *pollServer
関数
func server(fd int) *pollServer {
// 1. 現在のGOMAXPROCSの値を取得
// これは、Goスケジューラが利用できるOSスレッドの数を反映する。
pollN := runtime.GOMAXPROCS(0)
// 2. pollServerの数をpollMaxN(CPU数または8)で制限
// GOMAXPROCSがpollMaxNより大きい場合、pollMaxNに制限する。
// これは、Goスケジューラのスレッド数と物理CPU数の両方を考慮した上で、
// 経験的な上限値を超えないようにするため。
if pollN > pollMaxN {
pollN = pollMaxN
}
// 3. ファイルディスクリプタ(fd)に基づいて、使用するpollServerのインデックスを決定
// シンプルなモジュロ演算により、FDを複数のpollServerに分散させる。
k := fd % pollN
// 4. 選択されたpollServerがまだ起動していなければ起動する
// startServersOnce[k]() は、k番目のpollServerを一度だけ起動することを保証する。
startServersOnce[k]()
// 5. 割り当てられたpollServerインスタンスを返す
return pollservers[k]
}
この server
関数は、新しいネットワークFDが作成されるたびに呼び出されます。FDの整数値に基づいて、どの pollServer
インスタンスを使用するかを決定します。fd % pollN
というシンプルなハッシュ関数を使用することで、FDが複数の pollServer
に均等に分散されることを目指します。また、startServersOnce[k]()
を呼び出すことで、対応する pollServer
がまだ起動していなければ、この時点で一度だけ起動されることが保証されます。
これにより、各ネットワークFDは専用の pollServer
インスタンスと関連付けられ、I/O操作の待機時にその pollServer
を利用するようになります。結果として、単一の pollServer
に集中していた競合が複数の pollServer
に分散され、マルチコア環境でのネットワークI/O性能が大幅に向上します。
関連リンク
- Go言語の
net
パッケージ: https://pkg.go.dev/net - Go言語の
runtime
パッケージ: https://pkg.go.dev/runtime - Go言語の
sync
パッケージ: https://pkg.go.dev/sync - GoのI/O多重化に関する議論(一般的な情報源): Goの内部実装に関する公式ドキュメントは少ないですが、Goのネットワークプログラミングや並行処理に関する記事で、
epoll
やkqueue
といったOSのI/O多重化メカニズムがGoのランタイムでどのように利用されているかについて言及されていることがあります。
参考にした情報源リンク
- コミットメッセージに記載されているGoのコードレビューシステム (Gerrit) のリンク: https://golang.org/cl/6496054 (現在はGitHubにリダイレクトされる可能性があります)
- Goのソースコード:
src/pkg/net/fd_unix.go
(このコミットが適用された時点のバージョン) - Goのベンチマークに関する一般的な情報: Goの公式ドキュメントやブログ記事で、ベンチマークの読み方やGoの性能特性について解説されていることがあります。