■ 1. 問題の概要
- Go 1.24以前は、コンテナのcgroup CPU制限を無視してGOMAXPROCSを設定する
- GOMAXPROCSはGoランタイムが制御するOSスレッド数であり、ホスト全体のCPU数に設定される
- EKS上で48コアのノードにCPU制限5コアを設定しても、GOMAXPROCSは48のままとなる
- 過剰な並列実行によりCPUスロットリングが発生し、スループット低下を引き起こす
■ 2. 検証環境
- macOS上のDocker Desktop(11コアのLinux VM)を使用
- Go 1.24でコンパイルしたベンチマークアプリを作成
- info、benchmark、throttle-demoの3モードを実装して検証
■ 3. 実験結果
- 実験1 (GOMAXPROCS無視の確認):
- CPU制限1コア(--cpus=1.0)でもGOMAXPROCSは11のまま
- cgroupには正しくcpu.max: 100000 100000が設定されている
- 実験2 (スループット低下):
- CPU制限1コア環境で100goroutineを実行した結果
- GOMAXPROCS=1の場合:21,504 Ops/sec
- GOMAXPROCS=8の場合:6,833 Ops/sec(68.2%低下)
- 実験3 (CPU停止時間の測定):
- CPU制限0.5コア、5秒間のテストを実施
- GOMAXPROCS=8の場合:39秒のCPU停止
- GOMAXPROCS=1の場合:2.6秒のCPU停止(14.8倍の差)
- スロットリング発生率は両者とも98%だが、実際の停止時間に大きな差がある
- 実験4 (線形相関):
- GOMAXPROCSを1から16に段階的に増加させると、累積CPU停止時間がほぼ比例して増加
- GOMAXPROCS=16では5秒のテストで50.3秒分のCPU停止が発生
■ 4. 技術的解説
- CFS帯域制御の仕組み:
- 各ピリオド(100ms)内でCPUクォータが割り当てられる
- n本のスレッドが同時稼働すると、クォータ枯渇速度がn倍になる
- クォータ枯渇後は全スレッドが一斉に停止する(Thundering Herd問題)
- CPU使用率では問題が見えない理由:
- CPU使用率は「クォータ消費量÷割り当て」で計算されるため、消費ペース(バースト性)を反映しない
- 同じ使用率100%でも、1スレッドが穏やかに消費する場合と複数スレッドが瞬時に消費する場合ではレイテンシが大きく異なる
■ 5. 監視ポイント
- 以下3指標をセットで監視することを推奨する
- nr_periods:スケジューラの計測ピリオド総数
- nr_throttled:スロットリングが発生したピリオド数
- throttled_usec:実際のCPU停止時間(マイクロ秒)であり、最も重要な指標
■ 6. 対策
- Go 1.25+では、cgroup対応のGOMAXPROCS自動設定が実装される
- Go 1.24以前では、uber-go/automaxprocsライブラリを使用してcgroup対応を実現する
- Ruby(Puma)、Java、Node.js、Nginxなど各言語ランタイムでも並列設定の確認が必要
- CPU使用率だけでなく、CPU停止時間メトリクス(throttled_usec)の監視も必須とする