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のトリックはありますか?以下のコメント欄に書き込んでください。お互いの成功(そして失敗)から学びましょう!