NUMAの難題

スケジューラの調整に入る前に、背景を説明しましょう。非一様メモリアクセス(NUMA)アーキテクチャは、現代のサーバーハードウェアでは一般的になっています。しかし、ここでの問題は、多くの開発者がGoのマイクロサービスを、まるで均一なメモリアクセスで動作しているかのように開発・展開していることです。これは、四角いペグを丸い穴に無理やり押し込むようなもので、動作はするかもしれませんが、最適とは言えません。

GoマイクロサービスにおけるNUMAの重要性

Goのランタイムは非常に賢いですが、全知ではありません。NUMAに関しては、私たちが少し手助けをする必要があります。GoマイクロサービスにおいてNUMA対応が重要な理由は以下の通りです:

  • ローカルとリモートのNUMAノード間でメモリアクセスの遅延が大きく異なることがある
  • スレッドとメモリの配置が不適切だと、パフォーマンスが低下する可能性がある
  • GoのガベージコレクタのパフォーマンスがNUMAの影響を受ける可能性がある

GoマイクロサービスでNUMAを無視することは、旅行の計画を立てる際に交通渋滞を無視するようなものです。目的地には到達できるかもしれませんが、旅はスムーズではありません。

完全公平スケジューラ(CFS)の登場

さて、ここで主役の登場です:完全公平スケジューラ(CFS)。その名前にもかかわらず、CFSはNUMAシステムにおいて必ずしも完全に公平ではありません。しかし、少し調整を加えることで、Goマイクロサービスにとって素晴らしい効果を発揮することができます。

CFS: 良い点、悪い点、そしてNUMAの醜い点

CFSは公平であるように設計されています。各プロセスに等しいCPU時間を与えようとします。しかし、NUMAの世界では、公平性が必ずしも望ましいわけではありません。最適なパフォーマンスを達成するためには、時には少し不公平である必要があります。以下はその概要です:

  • 良い点: CFSは全体的なシステムの応答性と公平性を提供します
  • 悪い点: NUMAノード間で不要なタスクの移動が発生する可能性があります
  • NUMAの醜い点: 適切な調整がないと、Goマイクロサービスにとってメモリアクセスの遅延が増加する可能性があります

NUMA対応のGoマイクロサービスのためのCFSの調整

さて、スケジューラの調整に取り掛かりましょう。以下の主要な領域に焦点を当てます:

1. スケジューリングドメインの調整

スケジューリングドメインは、スケジューラがシステムのトポロジーをどのように見るかを定義します。これを調整することで、CFSをよりNUMA対応にすることができます:


# 現在のスケジューリングドメインを確認
cat /proc/sys/kernel/sched_domain/cpu0/domain*/name

# スケジューリングドメインのパラメータを調整
echo 1 > /proc/sys/kernel/sched_domain/cpu0/domain0/prefer_local_spreading

これにより、スケジューラは可能な限り同じNUMAノード上でタスクを保持することを優先し、不要な移動を減らします。

2. sched_migration_cost_nsの微調整

このパラメータは、スケジューラがCPU間でタスクを移動する際の積極性を制御します。NUMAシステムでGoマイクロサービスを実行する場合、この値を増やすことがよくあります:


# 現在の値を確認
cat /proc/sys/kernel/sched_migration_cost_ns

# 値を増やす(例:1000000ナノ秒に設定)
echo 1000000 > /proc/sys/kernel/sched_migration_cost_ns

この変更により、スケジューラがNUMAノード間でタスクを移動する可能性が低くなり、リモートメモリアクセスの可能性が減少します。

3. NUMA対応のリソース割り当てのためのcgroupsの活用

コントロールグループ(cgroups)は、NUMA対応のリソース割り当てを強制するための強力なツールです。以下は、cgroupsを使用してGoマイクロサービスを特定のNUMAノードに固定する簡単な例です:


# Goマイクロサービス用のcgroupを作成
mkdir /sys/fs/cgroup/cpuset/go_microservice

# CPUとメモリノードを割り当て
echo "0-3" > /sys/fs/cgroup/cpuset/go_microservice/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/go_microservice/cpuset.mems

