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

[インデックス 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ランタイムのコア部分に統合することで、以下の課題を解決しようとしています。

  1. コンテキストスイッチの削減: 従来のポーラーでは、ネットワークI/Oの待機とgoroutineのスケジューリングの間に余分なオーバーヘッドが発生していました。ランタイム統合により、I/Oイベントの発生とgoroutineの再開がより密接に連携し、コンテキストスイッチの回数を減らすことができます。
  2. スケーラビリティの向上: 多数の同時接続を扱う際に、ポーラーがボトルネックになる可能性がありました。ランタイム統合されたポーラーは、より効率的なイベント処理とgoroutineの管理により、スケーラビリティを向上させます。
  3. デッドライン処理の改善: ネットワーク操作にはタイムアウト(デッドライン)が設定されることがよくあります。新しいポーラーは、デッドライン処理をランタイムのタイマー機構と統合することで、より正確かつ効率的なタイムアウト管理を実現します。
  4. プラットフォーム固有の最適化: Darwinにおけるkqueueのような高性能なI/O多重化メカニズムを、ランタイムレベルで直接、かつ最適に利用できるようになります。これにより、OSが提供する機能を最大限に活用し、ネイティブに近いパフォーマンスを引き出すことが可能になります。

ベンチマーク結果が示すように、この変更はTCP接続の永続的な処理やタイムアウト処理において、大幅な性能改善をもたらしており、Goアプリケーションのネットワーク性能向上に大きく貢献しています。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. ネットワークポーラー (Network Poller): ネットワークポーラーは、オペレーティングシステムが提供するI/O多重化メカニズム(例: Linuxのepoll、macOS/BSDのkqueue、WindowsのI/O Completion Ports (IOCP))を利用して、複数のファイルディスクリプタ(ソケットなど)からのI/Oイベント(読み込み可能、書き込み可能など)を効率的に監視するコンポーネントです。アプリケーションはポーラーに監視対象のディスクリプタを登録し、イベントが発生するまで待機します。イベントが発生すると、ポーラーはどのディスクリプタでどのようなイベントが発生したかを通知し、アプリケーションはそれに応じてI/O処理を進めます。これにより、ブロッキングI/Oで発生するスレッドの無駄な待機を防ぎ、少数のスレッドで多数の同時接続を効率的に処理できるようになります。

  2. kqueue (Kernel Event Queue): kqueueは、FreeBSD、macOS、NetBSD、OpenBSDなどのBSD系UNIXシステムで利用される、高性能なI/Oイベント通知インターフェースです。これは、ファイルディスクリプタだけでなく、プロセス状態の変化、タイマー、シグナルなど、様々なカーネルイベントを監視できます。kqueueは、イベントの登録、変更、削除を行うkeventシステムコールを介して操作されます。epollと同様に、エッジトリガー(イベント発生時に一度だけ通知)とレベルトリガー(状態が変化するまで通知し続ける)の両方をサポートしますが、このコミットでは主にエッジトリガーモード(EV_CLEARフラグ)が利用されています。

  3. Goランタイム (Go Runtime): Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(goroutineの管理)、メモリ管理、そしてI/O処理の抽象化などが含まれます。Goのスケジューラは、OSスレッド(M: Machine)上でgoroutine(G)を実行し、I/O待機などでgoroutineがブロックされると、そのgoroutineを一時停止し、別の実行可能なgoroutineを同じOSスレッド上で実行します。ネットワークポーラーは、このスケジューラと密接に連携し、I/O待機中のgoroutineを効率的に管理します。

  4. goroutine: Go言語における軽量な並行処理の単位です。OSスレッドよりもはるかに軽量で、数百万個のgoroutineを同時に実行することも可能です。goroutineはGoランタイムのスケジューラによって管理され、I/O操作などでブロックされると、自動的にスケジューラによって一時停止され、他のgoroutineにCPUが譲られます。I/Oが完了すると、スケジューラによって再び実行可能状態に戻されます。

  5. 非ブロッキングI/O (Non-blocking I/O): 通常のブロッキングI/Oでは、I/O操作が完了するまで呼び出し元のスレッドが停止します。非ブロッキングI/Oでは、I/O操作がすぐに戻り、データが利用可能でない場合や書き込みバッファが満杯の場合にはエラー(例: EAGAINEWOULDBLOCK)を返します。ネットワークポーラーは、この非ブロッキングI/Oと組み合わせて使用され、I/O操作がすぐに完了しない場合に、ポーラーにイベントの監視を依頼し、スレッドを解放して他の処理を行えるようにします。

  6. PollDesc構造体: このコミットで導入されるPollDescは、Goランタイムがネットワークファイルディスクリプタ(FD)の状態を管理するために使用する重要なデータ構造です。各FDに対応するPollDescインスタンスが作成され、そのFDに対する読み書きの待機状態、デッドライン(タイムアウト)、関連するgoroutineなどの情報が保持されます。PollDescは、ポーラーとスケジューラの間の橋渡し役となり、I/Oイベントの発生時に適切なgoroutineを再開させる役割を担います。

技術的詳細

このコミットの核心は、GoランタイムがネットワークI/Oを処理する方法を根本的に変更し、特にDarwin環境でのkqueueの利用を最適化することにあります。

変更のアーキテクチャ:

  1. netパッケージからの分離: 従来のネットワークポーラーはsrc/pkg/net/fd_darwin.goにあり、netパッケージの一部として実装されていました。このコミットでは、このファイルを削除し、ポーラーのロジックをGoランタイムのコア部分(src/pkg/runtime)に移動しています。これにより、ネットワークI/Oの待機とgoroutineのスケジューリングがより密接に連携できるようになります。

  2. プラットフォーム非依存部分 (netpoll.goc): src/pkg/runtime/netpoll.gocは、ネットワークポーラーのプラットフォーム非依存なロジックを実装しています。これはC言語で書かれており、Goランタイムの内部構造(goroutine、タイマー、ロックなど)に直接アクセスできます。

    • PollDesc構造体を定義し、各ネットワークFDの状態(読み書きの待機goroutine、デッドライン、シーケンス番号など)を管理します。
    • runtime_pollOpenruntime_pollCloseruntime_pollResetruntime_pollWaitruntime_pollSetDeadlineruntime_pollUnblockといった関数を提供し、netパッケージからランタイムポーラーを操作するためのインターフェースとなります。
    • runtime_netpollready関数は、プラットフォーム固有のポーラー(例: kqueue)からI/Oイベントが通知された際に呼び出され、対応するPollDescの待機goroutineをREADY状態にし、スケジューラに実行可能としてキューイングします。
    • デッドライン(タイムアウト)処理は、ランタイムのタイマー機構と統合されています。PollDesc内のrd(read deadline)とwd(write deadline)フィールド、およびTimer構造体を使用して、指定されたデッドラインに達した際に自動的にgoroutineをアンブロックする仕組みが実装されています。seq(シーケンス番号)は、PollDescが再利用されたり、デッドラインがリセットされたりした場合に、古いタイマーイベントを無視するためのメカニズムとして機能します。
  3. Darwin固有の実装 (netpoll_kqueue.c): src/pkg/runtime/netpoll_kqueue.cは、Darwin環境におけるkqueueシステムコールを利用したネットワークポーラーの具体的な実装です。

    • runtime_netpollinit関数でkqueueインスタンスを作成し、初期化します。
    • runtime_netpollopen関数は、新しいFDがポーラーに登録される際に呼び出され、kqueueEVFILT_READEVFILT_WRITEのイベントをEV_ADD|EV_RECEIPT|EV_CLEARフラグ付きで登録します。EV_CLEARはエッジトリガーモードを意味し、イベントが発生した際に一度だけ通知され、その後はイベントキューからクリアされます。EV_RECEIPTは、kevent呼び出しが成功したかどうかを即座に確認するために使用されます。
    • runtime_netpoll関数は、Goスケジューラから定期的に呼び出され、kqueueからI/Oイベントをポーリングします。イベントが発生すると、対応するPollDescとモード(読み込みまたは書き込み)を引数としてruntime_netpollreadyを呼び出し、関連するgoroutineを実行可能状態にします。
  4. システムコールラッパー (sys_darwin_386.s, sys_darwin_amd64.s): src/pkg/runtime/sys_darwin_386.ssrc/pkg/runtime/sys_darwin_amd64.sには、32ビットおよび64ビットDarwin環境向けのkqueuekeventfcntlcloseonexec用)システムコールのGoアセンブリラッパーが追加されています。これにより、Goランタイムが直接これらのOS機能にアクセスできるようになります。

  5. 定義ファイルの更新 (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側から利用可能になります。
  • src/pkg/runtime/netpoll.goc:

    • 新規追加: ネットワークポーラーのプラットフォーム非依存なコアロジックが実装されています。PollDesc構造体の定義、ポーラーの初期化、FDのオープン/クローズ、読み書きの待機、デッドライン設定、そしてgoroutineのアンブロックといった機能が提供されます。
  • src/pkg/runtime/netpoll_kqueue.c:

    • 新規追加: Darwin環境におけるkqueueシステムコールを利用したネットワークポーラーの具体的な実装です。kqueueの初期化、FDの登録、イベントのポーリング、そしてイベント発生時のruntime_netpollreadyの呼び出しなどが含まれます。
  • 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ビットアーキテクチャ向けに、kqueuekeventfcntlcloseonexec用)システムコールを呼び出すためのGoアセンブリコードが追加されました。

