Tech Learning Daily

2026-07-18 (Sat) — 第 2 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Container 🌿 基礎 ⏱ 約 7 分

CPU limit を超えると遅くなるだけなのに、メモリ limit だと Pod が落ちるのはなぜか?

昨日も Deployment の manifest に requestslimits の数字を書いたはずだ。だがその数字が実際に何を保証し、超えたときに何が起きるかを説明できるだろうか。CPU が limit を超えたときと、メモリが limit を超えたときでは、カーネルの振る舞いがまったく別物になる。

🎯 3 行まとめ
  • requests はスケジューラが使う「最低保証」の申告値、limits はカーネルが cgroup で強制する上限である
  • CPU が limits を超えると Linux の CFS がその Pod を周期ごとに間引く(スロットリング)。プロセスは生きたまま待たされるだけ
  • メモリが limits を超えるとカーネルの OOM Killer がプロセスを強制終了する(OOMKilled)。だから CPU は遅くなるだけ、メモリは落ちて作り直される

requests ―― スケジューラへの「保証」申告

requests は、Pod をどのノードに配置するかを kube-scheduler が判断するときに使う値だ。あるノードに Pod を置いてよいかは、そのノードの空きリソースが Pod の requests 合計を上回っているかで決まる。kubelet はこの量を「最低限このコンテナのために確保する」量として扱うが、実際にコンテナが使ってよい上限を決めるものではない。

なお limits だけを書いて requests を省略すると、Kubernetes はその limits の値をそのまま requests としてコピーする。「limits さえ書けばスケジューリングには影響しない」という理解は誤りで、結果的に requests = limits の Pod としてスケジュールされることになる。

CPU limit を超えると何が起きるか ―― CFS スロットリング

CPU の limits を超えるとどうなるか。答えは「殺されない。ただし細切れに間引かれる」だ。Linux カーネルは CFS(Completely Fair Scheduler)という仕組みで CPU 時間を一定の周期(cfs_period_us、既定で 100ms)に区切り、その周期内でコンテナの cgroup が使ってよい CPU 時間の上限(cfs_quota_us)を「limit × 周期」で計算する。たとえば limit を 500m(0.5 CPU)に設定すると、1 周期あたり 50ms しか実行が許されない。

cfs_period_us = 100000(既定 100ms)
CPU limit 500m → quota 50ms/period

┌── 1 period(100ms)──┐
│■■■■■■■■■■□□□□□□□□□□│
└──────────────────────┘
 使用 50ms      throttle 50ms

50ms を使い切ると、そのコンテナ内の全プロセスは次の周期が始まるまで実行キューから外され、待たされる。これがスロットリングであり、プロセス自体は生きたまま止まっているだけなので Pod の再起動やイベントは発生しない。だが体感としては「急に応答が固まる」形でレイテンシに直結する。

🍱 たとえるなら

CPU の limit は毎月のデータ通信量に近い。契約量を使い切ると通信速度が落ちるだけで、翌月にはリセットされてまた普通に使えるようになる。一方でメモリの limit は引っ越しトラックの積載量に近い。荷物が積みきれないとわかった時点で、誰かの荷物をトラックから引きずり降ろすしかない。前者は待てば回復する「速度制限」、後者は取り返しのつかない「排除」——この違いが、CPU とメモリで症状がまったく違う理由の正体だ。

メモリ limit を超えると何が起きるか ―― OOM Killer

メモリの limits を超えた場合はまったく別の仕組みが働く。コンテナが使うメモリは cgroup によって計測されており、その cgroup の使用量が limits を超えると、Linux カーネルの OOM Killer(Out Of Memory Killer)が介入し、cgroup 内のプロセスを SIGKILL で強制終了する。CPU と違って「待てば戻ってくる」性質のものではなく、プロセスはその場で殺される。

