[インデックス 16820] ファイルの概要
このコミットは、Goランタイムにおけるsysmon
(システムモニター)がネットワークポーリングを過剰に行う問題を修正するものです。具体的には、ネットワークが一定時間(10ミリ秒)ポーリングされていない場合に、sysmon
が非常に短い間隔(20マイクロ秒ごと)でネットワークポーリングを開始し、CPUリソースを無駄に消費してしまう挙動を改善します。
コミット
commit 68572644576be5f1f7121428755e7d8af5b7044c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Jul 19 17:45:34 2013 +0400
runtime: prevent sysmon from polling network excessivly
If the network is not polled for 10ms, sysmon starts polling network
on every iteration (every 20us) until another thread blocks in netpoll.
Fixes #5922.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/11569043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68572644576be5f1f7121428755e7d8af5b7044c
元コミット内容
runtime: prevent sysmon from polling network excessivly
If the network is not polled for 10ms, sysmon starts polling network
on every iteration (every 20us) until another thread blocks in netpoll.
Fixes #5922.
変更の背景
Goランタイムには、ガベージコレクションのトリガー、スケジューラのポーリング、ネットワークI/Oの監視など、様々なバックグラウンドタスクを処理するsysmon
(システムモニター)というゴルーチンが存在します。sysmon
は通常、20マイクロ秒ごとに実行されます。
このコミットが修正する問題は、ネットワークI/Oのポーリングに関するものでした。GoのネットワークI/Oは、netpoll
というメカニズムを通じて非同期的に処理されます。通常、ネットワーク操作を行うゴルーチンは、データが利用可能になるまでnetpoll
でブロックします。しかし、もしネットワークI/Oが長時間行われない場合(具体的には10ミリ秒以上)、sysmon
はネットワークイベントを積極的にチェックし始めます。
問題は、この積極的なチェックが「過剰」であった点です。ネットワークが10ミリ秒以上ポーリングされていない状態が続くと、sysmon
は20マイクロ秒ごとの自身の実行サイクルごとにnetpoll
を呼び出すようになります。これは、ネットワークイベントがほとんど発生しない状況下でも、sysmon
がCPUリソースを消費し続け、無駄なポーリングを繰り返すことを意味します。特に、ネットワークI/Oが少ないアプリケーションや、アイドル状態のアプリケーションでは、この過剰なポーリングが不必要なCPU使用率の上昇を引き起こしていました。
この問題は、GoのIssue #5922として報告されており、このコミットはその解決を目的としています。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステム。スケジューラ、ガベージコレクタ、メモリ管理、システムコールインターフェースなどが含まれます。
- ゴルーチン (Goroutine): Goにおける軽量な実行スレッド。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理されます。
sysmon
(System Monitor): Goランタイム内部で動作する特別なゴルーチン。主な役割は以下の通りです。- スケジューラのポーリング: 実行可能なゴルーチンがないか定期的にチェックし、必要に応じてスケジューラを起動します。
- ネットワークポーリング: ネットワークI/Oの準備ができたファイルディスクリプタがないか監視します。
- ガベージコレクションのトリガー: GCの実行条件が満たされた場合にGCをトリガーします。
- デッドロック検出: デッドロック状態のゴルーチンがないか監視します。
runtime.nanotime()
: ナノ秒単位のモノトニック時間を返すGoランタイム関数。システム時刻の変更に影響されず、経過時間を正確に測定するために使用されます。
netpoll
: GoランタイムがネットワークI/Oを非同期的に処理するためのメカニズム。OSのepoll
(Linux),kqueue
(FreeBSD/macOS),IOCP
(Windows) などのI/O多重化APIを利用して、複数のネットワーク接続からのイベントを効率的に監視します。ゴルーチンはnetpoll
でブロックし、データが利用可能になったり、書き込みが可能になったりすると、ランタイムによって再開されます。runtime.atomicload64()
: アトミックに64ビット整数を読み込むGoランタイム関数。複数のゴルーチンから同時にアクセスされる共有変数に対して、競合状態を避けて安全に値を読み込むために使用されます。runtime.cas64()
(CompareAndSwap): アトミックな比較交換操作を行うGoランタイム関数。指定されたメモリ位置の値が期待値と一致する場合にのみ、その値を新しい値に更新します。この操作はアトミックに行われるため、複数のゴルーチンが同時にアクセスしてもデータの一貫性が保たれます。lastpoll
のような共有変数の更新に用いられます。runtime.sched.lastpoll
: Goランタイムのスケジューラに関するグローバルな状態を保持する構造体runtime.sched
の一部。lastpoll
フィールドは、最後にネットワークポーリングが行われた時刻(ナノ秒単位)を記録しています。
技術的詳細
このコミットの核心は、sysmon
ゴルーチン内のネットワークポーリングロジックの変更です。変更前は、sysmon
がruntime.sched.lastpoll
の値をチェックし、もし現在の時刻now
がlastpoll
から10ミリ秒以上経過している場合(lastpoll + 10*1000*1000 > now
が偽となる場合)、sysmon
はruntime.netpoll(false)
を呼び出して非ブロッキングでネットワークイベントをポーリングしていました。この条件が満たされると、sysmon
は自身の20マイクロ秒ごとのサイクルで毎回netpoll
を呼び出し続けました。
問題は、lastpoll
が更新されない限り、この「過剰ポーリングモード」が解除されないことでした。lastpoll
は、他のゴルーチンがnetpoll
でブロックしたときにのみ更新される設計になっていました。そのため、ネットワークI/Oが少ない状況では、lastpoll
が更新されず、sysmon
が永遠に過剰なポーリングを続ける可能性がありました。
このコミットでは、この過剰ポーリングを防ぐために、sysmon
が非ブロッキングのnetpoll
を呼び出す直前に、runtime.sched.lastpoll
を現在の時刻now
でアトミックに更新する処理を追加しています。
変更後のロジックは以下のようになります。
sysmon
はruntime.sched.lastpoll
の値を読み込みます。- 現在の時刻
now
を取得します。 - もし
lastpoll
が0でなく、かつlastpoll
から10ミリ秒以上経過していない場合(lastpoll + 10*1000*1000 > now
が真の場合)、以下の処理を実行します。runtime.cas64(&runtime.sched.lastpoll, lastpoll, now)
:runtime.sched.lastpoll
の値をlastpoll
(読み込んだ古い値)と比較し、もし一致すればnow
(現在の時刻)に更新します。このアトミック操作により、他のゴルーチンが同時にlastpoll
を更新しようとしても安全性が保たれます。gp = runtime.netpoll(false)
: 非ブロッキングでネットワークイベントをポーリングします。injectglist(gp)
: ポーリングによって準備ができたゴルーチンがあれば、スケジューラに投入します。
この変更により、sysmon
が非ブロッキングのnetpoll
を呼び出すたびにlastpoll
が更新されるため、sysmon
は常に「最後にポーリングした時刻」を最新の状態に保つことができます。これにより、sysmon
は10ミリ秒ごとに一度だけ非ブロッキングのnetpoll
を試みるようになり、ネットワークI/Oが少ない状況での過剰なポーリングが抑制されます。つまり、sysmon
自身がlastpoll
を更新することで、次のsysmon
の実行時には、前回のnetpoll
から10ミリ秒経過していないため、無駄なnetpoll
呼び出しがスキップされるようになります。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/proc.c
ファイル内のsysmon
関数に1行追加されただけです。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2098,6 +2098,7 @@ sysmon(void)
lastpoll = runtime·atomicload64(&runtime·sched.lastpoll);
now = runtime·nanotime();
if(lastpoll != 0 && lastpoll + 10*1000*1000 > now) {
+\t\t\truntime·cas64(&runtime·sched.lastpoll, lastpoll, now);\n \t\t\tgp = runtime·netpoll(false); // non-blocking
\t\t\tinjectglist(gp);\
\t\t}\
コアとなるコードの解説
追加された行は以下の通りです。
runtime·cas64(&runtime·sched.lastpoll, lastpoll, now);
runtime·cas64
: Goランタイム内部で使用されるアトミックな比較交換関数。C言語で書かれたランタイムコードからGoのアトミック操作を呼び出すためのシンタックスです。&runtime·sched.lastpoll
:runtime.sched
構造体内のlastpoll
フィールドのアドレス。このフィールドが更新対象です。lastpoll
:runtime·cas64
が実行される直前にruntime·atomicload64(&runtime·sched.lastpoll)
で読み込まれたlastpoll
の古い値。これが期待値として使用されます。now
:runtime·nanotime()
で取得された現在の時刻。これが新しい値としてlastpoll
に設定されます。
この1行の追加により、sysmon
が非ブロッキングのnetpoll
を呼び出すたびに、runtime.sched.lastpoll
が現在の時刻に更新されます。これにより、sysmon
は次のサイクルで、前回のnetpoll
から10ミリ秒経過していないと判断し、無駄なnetpoll
呼び出しをスキップするようになります。結果として、ネットワークI/Oが少ない状況でのCPU使用率の無駄な上昇が防がれます。
関連リンク
- Go Issue #5922: https://github.com/golang/go/issues/5922
- Go CL 11569043: https://golang.org/cl/11569043
参考にした情報源リンク
- Goソースコード (
src/pkg/runtime/proc.c
) - Go Issue Tracker (Issue #5922)
- Goのドキュメントおよび関連する技術記事(
sysmon
,netpoll
, アトミック操作に関する一般的な知識)