これらの変更は、GoのネットワークI/Oスタックを再構築し、ランタイムとOSのI/O多重化メカニズムとの連携を強化することを目的としています。

コアとなるコードの解説

このコミットのコアとなるロジックは、主にsrc/pkg/runtime/netpoll.gocsrc/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イベントが通知された際に呼び出されます。指定されたPollDescrgまたは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から呼び出され、指定されたfdkqueueに登録します。

    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を呼び出して、発生したイベントを取得します。blocktrueの場合、イベントが発生するまでブロックします。取得したイベントごとに、ev->udataから対応するPollDescを取得し、runtime·netpollreadyを呼び出して、関連するgoroutineを実行可能状態にします。

これらのファイルは連携して、GoのgoroutineとOSのI/O多重化メカニズム(kqueue)の間で効率的な橋渡しを行い、非ブロッキングI/Oとデッドライン処理をGoランタイムのスケジューラに統合することで、高性能なネットワークI/Oを実現しています。

関連リンク

参考にした情報源リンク

  • Go言語のドキュメント(特にnetパッケージとランタイムに関する部分)
  • kqueueおよびkeventシステムコールのmanページ(macOS/FreeBSDのドキュメント)
  • Goランタイムのスケジューラに関する技術記事や解説
  • 非ブロッキングI/OとI/O多重化(epoll, kqueueなど)に関する一般的なオペレーティングシステムの概念