kubelet はこの終了を検知すると、コンテナステータスの reason を OOMKilled、exitCode を 137(SIGKILL によって終了したことを示す値)として記録し、Pod の restartPolicy に従ってコンテナを再起動する。再起動を繰り返す場合、kubectl get pods の RESTARTS が増え続けているのに気づくはずだ。CPU が「絞られる」のに対し、メモリは「殺されて作り直される」——これが冒頭の問いへの答えである。

QoS クラス ―― ノード全体が逼迫したときの優先順位

requests と limits の関係は、ノード全体のメモリが逼迫したときの挙動にも影響する。requests と limits が全コンテナで一致している Pod は Guaranteed、どちらか一部でも設定されていれば Burstable、まったく設定がなければ BestEffort という QoS クラスに分類される。ノードのメモリ不足で kubelet が Pod を退避(eviction)させる必要が出たとき、真っ先に狙われるのは BestEffort、続いて requests を超過している Burstable で、Guaranteed は最後まで残される。

つまり OOMKilled は「その Pod 自身の cgroup が自分の limits を超えた」場合に起きる話であり、eviction は「ノード全体が足りない」場合に kubelet が能動的に間引く話だ。両者は別の仕組みだが、どちらも requests / limits の設定次第で自分の Pod がどれだけ優先的に守られるかが決まる、という点は共通している。

💼 実務でどう出会うか

p99 レイテンシだけが時々跳ねるのに、kubectl top pod で見る平均 CPU 使用率は limit よりずっと低い——このズレは CFS スロットリングが原因であることが多い。container_cpu_cfs_throttled_periods_total のようなメトリクスで検知できる。一方、Pod の RESTARTS が増え続けている場合は kubectl describe pod の Last State に OOMKilled と出ていないか確認する。多くの場合、limits をアプリの実際のリソース使用量に対して余裕を持たせるだけで解決する。

⌨️ 手を動かす(5 分)

OOM Killer が実際にコンテナを落とす様子を、ローカルの kind クラスタで確認する。

kind create cluster --name restest
kubectl run oom-test --image=polinux/stress --restart=Never \
  --requests=memory=50Mi --limits=memory=100Mi \
  -- stress --vm 1 --vm-bytes 250M --vm-hang 1
sleep 8
kubectl get pod oom-test
kubectl describe pod oom-test | grep -A5 "Last State"

kubectl get pod の STATUS が OOMKilled になっており、describe の Last State には Reason: OOMKilledExit Code: 137 が出ているはずだ。limits の 100Mi に対して 250M を確保しようとしたため、cgroup の上限を超えてカーネルに強制終了された。

🙅 よくある誤解
  • CPU limit を超えるとその Pod も OOMKilled のように落とされる — CPU 超過はスロットリングのみで、プロセスは生きたまま次の周期を待つ。落ちるのはメモリ limit を超えた場合だけだ。
  • limits さえ設定しておけば requests は書かなくていい — requests を省略すると limits の値がそのまま requests にコピーされ、意図せずスケジューリングの前提が変わってしまう。
  • OOMKilled は必ずアプリのメモリリークが原因 — リークがなくても、瞬間的な負荷でメモリ使用量が limits を一時的に超えれば OOMKilled は起こり得る。limits に余裕を持たせる設計不足も同じ結果を生む。
📖 用語ミニ辞典
requests
スケジューラが Pod 配置の判断に使う、コンテナに保証する最低限のリソース量。
limits
コンテナが使ってよいリソースの上限。カーネルの cgroup 機構によって強制される。
CFS スロットリング
CPU limits を超えたコンテナを、一定周期の中で一定時間だけ実行キューから外して制限する仕組み。
OOM Killer
cgroup のメモリ使用量が limits を超えたときに、カーネルがプロセスを強制終了する仕組み。
QoS クラス
requests / limits の設定状況から Pod を Guaranteed / Burstable / BestEffort に分類し、eviction の優先順位を決める区分。
🔗 もっと深く