「動いている」が「最適ではない」という恐怖 #
多くの SRE やバックエンドエンジニアにとって、Kubernetes へのデプロイは日常的なルーチンだ。resources.limits.cpu を設定し、Pod が正常に起動してヘルスチェックが通れば、概ね「成功」と見なされる。
しかし、ここには罠がある。Go のランタイムは、ホストマシンの物理リソースを基準に設計されており、コンテナによる「擬似的な制限(CPU クォータ)」を完全には理解していない。この認識の乖離が、サイレントにパフォーマンスを蝕む「サイレント・クライシス」を引き起こす。
動画で見る: この記事の内容を YouTube で解説しています。
CPU クォータという名の錯覚 #
Kubernetes の CPU リミットは、実際には Linux カーネルの CFS (Completely Fair Scheduler) クォータによって制御されている。例えば cpu: 500m と設定した場合、それは「1 秒間のうち 0.5 秒間だけ CPU を使用して良い」という時間制限を意味する。
ここで問題となるのが Go の GOMAXPROCS だ。デフォルトでは、Go はホストマシンの論理コア数をすべてカウントし、それに基づいた並列度でスケジュールを行う。
- ホスト: 64 コア
- Pod リミット: 0.5 コア (500m)
- GOMAXPROCS: 64 (デフォルト)
この状態で Go ランタイムは 64 個の OS スレッドを効率的に回そうと試みるが、カーネルによって 0.5 秒で強制的にスロットリング( throttling)される。結果として、コンテキストスイッチが激増し、テールレイテンシ(P99)が跳ね上がる。
メモリの誤解と OOMKill の正体 #
CPU だけでなく、メモリ管理にも深い溝がある。Go の GC (Garbage Collector) は、デフォルトでは利用可能なメモリ全体を基準に動作する。しかし、コンテナ環境では cgroups によるハードリミットが存在する。
多くの開発者が、「メモリ使用率を 80% 程度に抑えれば安全だ」という曖昧な経験則に頼っている。だが、Go のメモリ管理は単純な「使用量」だけでは決まらない。GC のトリガータイミングと、一時的に急増するアロケーション、そしてコンテナのメモリリミットが衝突した瞬間、カーネルは容赦なく OOMKill を執行する。
検出のギャップ:なぜ気づかないのか? #
最悪なのは、平均的なメトリクス(Average CPU/Memory)ではこの問題が表面化しにくいことだ。
- CPU Throttling: Prometheus の CPU 使用率グラフは「低く」見えるが、実際にはクォータに達して停止している時間がある。
- GC ポーズ: 平均レスポンスタイムは正常だが、特定のタイミングで発生する GC ストールが、分散システムにおけるカスケード失敗のトリガーとなる。
次回、解決への道へ #
この「サイレント・クライシス」を脱するには、Go ランタイムをコンテナの現実に適応させる必要がある。
次回は、この問題の核心である GOMAXPROCS を解剖する。特に Go 1.25 で導入された新しい丸めロジックが、どのようにマルチコアホストにおける GC ストールを防ぐのか。そして、なぜ単なる「整数への切り捨て」では不十分なのかを深掘りする。
「コンテナの中の Go」シリーズ: イントロ — サイレント・クライシス · 1. GOMAXPROCS の謎を解く · 2. 圧力下でのガベージコレクション · 3. GC ポーズとレイテンシ · 4. コンテナ・デバッグの実践ワークフロー