# このcgroup内でGoマイクロサービスを実行
cgexec -g cpuset:go_microservice ./my_go_microservice

これにより、Goマイクロサービスが単一のNUMAノードのCPUとメモリのみを使用し、ノード間のメモリアクセスを減少させます。

Goランタイム: NUMA対応の味方

スケジューラの調整に焦点を当てていますが、GoのランタイムもNUMA対応のための味方になり得ます。以下はGoに特化したいくつかのヒントです:

1. GOGCとNUMA

GOGC環境変数は、Goのガベージコレクタの動作を制御します。NUMAシステムでは、この値を調整してグローバルコレクションの頻度を減らすことができます:


export GOGC=200

これにより、Goランタイムがガベージコレクションをあまり頻繁にトリガーしないようにし、コレクション中のノード間メモリアクセスを減少させる可能性があります。

2. runtime.NumCPU()の活用

NUMAシステム用にGoコードを書く際には、ゴルーチンの使用方法に注意を払う必要があります。以下は、NUMA対応のワーカープールを作成する簡単な例です:


import "runtime"

func createNUMAAwareWorkerPool() {
    numCPU := runtime.NumCPU()
    for i := 0; i < numCPU; i++ {
        go worker(i)
    }
}

func worker(id int) {
    runtime.LockOSThread()
    // ワーカーのロジック
}

runtime.NumCPU()runtime.LockOSThread()を使用することで、NUMA境界を尊重する可能性が高いワーカープールを作成しています。

影響の測定

これらの調整は素晴らしいですが、実際に効果があるかどうかを確認する方法は?以下は注目すべきツールとメトリクスです:

  • numastat: NUMAメモリ統計を提供します
  • perf: キャッシュミスやメモリアクセスパターンを測定するために使用できます
  • Goの組み込みプロファイリング: runtime/pprofを使用して、調整前後のアプリケーションをプロファイルします

以下は、numastatを使用してNUMAメモリ使用量を確認する簡単な例です:


numastat -p $(pgrep my_go_microservice)

NUMAノード間のメモリ割り当ての不均衡を探します。「外国」メモリアクセスが多い場合、調整が必要かもしれません。

落とし穴と注意点

すべてのシステムを調整し始める前に、注意点をいくつか:

  • 過度の調整はリソースの未利用を招く可能性があります
  • あるGoマイクロサービスに有効なものが、他のものには有効でないかもしれません
  • スケジューラの調整は、Goのランタイムの動作と複雑に相互作用する可能性があります

常に測定、テスト、変更を制御された環境で検証してから、本番環境にプッシュしてください。大きな力には大きな責任が伴うことを忘れずに(そして注意しないと大きな頭痛の種になる可能性もあります)。

まとめ: バランスの芸術

NUMA対応のGoマイクロサービスのための完全公平スケジューラの調整は、まさに芸術です。公平性、パフォーマンス、リソース利用のバランスを見つけることが重要です。以下は重要なポイントです:

  • ハードウェアを理解する: NUMAアーキテクチャは重要です
  • NUMAを考慮してCFSパラメータを調整する
  • cgroupsを活用して詳細な制御を行う
  • Goのランタイムと協力する
  • 常に調整の効果を測定し、検証する

目標は、完全にNUMA対応のシステムを作成することではなく(それは事実上不可能です)、NUMAアーキテクチャの制約内でGoマイクロサービスが最適に動作するスイートスポットを見つけることです。

次に誰かが「ただのスケジューラだよ、どれだけ複雑になり得るの?」と言ったら、微笑んでこの記事を指し示してください。調整を楽しんでください、そしてGoマイクロサービスがNUMAノード間でスムーズに動作し続けることを願っています!

"NUMA対応のGoマイクロサービスの世界では、スケジューラは単なる審判ではなく、コードとハードウェアの間の複雑なダンスの振付師です。"

NUMAシステムのスケジューラ調整に関する戦いの話はありますか?またはNUMA対応のための巧妙なGoのトリックはありますか?以下のコメント欄に書き込んでください。お互いの成功(そして失敗)から学びましょう!