■ 1. 概要
- 発表: クラウドネイティブ会議 2026年5月14日
- 発表者: James Kirk(株式会社エウレカ、Engineering Manager)
- テーマ: 恋活・婚活マッチングアプリ「Pairs(ペアーズ)」本番環境におけるcgroup-aware化の取り組みと事例
■ 2. 対象システム: pairs-main(2024年当時)
- コア体験の中心にあるAPIで、Go言語で実装、Amazon EKS上で運用
- 1 Nodeで二桁のpairs-main Podが稼働
- ワークロードはI/Oヘビーで並行処理が多い
- Amazon Aurora MySQLのリードレプリカをHAProxyでロードバランス
■ 3. Act 1: CPUリソースの「空回り」
- GoのCPU改善の経緯:
- CPUプロファイル駆動でGoのガベージコレクター(GC)を主なターゲットに改善を実施
- 実施内容: RDBのORM置き換え、DynamoDB SDKの独自シリアライザの開発、ホットパスのallocation削減
- 結果: Go GCのCPU使用率を60%以上削減
- GOMAXPROCSの過剰並列:
- GoのランタイムスケジューラがCPUの約15%を占有していたため、次の改善対象として調査を開始
- Goはgoroutineをワーカースレッドに割り当てて実行し、スレッド数はGOMAXPROCS変数で調整される
- GOMAXPROCSがCPUコア数を超えると過剰並列となり、GoとOSスケジューラのオーバーヘッドが増大してCPU使用率が悪化する
- pairs-mainではlimits.cpu=5000m(5コア分)に対し、GOMAXPROCS=48(ノードのコア数)が設定されていた
- Go 1.24以前はGOMAXPROCSのデフォルト値がコンテナのLimitsを考慮しなかった(1.25からcontainer-aware化)
- GOMAXPROCS変更後の初期結果と問題:
- GOMAXPROCS=5に変更後: スループット大幅増加、Goスケジューラのcpu使用率50%以上削減
- 夕方になるとレイテンシーが急増し、Pod CPUは安定していたためオートスケールが反応しなかった
- 変更をロールバックし、根本原因の調査を開始
■ 4. Act 2: 監視の死角に潜んでいたもの
- レイテンシー急増のドリルダウン:
- 平均レイテンシーが急増し、エンドポイントの偏りはなし
- Goのスループット改善によりdownstreamの詰まりを疑い、アプリケーション層のトレースでサービス別レイテンシーを確認
- Auroraリードレプリカのレイテンシーが急増前から右肩上がりになっていたことを確認
- Aurora調査結果:
- Auroraリードレプリカ: CPU余力あり、週比で良化傾向でレイテンシーも良化
- レイテンシー急増の原因とは考えにくい状態
- HAProxyのボトルネック発見:
- GoとAuroraの間に存在するHAProxyを調査するため、全メトリクスを洗い出し
- 洗い出した結果、CPUスロットリング(CPU使用が一時的に止められる現象)が原因と判明
- HAProxyに限らず、当時の監視の死角となっていた
- HAProxyのCPUスロットリングは数ヶ月前から徐々に悪化しており、HAProxy 2.0へのアップデートと時期が一致
- HAProxy 2.0以降、Multithreadingがデフォルトで有効化され、並列数はnbthreadという設定で決定される
- HAProxyの過剰並列:
- limits.cpu=1000m(1コア分)に対し、nbthread=48(ノードの48コアから取得)が設定されていた
- GOMAXPROCSと同様の過剰並列の問題
- nbthread=1に変更した結果: スロットリングが完全に解消し、CPU・メモリも半分以下に削減
■ 5. Act 3: CPU制限と過剰並列の衝突
- Linux cgroupsの仕組み:
- KubernetesではRequests(競合時の配分)とLimits(制限)でCPUリソースを指定する
- これらがLinuxのcgroupのパラメータに変換され、LinuxデフォルトスケジューラであるCFS(Completely Fair Scheduler)がCPU時間を管理する
- cgroup v1(v1.35から非推奨): requests.cpu → cpu.shares、limits.cpu → cpu.cfs_quota_us / cpu.cfs_period_us
- cgroup v2: requests.cpu → cpu.weight、limits.cpu → cpu.max
- Requestsの動作:
- requests.cpuはcpu.weightに変換される
- cpu.weightは競合時の相対的な配分比率を表す(例: 500m : 1500m = 1 : 3 → 25% : 75%)
- 競合時はCPU時間がその比率に応じて優先的に割り当てられる
- 非競合時はRequestsを超えて余剰CPUを利用可能であり、cpu.weightの比率は使用上限ではない
- Limitsの動作:
- limits.cpuはcpu.maxに変換され、ピリオド(期間)とクォータ(上限時間)で構成される
- ピリオドのデフォルト値は100ms
- クォータはLimitsの1コア分(1000m)あたり1ピリオド分(100ms)で計算される(例: 500m → 50ms)
- ピリオド内でクォータを使い切ると、次のリセットまでスロットリング(停止)が発生する
- クォータとスロットリングの関係:
- クォータはコンテナ内のスレッド間で共有される
- スレッドが多いほど共有クォータを早く消費しやすく、スロットリング時はコンテナ内の全スレッドが同時に停止する
- HAProxyの場合: limits.cpu=1000mで100msのクォータに対し、nbthread=48の48本のワーカースレッドが共有クォータを瞬時に消費し、全スレッドが同時にスロットリングするThundering Herdが発生
- CPU使用率では気づけなかった理由:
- 停止時間が長いほど、平均CPU使用率のような集約値は実態とズレやすい
- CPU使用率に加えて、スロットリングのピリオドと時間をセットで監視することが重要
- Limitsを外す場合の考慮事項:
- Limitsなしではスロットリングが発生せず余剰CPUも利用可能
- ただし、CPU使用を抑える上限とスロットリングのメトリクスを失う
- マルチテナント環境では、CPU上限自体がセーフティとして機能する
- GOMAXPROCSなどのcgroup-awareなデフォルト値はLimitsに依存するため、Limitsを外す場合はRequests・プロセスの並列数・異常検知をセットで考える必要がある
■ 6. まとめ: pairs-mainのcgroup-aware化の進捗と学び
- cgroup-aware化の進捗:
- GOMAXPROCSの最適化(リベンジ)に成功し、Pod数を数割カット
- Nginxのworker_processesにも対応し、メモリ使用量を220 MB→8 MBに削減
- 現在は全コンテナ(Go: GOMAXPROCS、Nginx: worker_processes、HAProxy: nbthread、Datadog Agent: GOMAXPROCS)がcgroup-aware化済み
- 学び:
- 各プロセスのデフォルト設定・挙動を把握し、過剰並列を避ける
- プロセスの並列数はRequests・Limits・ワークロード特性を踏まえて決める
- CPU使用率だけでなくスロットリングも監視する
- Limitsなしでは過剰並列と異常検知をセットで考える