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

[インデックス 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ゴルーチン内のネットワークポーリングロジックの変更です。変更前は、sysmonruntime.sched.lastpollの値をチェックし、もし現在の時刻nowlastpollから10ミリ秒以上経過している場合(lastpoll + 10*1000*1000 > nowが偽となる場合)、sysmonruntime.netpoll(false)を呼び出して非ブロッキングでネットワークイベントをポーリングしていました。この条件が満たされると、sysmonは自身の20マイクロ秒ごとのサイクルで毎回netpollを呼び出し続けました。

問題は、lastpollが更新されない限り、この「過剰ポーリングモード」が解除されないことでした。lastpollは、他のゴルーチンがnetpollでブロックしたときにのみ更新される設計になっていました。そのため、ネットワークI/Oが少ない状況では、lastpollが更新されず、sysmonが永遠に過剰なポーリングを続ける可能性がありました。

このコミットでは、この過剰ポーリングを防ぐために、sysmonが非ブロッキングのnetpollを呼び出す直前に、runtime.sched.lastpollを現在の時刻nowでアトミックに更新する処理を追加しています。

変更後のロジックは以下のようになります。

  1. sysmonruntime.sched.lastpollの値を読み込みます。
  2. 現在の時刻nowを取得します。
  3. もし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ソースコード (src/pkg/runtime/proc.c)
  • Go Issue Tracker (Issue #5922)
  • Goのドキュメントおよび関連する技術記事(sysmon, netpoll, アトミック操作に関する一般的な知識)