CPU limit を超えると遅くなるだけなのに、メモリ limit だと Pod が落ちるのはなぜか?
昨日も Deployment の manifest に requests と limits の数字を書いたはずだ。だがその数字が実際に何を保証し、超えたときに何が起きるかを説明できるだろうか。CPU が limit を超えたときと、メモリが limit を超えたときでは、カーネルの振る舞いがまったく別物になる。
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 をアプリの実際のリソース使用量に対して余裕を持たせるだけで解決する。
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: OOMKilled と Exit 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 の優先順位を決める区分。
- Resource Management for Pods and Containers | Kubernetes — requests / limits の基本挙動と、CPU スロットリング・OOM Kill の違いの一次資料。
- Pod Quality of Service Classes | Kubernetes — QoS クラスの判定条件と eviction の優先順位。
- Assign Memory Resources to Containers and Pods | Kubernetes — OOMKilled を実際に再現するチュートリアルと exitCode 137 の実例。
- Kubelet Configuration (v1beta1) | Kubernetes — cpuCFSQuota / cpuCFSQuotaPeriod など CFS 関連設定のリファレンス。