Dev Study
← 宣言的に動かす — Kubernetes と Terraform に共通する仕組み
レッスン 10 / 12·約 8 分·第26期 宣言的に動かす

予約と上限は、別の者が別の時点で使う数 — requests と limits

アプリと DB の下に何が積まれているか(左が下、右が上)
  1. 物理機リージョン/AZ
  2. ハイパーバイザVM exit で捕まえる
  3. VMゲストOS
  4. コンテナnamespace / cgroup今ここ
  5. プロセスアプリ / DB

宣言は望む状態の記録にすぎず、誰かが世界と比べたときにだけ効きます。今回は resources の2つの数を、それぞれ誰がいつ比べるのかを見ます。

まず結論

マニフェストの resources には requests と limits が並んで書かれますが、読む者も読む時点も違います。requests はスケジューラが Pod を置くときに合計で足し合わせる申告で、使用量ではありません。limits はカーネルが、動いている間に cgroup を通して強制します。

だから CPU をほとんど使わないコンテナでも、申告の合計が入りきらなければ Pending のまま置かれません。宣言が同じ1ファイルでも、それを世界と比べる者は1人ではありません。

? 読む前に予測

割り当て可能な CPU が 10 のノード1台に、`requests.cpu: "4"` の Pod を3つ持つ Deployment を apply します。中身は sleep するだけで、CPU はほとんど使いません。どうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

申告を足すのはスケジューラで、置くときだけ

resources の2つの数は、同じ行に並んでいるので同じ種類の数に見えます。公式文書の冒頭は、読む者を分けて書いています。

When you specify the resource request for containers in a Pod, the kube-scheduler uses this information to decide which node to place the Pod on. When you specify a resource limit for a container, the kubelet enforces those limits so that the running container is not allowed to use more of that resource than the limit you set.

requests を使うのは kube-scheduler で、使う場面は「どのノードに置くか」を決めるとき。スケジューラが比べるのは使用量ではなく申告の合計です。同じ文書はこの直後に、kubelet も request の分をそのコンテナのために確保する(The kubelet also reserves at least the request amount)と続けています。置いた後も requests は無関係になるわけではありません。

The scheduler ensures that, for each resource type, the sum of the resource requests of the scheduled containers is less than the capacity of the node.

しかも、ノードが暇でも断ると明記されています。

Note that although actual memory or CPU resource usage on nodes is very low, the scheduler still refuses to place a Pod on a node if the capacity check fails.

理由は、後で使用量が増えたとき(文書の例では日次のピーク)に足りなくならないように、です。requests は予約で、予約は使っていなくても席を取ります。 逆に、置かれた後は it's possible (and allowed) for a container to use more resource than its request とも書かれていて、requests は上限でもありません。

一方 limits は、別の者が別の時点で使います。

Limits are a different story. Both cpu and memory limits are applied by the kubelet (and container runtime), and are ultimately enforced by the kernel.

1つのマニフェストの2つの数を、別の者が別の時点で読む
マニフェストrequests と limits が同じ行に並ぶ
置くとき(1回)
kube-schedulerrequests の合計を容量と比べる
置いた後、kubelet とランタイムが cgroup に書く
カーネル動いている間ずっと limits を強制
  • ・スケジューラは使用量を見ない。カーネルは requests の合計を見ない
  • ・入らなければ Pending のまま、カーネルの出番は来ない
具体実物

使っていないのに、置けない

実演は k3s rancher/k3s:v1.37.0-k3s1 の単一ノード(kernel 6.10.14-linuxkit arm64、cgroup v2)です。ノードの allocatable は cpu 10。ここに requests.cpu: "4"、replicas 3、中身は sleep 3600 だけの Deployment big を apply しました。

