[インデックス 15760] ファイルの概要
このコミットは、Go言語のランタイムにおけるネットワークポーラーの実装を、Darwin (macOS) 環境向けに大幅に改善するものです。具体的には、従来のnet
パッケージ内のポーラーから、Goランタイムに統合された新しいネットワークポーラーへと移行し、kqueue
システムコールを効率的に利用することで、ネットワークI/Oのパフォーマンスを劇的に向上させています。ベンチマーク結果が示すように、TCP接続の永続的な処理において最大50%以上の性能改善を達成しています。
コミット
commit 0bee99ab3b17caca812aa78a51485aadf0bc1788
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 14 10:38:37 2013 +0400
runtime: integrated network poller for darwin
vs tip:
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 67786 33175 -51.06%
BenchmarkTCP4Persistent-2 49085 31227 -36.38%
BenchmarkTCP4PersistentTimeout 69265 32565 -52.98%
BenchmarkTCP4PersistentTimeout-2 49217 32588 -33.79%
vs old scheduler:
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 63517 33175 -47.77%
BenchmarkTCP4Persistent-2 54760 31227 -42.97%
BenchmarkTCP4PersistentTimeout 63234 32565 -48.50%
BenchmarkTCP4PersistentTimeout-2 56956 32588 -42.78%
R=golang-dev, bradfitz, devon.odell, mikioh.mikioh, iant, rsc
CC=golang-dev, pabuhr
https://golang.org/cl/7569043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0bee99ab3b17caca812aa78a51485aadf0bc1788
元コミット内容
runtime: integrated network poller for darwin
vs tip:
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 67786 33175 -51.06%
BenchmarkTCP4Persistent-2 49085 31227 -36.38%
BenchmarkTCP4PersistentTimeout 69265 32565 -52.98%
BenchmarkTCP4PersistentTimeout-2 49217 32588 -33.79%
vs old scheduler:
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 63517 33175 -47.77%
BenchmarkTCP4Persistent-2 54760 31227 -42.97%
BenchmarkTCP4PersistentTimeout 63234 32565 -48.50%
BenchmarkTCP4PersistentTimeout-2 56956 32588 -42.78%
R=golang-dev, bradfitz, devon.odell, mikioh.mikioh, iant, rsc
CC=golang-dev, pabuhr
https://golang.org/cl/7569043
変更の背景
この変更の主な背景は、GoランタイムのネットワークI/O処理の効率化とパフォーマンス向上です。Goのgoroutineベースの並行処理モデルでは、多数のネットワーク接続を同時に扱うことが一般的です。従来のネットワークポーラーは、net
パッケージ内に実装されており、ランタイムのスケジューラとの連携が最適化されていませんでした。特にDarwin環境では、kqueue
という高性能なI/Oイベント通知メカニズムが存在するにもかかわらず、その潜在能力を十分に引き出せていませんでした。
このコミットは、ネットワークポーラーをGoランタイムのコア部分に統合することで、以下の課題を解決しようとしています。
- コンテキストスイッチの削減: 従来のポーラーでは、ネットワークI/Oの待機とgoroutineのスケジューリングの間に余分なオーバーヘッドが発生していました。ランタイム統合により、I/Oイベントの発生とgoroutineの再開がより密接に連携し、コンテキストスイッチの回数を減らすことができます。
- スケーラビリティの向上: 多数の同時接続を扱う際に、ポーラーがボトルネックになる可能性がありました。ランタイム統合されたポーラーは、より効率的なイベント処理とgoroutineの管理により、スケーラビリティを向上させます。
- デッドライン処理の改善: ネットワーク操作にはタイムアウト(デッドライン)が設定されることがよくあります。新しいポーラーは、デッドライン処理をランタイムのタイマー機構と統合することで、より正確かつ効率的なタイムアウト管理を実現します。
- プラットフォーム固有の最適化: Darwinにおける
kqueue
のような高性能なI/O多重化メカニズムを、ランタイムレベルで直接、かつ最適に利用できるようになります。これにより、OSが提供する機能を最大限に活用し、ネイティブに近いパフォーマンスを引き出すことが可能になります。
ベンチマーク結果が示すように、この変更はTCP接続の永続的な処理やタイムアウト処理において、大幅な性能改善をもたらしており、Goアプリケーションのネットワーク性能向上に大きく貢献しています。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
-
ネットワークポーラー (Network Poller): ネットワークポーラーは、オペレーティングシステムが提供するI/O多重化メカニズム(例: Linuxの
epoll
、macOS/BSDのkqueue
、WindowsのI/O Completion Ports (IOCP))を利用して、複数のファイルディスクリプタ(ソケットなど)からのI/Oイベント(読み込み可能、書き込み可能など)を効率的に監視するコンポーネントです。アプリケーションはポーラーに監視対象のディスクリプタを登録し、イベントが発生するまで待機します。イベントが発生すると、ポーラーはどのディスクリプタでどのようなイベントが発生したかを通知し、アプリケーションはそれに応じてI/O処理を進めます。これにより、ブロッキングI/Oで発生するスレッドの無駄な待機を防ぎ、少数のスレッドで多数の同時接続を効率的に処理できるようになります。 -
kqueue
(Kernel Event Queue):kqueue
は、FreeBSD、macOS、NetBSD、OpenBSDなどのBSD系UNIXシステムで利用される、高性能なI/Oイベント通知インターフェースです。これは、ファイルディスクリプタだけでなく、プロセス状態の変化、タイマー、シグナルなど、様々なカーネルイベントを監視できます。kqueue
は、イベントの登録、変更、削除を行うkevent
システムコールを介して操作されます。epoll
と同様に、エッジトリガー(イベント発生時に一度だけ通知)とレベルトリガー(状態が変化するまで通知し続ける)の両方をサポートしますが、このコミットでは主にエッジトリガーモード(EV_CLEAR
フラグ)が利用されています。 -
Goランタイム (Go Runtime): Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(goroutineの管理)、メモリ管理、そしてI/O処理の抽象化などが含まれます。Goのスケジューラは、OSスレッド(M: Machine)上でgoroutine(G)を実行し、I/O待機などでgoroutineがブロックされると、そのgoroutineを一時停止し、別の実行可能なgoroutineを同じOSスレッド上で実行します。ネットワークポーラーは、このスケジューラと密接に連携し、I/O待機中のgoroutineを効率的に管理します。
-
goroutine: Go言語における軽量な並行処理の単位です。OSスレッドよりもはるかに軽量で、数百万個のgoroutineを同時に実行することも可能です。goroutineはGoランタイムのスケジューラによって管理され、I/O操作などでブロックされると、自動的にスケジューラによって一時停止され、他のgoroutineにCPUが譲られます。I/Oが完了すると、スケジューラによって再び実行可能状態に戻されます。
-
非ブロッキングI/O (Non-blocking I/O): 通常のブロッキングI/Oでは、I/O操作が完了するまで呼び出し元のスレッドが停止します。非ブロッキングI/Oでは、I/O操作がすぐに戻り、データが利用可能でない場合や書き込みバッファが満杯の場合にはエラー(例:
EAGAIN
やEWOULDBLOCK
)を返します。ネットワークポーラーは、この非ブロッキングI/Oと組み合わせて使用され、I/O操作がすぐに完了しない場合に、ポーラーにイベントの監視を依頼し、スレッドを解放して他の処理を行えるようにします。 -
PollDesc
構造体: このコミットで導入されるPollDesc
は、Goランタイムがネットワークファイルディスクリプタ(FD)の状態を管理するために使用する重要なデータ構造です。各FDに対応するPollDesc
インスタンスが作成され、そのFDに対する読み書きの待機状態、デッドライン(タイムアウト)、関連するgoroutineなどの情報が保持されます。PollDesc
は、ポーラーとスケジューラの間の橋渡し役となり、I/Oイベントの発生時に適切なgoroutineを再開させる役割を担います。
技術的詳細
このコミットの核心は、GoランタイムがネットワークI/Oを処理する方法を根本的に変更し、特にDarwin環境でのkqueue
の利用を最適化することにあります。
変更のアーキテクチャ:
-
net
パッケージからの分離: 従来のネットワークポーラーはsrc/pkg/net/fd_darwin.go
にあり、net
パッケージの一部として実装されていました。このコミットでは、このファイルを削除し、ポーラーのロジックをGoランタイムのコア部分(src/pkg/runtime
)に移動しています。これにより、ネットワークI/Oの待機とgoroutineのスケジューリングがより密接に連携できるようになります。 -
プラットフォーム非依存部分 (
netpoll.goc
):src/pkg/runtime/netpoll.goc
は、ネットワークポーラーのプラットフォーム非依存なロジックを実装しています。これはC言語で書かれており、Goランタイムの内部構造(goroutine、タイマー、ロックなど)に直接アクセスできます。PollDesc
構造体を定義し、各ネットワークFDの状態(読み書きの待機goroutine、デッドライン、シーケンス番号など)を管理します。runtime_pollOpen
、runtime_pollClose
、runtime_pollReset
、runtime_pollWait
、runtime_pollSetDeadline
、runtime_pollUnblock
といった関数を提供し、net
パッケージからランタイムポーラーを操作するためのインターフェースとなります。runtime_netpollready
関数は、プラットフォーム固有のポーラー(例:kqueue
)からI/Oイベントが通知された際に呼び出され、対応するPollDesc
の待機goroutineをREADY
状態にし、スケジューラに実行可能としてキューイングします。- デッドライン(タイムアウト)処理は、ランタイムのタイマー機構と統合されています。
PollDesc
内のrd
(read deadline)とwd
(write deadline)フィールド、およびTimer
構造体を使用して、指定されたデッドラインに達した際に自動的にgoroutineをアンブロックする仕組みが実装されています。seq
(シーケンス番号)は、PollDesc
が再利用されたり、デッドラインがリセットされたりした場合に、古いタイマーイベントを無視するためのメカニズムとして機能します。
-
Darwin固有の実装 (
netpoll_kqueue.c
):src/pkg/runtime/netpoll_kqueue.c
は、Darwin環境におけるkqueue
システムコールを利用したネットワークポーラーの具体的な実装です。runtime_netpollinit
関数でkqueue
インスタンスを作成し、初期化します。runtime_netpollopen
関数は、新しいFDがポーラーに登録される際に呼び出され、kqueue
にEVFILT_READ
とEVFILT_WRITE
のイベントをEV_ADD|EV_RECEIPT|EV_CLEAR
フラグ付きで登録します。EV_CLEAR
はエッジトリガーモードを意味し、イベントが発生した際に一度だけ通知され、その後はイベントキューからクリアされます。EV_RECEIPT
は、kevent
呼び出しが成功したかどうかを即座に確認するために使用されます。runtime_netpoll
関数は、Goスケジューラから定期的に呼び出され、kqueue
からI/Oイベントをポーリングします。イベントが発生すると、対応するPollDesc
とモード(読み込みまたは書き込み)を引数としてruntime_netpollready
を呼び出し、関連するgoroutineを実行可能状態にします。
-
システムコールラッパー (
sys_darwin_386.s
,sys_darwin_amd64.s
):src/pkg/runtime/sys_darwin_386.s
とsrc/pkg/runtime/sys_darwin_amd64.s
には、32ビットおよび64ビットDarwin環境向けのkqueue
、kevent
、fcntl
(closeonexec
用)システムコールのGoアセンブリラッパーが追加されています。これにより、Goランタイムが直接これらのOS機能にアクセスできるようになります。 -
定義ファイルの更新 (
defs_darwin.go
,defs_darwin_386.h
,defs_darwin_amd64.h
):kqueue
関連の定数(EV_ADD
,EV_DELETE
,EVFILT_READ
など)や構造体(Kevent
,Timespec
)がGoの定義ファイルに追加され、Cgoを通じてGoコードから利用できるようになっています。
パフォーマンス向上メカニズム:
- エッジトリガーモードの利用:
kqueue
のエッジトリガーモード(EV_CLEAR
)を使用することで、イベントが一度通知されたら、そのイベントはキューからクリアされます。これにより、同じイベントが繰り返し通知されるのを防ぎ、ポーラーの効率が向上します。 - ランタイムとの密な統合: ネットワークI/Oの待機とgoroutineのスケジューリングがランタイムレベルで直接管理されるため、I/Oイベント発生からgoroutineの再開までのレイテンシが短縮され、不要なコンテキストスイッチが削減されます。
PollDesc
による状態管理: 各FDの状態をPollDesc
で一元的に管理することで、デッドライン処理やgoroutineのアンブロックが効率的に行われます。特に、seq
フィールドによる古いタイマーイベントの破棄は、PollDesc
の再利用時の問題を回避し、堅牢性を高めます。- GCメモリからの分離:
PollDesc
オブジェクトは、runtime·SysAlloc
によってGCの対象とならないメモリ領域に割り当てられます。これは、epoll
/kqueue
の内部から参照される可能性があるため、GCによる移動や解放を防ぐためです。
これらの変更により、GoアプリケーションはDarwin環境でより高速かつ効率的なネットワークI/O処理を実現し、特に多数の同時接続を扱うサーバーアプリケーションにおいて顕著なパフォーマンス改善が見られます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更箇所は以下のファイルに集中しています。
-
src/pkg/net/fd_darwin.go
:- 削除: 従来のDarwin向けネットワークポーラーの実装が完全に削除されました。これは、ポーラーのロジックがランタイムに移動したためです。
-
src/pkg/net/fd_poll_runtime.go
:- 新規追加:
net
パッケージがランタイム統合されたポーラーと連携するための新しいインターフェースファイルです。runtime_pollOpen
,runtime_pollWait
などのランタイム関数を宣言し、pollDesc
構造体を定義しています。このファイルは、net
パッケージがランタイムポーラーの機能を利用するための橋渡し役となります。
- 新規追加:
-
src/pkg/net/fd_poll_unix.go
:- 変更: ビルドタグから
darwin
が削除されました。これは、Darwinがこの汎用Unixポーラーではなく、専用のランタイム統合ポーラーを使用するようになったためです。
- 変更: ビルドタグから
-
src/pkg/runtime/defs_darwin.go
,src/pkg/runtime/defs_darwin_386.h
,src/pkg/runtime/defs_darwin_amd64.h
:- 変更: Darwin固有のシステムコール定数(
EINTR
,EFAULT
など)やkqueue
関連の定数(EV_ADD
,EV_DELETE
,EVFILT_READ
,EVFILT_WRITE
など)、およびTimespec
,Kevent
といった構造体の定義が追加されました。これにより、Goランタイムがkqueue
システムコールを直接呼び出すために必要なCの定義がGo側から利用可能になります。
- 変更: Darwin固有のシステムコール定数(
-
src/pkg/runtime/netpoll.goc
:- 新規追加: ネットワークポーラーのプラットフォーム非依存なコアロジックが実装されています。
PollDesc
構造体の定義、ポーラーの初期化、FDのオープン/クローズ、読み書きの待機、デッドライン設定、そしてgoroutineのアンブロックといった機能が提供されます。
- 新規追加: ネットワークポーラーのプラットフォーム非依存なコアロジックが実装されています。
-
src/pkg/runtime/netpoll_kqueue.c
:- 新規追加: Darwin環境における
kqueue
システムコールを利用したネットワークポーラーの具体的な実装です。kqueue
の初期化、FDの登録、イベントのポーリング、そしてイベント発生時のruntime_netpollready
の呼び出しなどが含まれます。
- 新規追加: Darwin環境における
-
src/pkg/runtime/netpoll_stub.c
:- 変更: ビルドタグに
darwin
が追加されました。これは、他のOSと同様に、Darwinもこのスタブファイルを通じてruntime·netpoll
関数を公開するようになったことを示唆しています。ただし、実際のポーリングはnetpoll_kqueue.c
で行われます。
- 変更: ビルドタグに
-
src/pkg/runtime/runtime.h
:- 変更:
PollDesc
構造体の前方宣言と、runtime·netpollinit
,runtime·netpollopen
,runtime·netpollready
といった新しいランタイムポーラー関数のプロトタイプ宣言が追加されました。
- 変更:
-
src/pkg/runtime/sys_darwin_386.s
,src/pkg/runtime/sys_darwin_amd64.s
:- 変更: Darwinの32ビットおよび64ビットアーキテクチャ向けに、
kqueue
、kevent
、fcntl
(closeonexec
用)システムコールを呼び出すためのGoアセンブリコードが追加されました。
- 変更: Darwinの32ビットおよび64ビットアーキテクチャ向けに、
これらの変更は、GoのネットワークI/Oスタックを再構築し、ランタイムとOSのI/O多重化メカニズムとの連携を強化することを目的としています。
コアとなるコードの解説
このコミットのコアとなるロジックは、主にsrc/pkg/runtime/netpoll.goc
とsrc/pkg/runtime/netpoll_kqueue.c
に実装されています。
src/pkg/runtime/netpoll.goc
の解説
このファイルは、ネットワークポーラーのプラットフォーム非依存な部分を担い、Goランタイムのスケジューラと密接に連携します。
-
PollDesc
構造体:struct PollDesc { PollDesc* link; // in pollcache, protected by pollcache.Lock Lock; // protectes the following fields bool closing; uintptr seq; // protects from stale timers and ready notifications G* rg; // G waiting for read or READY (binary semaphore) Timer rt; // read deadline timer (set if rt.fv != nil) int64 rd; // read deadline G* wg; // the same for writes Timer wt; int64 wd; };
この構造体は、各ネットワークファイルディスクリプタ(FD)の状態を管理します。
link
:pollcache
内のフリーリストを形成するためのポインタ。Lock
:PollDesc
のフィールドを保護するためのミューテックス。closing
: FDがクローズ中であるかを示すフラグ。seq
: シーケンス番号。PollDesc
が再利用されたり、デッドラインがリセットされたりした場合にインクリメントされます。これにより、古いタイマーイベントやポーラーからの通知が誤って処理されるのを防ぎます。rg
,wg
: 読み込み/書き込みで待機しているgoroutineへのポインタ。READY
という特殊な値は、I/Oが完了しており、goroutineがすぐに実行可能であることを示します。rt
,wt
: 読み込み/書き込みデッドライン用のタイマー。rd
,wd
: 読み込み/書き込みデッドラインの時刻(ナノ秒単位)。
-
runtime_pollOpen(fd int) (pd *PollDesc, errno int)
: 新しいネットワークFDがオープンされる際に呼び出されます。pollcache
からPollDesc
インスタンスを割り当て、初期化します。その後、プラットフォーム固有のruntime·netpollopen
を呼び出して、OSのポーラーにFDを登録します。 -
runtime_pollWait(pd *PollDesc, mode int) (err int)
: 指定されたPollDesc
のFDでI/Oイベントが発生するまで、現在のgoroutineをブロックします。netpollblock
関数を呼び出し、goroutineをpd->rg
またはpd->wg
に設定し、runtime·park
で待機状態にします。I/Oイベントが発生すると、netpollunblock
によってgoroutineが再開されます。 -
runtime_pollSetDeadline(pd *PollDesc, d int64, mode int)
: 指定されたPollDesc
のFDに読み込み/書き込みデッドラインを設定します。既存のタイマーをリセットし、新しいデッドラインに基づいてランタイムタイマー(runtime·addtimer
)を登録します。pd->seq
をインクリメントすることで、古いタイマーイベントが新しいデッドラインと競合しないようにします。 -
runtime_pollUnblock(pd *PollDesc)
:PollDesc
をアンブロックし、待機中のgoroutineを強制的に再開させます。これは、FDがクローズされる際などに呼び出されます。pd->closing
フラグを設定し、pd->seq
をインクリメントして、関連するタイマーを無効化します。 -
runtime·netpollready(G **gpp, PollDesc *pd, int32 mode)
: プラットフォーム固有のポーラー(例:kqueue
)からI/Oイベントが通知された際に呼び出されます。指定されたPollDesc
のrg
またはwg
をアンブロックし、関連するgoroutineを*gpp
リストに追加して、スケジューラが実行可能としてキューイングできるようにします。
src/pkg/runtime/netpoll_kqueue.c
の解説
このファイルは、Darwin環境でkqueue
システムコールを直接操作し、ネットワークイベントを監視します。
-
runtime·netpollinit(void)
: Goランタイムの起動時に一度だけ呼び出され、kqueue
インスタンスを作成します。runtime·kqueue()
システムコールラッパーを使用して、新しいkqueue
ファイルディスクリプタを取得します。 -
runtime·netpollopen(int32 fd, PollDesc *pd)
:runtime_pollOpen
から呼び出され、指定されたfd
をkqueue
に登録します。Kevent ev[2]; // ... ev[0].ident = fd; ev[0].filter = EVFILT_READ; ev[0].flags = EV_ADD|EV_RECEIPT|EV_CLEAR; // EV_CLEAR for edge-triggered ev[0].udata = (byte*)pd; // Associate PollDesc with the event ev[1] = ev[0]; ev[1].filter = EVFILT_WRITE; n = runtime·kevent(kq, ev, 2, ev, 2, nil);
ここでは、読み込み(
EVFILT_READ
)と書き込み(EVFILT_WRITE
)の両方のイベントをEV_ADD
フラグで追加します。EV_CLEAR
フラグは、イベントが一度通知されたら自動的にクリアされるエッジトリガーモードを有効にします。udata
フィールドには、対応するPollDesc
へのポインタが格納され、イベント発生時にどのPollDesc
に関連するかを識別できるようにします。 -
runtime·netpoll(bool block)
: Goスケジューラから呼び出され、kqueue
からI/Oイベントをポーリングします。Kevent events[64], *ev; // ... n = runtime·kevent(kq, nil, 0, events, nelem(events), tp); // ... for(i = 0; i < n; i++) { ev = &events[i]; if(ev->filter == EVFILT_READ) runtime·netpollready(&gp, (PollDesc*)ev->udata, 'r'); if(ev->filter == EVFILT_WRITE) runtime·netpollready(&gp, (PollDesc*)ev->udata, 'w'); }
runtime·kevent
を呼び出して、発生したイベントを取得します。block
がtrue
の場合、イベントが発生するまでブロックします。取得したイベントごとに、ev->udata
から対応するPollDesc
を取得し、runtime·netpollready
を呼び出して、関連するgoroutineを実行可能状態にします。
これらのファイルは連携して、GoのgoroutineとOSのI/O多重化メカニズム(kqueue
)の間で効率的な橋渡しを行い、非ブロッキングI/Oとデッドライン処理をGoランタイムのスケジューラに統合することで、高性能なネットワークI/Oを実現しています。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットのChange List (CL): https://golang.org/cl/7569043
参考にした情報源リンク
- Go言語のドキュメント(特に
net
パッケージとランタイムに関する部分) kqueue
およびkevent
システムコールのmanページ(macOS/FreeBSDのドキュメント)- Goランタイムのスケジューラに関する技術記事や解説
- 非ブロッキングI/OとI/O多重化(
epoll
,kqueue
など)に関する一般的なオペレーティングシステムの概念