GoProgrammingシニアGoバックエンド開発者

**Go**のスケジューラは、オペレーティングシステムに依存せず、単一のCPUバウンドゴルーチンが他の実行可能なゴルーチンを飢餓させないようにどのように防ぐのか?

Hintsage AIアシスタントで面接を突破

質問への回答

Goのスケジューラは、OSの介入なしに飢餓を防ぐために、ハイブリッドな協調的および先制的マルチタスクモデルを採用しています。バージョン1.14以降、ランタイムは非同期の先制ポイントを注入するために、タイムスライス(通常は10ms)を超えたゴルーチンを実行しているスレッドにSIGURG信号を送信します。信号ハンドラが安全なポイント(ゴルーチンが関数を呼び出すかスタックにアクセスしようとしているときなど)を検出すると、スケジューラはコンテキストを保存し、他の実行可能なゴルーチンに切り替えます。このメカニズムにより、関数呼び出しのない密なCPUバウンドループであっても、**プロセッサ(P)**を無期限に独占できないことが保証されます。

実生活の状況

私たちの高頻度取引プラットフォームは、市場のボラティリティ中に著しいレイテンシスパイクを経験しました。複雑なモンテカルロシミュレーションを実行する単一の分析ゴルーチンが、数百ミリ秒の間で受注処理パイプラインを凍結させました。問題は、関数呼び出しのない密な数学ループを実行しているゴルーチンが、Go 1.14以前にスケジューラによって先制されることを妨げていたことに起因していました。

私たちは、この競合を解決するために3つの異なるアプローチを評価しました。最初の選択肢は、シミュレーションループ内に手動で**runtime.Gosched()**呼び出しを挿入することでした。このアプローチは即時の緩和を提供しましたが、重大なメンテナンスの負担を引き起こし、開発者が深いスケジューラの知識を持っている必要があり、リファクタリングされた場合に脆弱なコードが生まれる可能性が高まりました。

二番目の解決策は、分析作業負荷をCPU制限のある別のマイクロサービスに隔離することを提案しました。これは硬い隔離と独立したスケーリングを提供しましたが、ネットワークシリアル化のオーバーヘッドとプロセス間通信の追加レイテンシが、リスク計算に対するサブミリ秒のレイテンシ要件に違反しました。

最終的に、私たちはランタイムをGo 1.20にアップグレードし、物理CPUコアに合わせてGOMAXPROCSを明示的に調整することを選択しました。このアップグレードにより、信号を介した非同期先制が提供され、コードの変更なしにスケジューラが10msごとにCPUバウンドゴルーチンを強制的に放棄できるようになりました。展開後のメトリクスは、ピーク負荷時にP99レイテンシが8msに安定し、タイムアウトのカスケードを排除し、単一プロセスのアーキテクチャのシンプルさを維持していることを示しました。

候補者が見落としがちなこと

関数呼び出しのない密なループが、古いGoバージョンでスケジューリングの問題を引き起こすが、新しいものでは引き起こさないのはなぜか?

Go 1.14以前は、スケジューラは協調的先制のみに依存していたため、ゴルーチンは関数呼び出し、チャネル操作、またはミューテックスの競合時にのみ自発的に放棄していました。純粋な算術演算を行う密なループは安全なポイントに到達しないため、そのプロセッサ(P)を完了まで独占します。最新のGoは、スレッドにSIGURG信号を送信して非同期先制を利用し、関数呼び出しが行われるかどうかにかかわらず次の安全なポイントでコンテキストスイッチをトリガーします。

プロセッサ(P)が利用可能になると、Goのスケジューラは次にどのゴルーチンを実行させると決定するのか?

スケジューラは、まず現在のPのローカル実行キューを確認し、次に別のPのローカルキューから半分のゴルーチンを盗むことを試みます。これは、競合を減らすためにランダム化された開始インデックスを使用します。ローカルキューが空の場合、スケジューラは新しく作成されたゴルーチンの飢餓を防ぐために、61スケジューラティックごとにグローバル実行キューをチェックします。この階層的選択は、同期コストを最小限に抑えながら、利用可能なすべての**マシン(M)**スレッド間での負荷分散を保証します。

ゴルーチンがファイル入出力などのブロッキングシステムコールを実行すると、プロセッサ(P)には何が起こるのか?

ゴルーチンがシステムコールでブロックすると、Goランタイムは直ちにマシン(M)スレッドをそのPから切り離し、そのPを新しいまたはアイドル状態のMに割り当てます。これにより、他のゴルーチンが同じOSスレッド抽象上で実行を続けることができます。元のMはシステムコールに入って、カーネルが操作を完了するのを待ちます。戻った後、それは元のPを再獲得しようとするか、Pが別のスレッドにバウンドしている場合は自らをパークします。このM:Nマルチプレクシングは、I/O中にOSスレッドがアイドル状態になるのを防ぎ、数千のゴルーチン全体で高いCPU使用率を維持します。