kubectl apply の応答(big の行のみ抜粋)と、その後の kubectl get pods -l app=big -o wide、Pending の Pod のイベント(k3s v1.37.0、1回目の実行。Pod 名は実行ごとに変わる)コマンド出力
deployment.apps/big created
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
big-7bbcd886b4-7b9xn 0/1 Pending 0 27s <none> <none> <none> <none>
big-7bbcd886b4-hwtwt 1/1 Running 0 27s 10.42.0.28 a8a2e58ff591 <none> <none>
big-7bbcd886b4-xlhfc 1/1 Running 0 27s 10.42.0.27 a8a2e58ff591 <none> <none>
--- pod/big-7bbcd886b4-7b9xn
LAST REASON MSG
<nil> FailedScheduling 0/1 nodes are available: 1 Insufficient cpu. preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.

apply は created で返っています。記録はされたが、3つ目はどのノードにも置かれていません(NODE が <none>)。理由は Insufficient cpu。同じ時点のノードの帳簿がこれです。

kubectl describe node の Allocated resources(実演前と実演中。見出し行と cpu 行だけを抜粋し、# の行は区切りとして付けたもの)コマンド出力
# 実演前
Resource Requests Limits
cpu 100m (1%) 0 (0%)
# 実演中
Resource Requests Limits
cpu 8200m (82%) 100m (1%)

8200m は 4000m×2 と、実演前から載っていた 100m と、同時に動かした cpu-demo(後で使う、limits.cpu 100m だけを書いた Pod)の 100m の和です。cpu-demo は requests を書いていませんが、limit だけを書き、受け付け時に既定の request を入れる仕組みも無いときは、Kubernetes が limit を request に写すと公式文書にあります。3つ目の 4000m を足すと 12200m で 10000m を超えるので、容量の確認に落ちます。

metrics-server を外した環境なので kubectl top node は error: Metrics API not available で、使用量の数値は取れていません。ただ Running の2つは sleep するだけのコンテナです。82% は予約の割合で、CPU が 82% 忙しいという意味ではありません。

✓ ここまでの確認

実演の3つ目の Pod に出た `0/1 nodes are available: 1 Insufficient cpu.` は、何が足りなかったことを示していますか。

抽象原理

上限を守らせるのはカーネルで、動いている間ずっと

limits の側は、スケジューラが Pod を置いた後に効き始めます。kubelet とランタイムが cgroup に数を書き、あとはカーネルの仕事です。文書は CPU とメモリで書き分けています。

cpu limits are enforced by CPU throttling. When a container approaches its cpu limit, the kernel will restrict access to the CPU corresponding to the container's limit.

memory limits are enforced by the kernel with out of memory (OOM) kills. When a container uses more than its memory limit, the kernel may terminate it. However, terminations only happen when the kernel detects memory pressure. Thus, a container that over allocates memory may not be immediately killed.

メモリ側の may not be immediately killed を落とすと「超えた瞬間に殺される」と言い過ぎになります。cgroup のファイルの読み方と、絞られることと殺されることの違いは第8期『cgroup で資源を配る』で扱いました。ここで足すのは「Kubernetes の宣言が、その cgroup の数に翻訳される」ことです。

もう1人、比べる者がいます。ノードが逼迫したときに Pod を追い出す kubelet です。ここで公式文書の2ページは字面が食い違います。

具体実物

100m は cgroup の数になり、32Mi は再起動の繰り返しになる

同じ実行で、limits.cpu: 100m の cpu-demo(シェルの無限ループ)と、limits.memory: 32Mi の mem-demo(約1億バイトをシェル変数に読み込む)を動かしました。

cpu-demo のコンテナ内の cpu.max と cpu.stat(cpu.stat は nr_periods と nr_throttled の行だけ抜粋、2回目は5秒後)、mem-demo の kubectl get pod(1回目の実行)コマンド出力
=== cpu-demo: cgroup files as seen inside the container
10000 100000
nr_periods 201
nr_throttled 193
nr_periods 255
nr_throttled 246
=== mem-demo
NAME READY STATUS RESTARTS AGE
mem-demo 0/1 OOMKilled 2 (24s ago) 26s

limits.cpu: 100m が cpu.max の 10000 100000 になっています。周期 100000 のうち 10000、つまり 0.1 CPU です。nr_throttled は nr_periods に迫り、ほぼ毎周期で絞られています(この数は実行ごとに変わり、別の回では 197 と 188 でした)。

mem-demo は OOMKilled で、RESTARTS 2。記録では 2 回とも exitCode 137、reason OOMKilled でした。殺したのはカーネルで、起こし直したのは kubelet です。文書は Unlike pod eviction, if a container is OOM killed, the kubelet can restart it based on its restartPolicy. と書き分けています。Pod は同じノードにいて、スケジューラは一度も呼ばれていません。

✓ ここまでの確認

`limits.memory: 32Mi` の mem-demo は `OOMKilled` と表示され、RESTARTS が 2 でした。殺した者と起こし直した者の組み合わせとして正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

requests.cpu: 4 の Pod が3つ

スケジューラが置くときに、ノードの requests の合計と容量を比べます。中身は sleep だけでも、3つ目は Insufficient cpu で Pending のまま。カーネルの出番は来ません。

誰が、いつ、何と比べたか表
比べた者 kube-scheduler
時点 置くとき
比べた数 requests の合計 12200m と容量 10000m
結果 3つ目は Pending、コンテナは作られない
具体例 B

limits.cpu: 100m と limits.memory: 32Mi

Pod は置かれた後、kubelet とランタイムが cgroup に数を書き、カーネルが動いている間に比べます。CPU は周期ごとに絞られ、メモリは OOM kill と kubelet による再起動を繰り返しました。

誰が、いつ、何と比べたか表
比べた者 カーネル(cgroup)
時点 動いている間ずっと
比べた数 cpu.max 10000 100000 / メモリの上限
結果 CPU は絞られ、mem-demo は OOMKilled と再起動

片方は置けずに止まり、片方は動いてから絞られたり殺されたりしました。同じ resources の数なのに、何が違ったのでしょうか。

それぞれの数を「誰が」「いつ」読んだかを、2例で並べてみてください。

この章のまとめ

  • requests は kube-scheduler が置くときに、ノードごとの合計として 容量と比べる申告。使用量ではない。使用量が低くても、確認に落ちれば置かない。
  • 実演: allocatable cpu 10 のノードで requests.cpu: "4" を3つ。sleep だけでも3つ目は Insufficient cpu で Pending。帳簿は 8200m (82%) で、これは予約の割合。apply は created で返っていた。
  • limits は kubelet とランタイムが cgroup に書き、カーネルが動いている間に強制する。limits.cpu: 100m は cpu.max の 10000 100000 になった。
  • メモリの limit 超過は may not be immediately killed。OOM kill したのはカーネル、再起動したのは kubelet。
  • 追い出しの順は、eviction のページ(QoS を使わない、鍵は requests 超過・Priority・超過量)と Pod QoS のページ(BestEffort から順に)の両方が現行。requests はここでも読まれる。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

`limits.cpu: 100m` だけを書いた Pod で、コンテナ内の `/sys/fs/cgroup/cpu.max` を読むと何が見えましたか。

用語集(4語)
requests
コンテナが必要とする資源量の申告。kube-scheduler が配置先を決めるときに使い、ノードごとの合計を容量未満に保つ。使用量の上限ではない。
limits
コンテナが使える資源量の上限。kubelet とランタイムが cgroup に書き、最終的にカーネルが強制する。CPU は絞られ、メモリは OOM kill になりうる。
Pending
Pod は受け付けられたが、コンテナがまだ動く準備を終えていない段階。置き先を待つ時間とイメージの取得時間を含む。requests の合計が入らない Pod はここに留まる。
eviction(追い出し)
ノードが逼迫したとき kubelet が Pod を終わらせること。カーネルの OOM kill とは別の者による別の仕組み。