Go、コンテナ、Linux スケジューラ

1699387397
2023-11-07 19:10:04

多くの Go 開発者と同様に、私のアプリケーションは通常、コンテナーにデプロイされます。 コンテナー オーケストレーターで実行する場合は、コンテナーがホスト上のすべての CPU を消費しないように CPU 制限を設定することが重要です。 ただし、Go ランタイムはコンテナーに設定された CPU 制限を認識せず、利用可能なすべての CPU を喜んで使用します。 過去にこれが原因で遅延が大きくなったことがありました。このブログでは、何が起こっているのか、そしてそれを修正する方法を説明します。

Go ガベージ コレクターの仕組み

これは Go ガベージ コレクター (GC) のかなり高レベルの概要になります。 より詳細な概要については、読むことをお勧めします ゴードキュメント
この 素晴らしいブログシリーズ
ウィル・ケネディ著。

ほとんどの場合、Go ランタイムはプログラムの実行と同時にガベージ コレクションを実行します。 これは、GC がプログラムと同時に実行されていることを意味します。 ただし、GC プロセスには、Go ランタイムがすべての Goroutine を停止する必要があるポイントが 2 つあります。 これはデータの整合性を確保するために必要です。 GC のマーク フェーズの前に、ランタイムは書き込みバリアを適用するためにすべての Goroutine を停止します。これにより、この時点以降に作成されたオブジェクトがガベージ コレクションされなくなります。 このフェーズはスイープ終了として知られています。 マーク フェーズが終了した後、別のワールド フェーズの停止が行われます。これはマーク終了として知られ、同じプロセスが書き込みバリアを削除するために発生します。 通常、これには数十マイクロ秒程度かかります。

大量のメモリを割り当てる単純な Web アプリケーションを作成し、次のコマンドを使用して 4 CPU コアの制限を持つコンテナーで実行しました。このソース コードは利用可能です。 ここ。

docker run --cpus=4 -p 8080:8080 $(ko build -L main.go)

トレースを収集するには、 ランタイム/トレース パッケージ化してからそれを分析します go tool trace。 次のトレースは、私のマシンでキャプチャされた GC サイクルを示しています。 スイープ ターミネーションとマーク ターミネーションがワールド フェーズを停止していることがわかります。 Proc 5 (ストップ・ザ・ワールドのSTWというラベルが付いています)。

この GC サイクルには 2.5 ミリ秒弱かかりましたが、その 10% 近くを世界停止フェーズに費やしました。 これは、特に遅延の影響を受けやすいアプリケーションを実行している場合には、かなりの時間になります。

Linux スケジューラ

完全に公平なスケジューラー (CFS) Linux 2.6.23 で導入され、先週リリースされた Linux 6.6 まではデフォルトのスケジューラでした。 CFS を使用している可能性があります。

CFS は、 比例配分スケジューラ、これは、プロセスの重みが、使用が許可されている CPU コアの数に比例することを意味します。 たとえば、プロセスで 4 つの CPU コアの使用が許可されている場合、その重みは 4 になります。プロセスで 2 つの CPU コアの使用が許可されている場合、重みは 2 になります。

CFS は、CPU 時間の一部を割り当てることでこれを実行します。 4 コア システムでは、毎秒 4 秒の CPU 時間が割り当てられます。 コンテナーに多数の CPU コアを割り当てると、基本的に Linux スケジューラーに割り当てを要求することになります。 n 時間をかける価値のある CPU。

上記では docker run コマンドには 4 CPU に相当する時間が必要です。 これは、コンテナーが毎秒 4 秒の CPU 時間を取得することを意味します。

問題

Go ランタイムが起動すると、CPU コアごとに OS スレッドが作成されます。 これは、16 コアのマシンがある場合、CGroup の CPU 制限に関係なく、Go ランタイムは 16 個の OS スレッドを作成することを意味します。 Go ランタイムはこれらの OS スレッドを使用してゴルーチンをスケジュールします。

問題は、Go ランタイムが CGroup の CPU 制限を認識しておらず、16 個の OS スレッドすべてでゴルーチンを問題なくスケジュールしてしまうことです。 これは、Go ランタイムが毎秒 16 秒の CPU 時間を使用できることを期待することを意味します。

ストップ ザ ワールドの長い停止時間は、Go ランタイムが Linux スケジューラのスケジュールを待っているスレッド上で Goroutine を停止する必要があることから発生します。 コンテナーが CPU クォータを使用すると、これらのスレッドはスケジュールされなくなります。

ソリューション

Go では、ランタイムが作成する CPU スレッドの数を制限できます。 GOMAXPROCS 環境変数。 今回は以下のコマンドを使用してコンテナを起動しました

docker run --cpus=4 -e GOMAXPROCS=4 -p 8080:8080 $(ko build -L main.go)

以下は、上記と同じアプリケーションからキャプチャされたトレースです。 GOMAXPROCS CPU クォータと一致する環境変数。

GC トレース

このトレースでは、まったく同じ負荷があるにもかかわらず、ガベージ コレクションがはるかに短くなります。 GC サイクルは 1 ミリ秒未満で、ワールド フェーズの停止は 26 μ秒で、制限がなかった場合の約 1/10 の時間でした。

GOMAXPROCS 端数の CPU を割り当てる場合はコンテナが使用できる CPU コアの数に設定する必要があります。ただし、1 CPU コア未満を割り当てる場合は切り上げます。 GOMAXPROCS=max(1, floor(CPUs)) 値を計算するために使用できます。 もっと簡単だと思うなら、Uber はライブラリをオープンソースにしました automaxproc コンテナの cgroup からこの値を自動的に計算します。

Read more:  スーパーリーグの6チームがPFDKから罰金を科された

未解決のものがあります Githubの問題 Go ランタイムではこれをすぐにサポートできるため、最終的には追加されることを願っています。

結論

コンテナー化されたアプリケーションで Go を実行する場合は、CPU 制限を設定することが重要です。 賢明な設定を行うことで、Go ランタイムがこれらの制限を確実に認識できるようにすることも重要です。 GOMAXPROCS 値を使用するか、次のようなライブラリを使用します automaxproc

#GoコンテナLinux #スケジューラ

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Recent News

Editor's Pick