resources.requests / limits
コンテナが CPU とメモリをどう要求し、どう上限を与えられるかのモデルを扱います。requests と limits の分離、QoS クラス (Guaranteed · Burstable · BestEffort)、CPU throttling とメモリ OOMKilled の動作の違い、JVM · Go ランタイムの cgroup 認識、LimitRange · ResourceQuota のネームスペースポリシー、そして最初の値を決めて調整する運用サイクルまでを一連の流れで整理します。
第10章 Ingress と Ingress Controller まで視点はクラスタの外にありました — Service、Ingress、Ingress Controller で外部トラフィックがどうクラスタの中のワークロードまで到達するかのモデルでした。本章の視点は再び Pod の中へ戻ります。入ってきたトラフィックを受けて働くコンテナが CPU とメモリをどう要求し、どう上限を与えられるか のモデル、つまり resources.requests と resources.limits の話です。この2つのフィールドの分離が K8s のスケジューリングと安定性を同時に支える土台です。
本章の終わりには マニフェストの resources ブロックに出会ったとき、そのコンテナがどの QoS で、ノードリソースの圧迫時にどう振る舞うか を一行で読み取れる状態になります。
requests と limits — 2つの値の役割が異なる #
K8s マニフェストでリソースモデルを表現するフィールドは2つです。コンテナ1個の resources.requests と resources.limits です。2つが似て見えますが、見る主体と見る時点が完全に異なります。
| フィールド | 見る主体 | 見る時点 | 意味 |
|---|---|---|---|
resources.requests | スケジューラ (kube-scheduler) | Pod をどのノードに置くか決めるとき | このコンテナが立ち上がっているために保証されるべき最小リソース |
resources.limits | kubelet (cgroup) | コンテナが実際に回っているとき | このコンテナが絶対に超えられない上限 |
スケジューラは新しい Pod をどこに置くか決めるとき、候補ノードたちの allocatable リソース (ノード全体のリソースからシステムデーモン · kubelet の分を引いた値) から、すでに立ち上がっている Pod たちの requests の合計を差し引きます。新しい Pod の requests がその残量の中に収まるノードだけが候補になります。limits はこの決定に入りません。 1つのノードの limits の合計が allocatable を超えても K8s は Pod をその上に立ち上げます — これを オーバーコミット (overcommit) と呼び、ノードのリソースを統計的に効率よく使うための基本動作です。
limits はその次の層で働きます。Pod がノードに割り当てられてコンテナが立ち上がると、kubelet がそのコンテナの cgroup に limits の値を設定します。コンテナがその限度を超えようとするとリナックスカーネルが強制的に止めます。このときリソースの種類によって動作が分かれます — CPU は throttling (演算をしばらく受けられなくする)、メモリは OOMKilled (コンテナ強制終了) です。この違いは後で別に扱います。
頭の中で一行に縮めると — requests は「保証されるべき量」なのでスケジューリングが見て、limits は「絶対に超えてはいけない量」なのでランタイムが強制します。 この2つの値を別々に使えるという点が K8s リソースモデルの核心です。
CPU とメモリの単位 #
マニフェストでよく混同する部分が単位の表記です。CPU とメモリがそれぞれ別の表記法を使います。
CPU — コアとミリコア #
CPU は コア単位 です。1 は1コア、2 は2コアを意味します。1コアより小さく刻むには ミリコア (millicore) 表記を使います。
| 表記 | 意味 |
|---|---|
1 | 1コア (1000 millicore) |
500m | 0.5コア |
250m | 0.25コア |
100m | 0.1コア |
0.5 | 500m と同じ |
運用マニフェストでは 100m、250m のように ミリコア整数形 で書く場合が多いです。0.1 のような小数表記は YAML パースの段階で混同する余地があるので避けるパターンです。CPU 単位はコンテナ cgroup の CPU quota にマッピングされます — 100m なら毎100msサイクルあたり10msの CPU 時間を受ける、という具合です。
メモリ — バイナリ vs 十進数 #
メモリは単位の接尾辞が2系統あるので運用事故の常連の原因になります。
| 表記 | 値 | 備考 |
|---|---|---|
1Ki | 1024 バイト | バイナリ |
1Mi | 1024 KiB = 1,048,576 バイト | バイナリ |
1Gi | 1024 MiB = 1,073,741,824 バイト | バイナリ |
1K | 1000 バイト | 十進数 |
1M | 1,000,000 バイト | 十進数 |
1G | 1,000,000,000 バイト | 十進数 |
1Gi と 1G は約7%の差があります (1GiB の方が大きい)。運用マニフェストの標準は バイナリ接尾辞 (Mi、Gi) です。コンテナランタイムと OS がメモリを扱う単位がバイナリで、kubectl top のようなツールが表示する値もバイナリだからです。1G と書いたのに使用量の表示が 0.93Gi で出る事故は単位の不一致から来ます。
マニフェスト1枚 #
上の2つの単位をそのままマニフェストに適用してみます。Deployment の Pod テンプレートの中のコンテナ定義に resources キーを入れる形が標準です。
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: app
image: myapp/web:1.4.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"このコンテナは 0.25コアと 512 MiB メモリが保証されて初めて 立ち上がっていられます。スケジューラは新しい Pod を受けるとき、その分の余裕があるノードにだけ置きます。立ち上がっている間は 最大1コアと 1 GiB メモリまで 使えて、その限度を超えようとする試みは cgroup が強制的に止めます。
resources フィールドはコンテナ単位です — 1つの Pod の中に複数のコンテナがあるならコンテナごとに別々に書きます。Pod 全体の requests / limits はコンテナたちの合計で計算されます。サイドカーコンテナ (例: ログ収集器) があるなら、そのコンテナにも小さな requests / limits を忘れずに書かなければなりません。第8章 の DaemonSet で立ち上げるノードエージェント · ログ収集器も同様にリソース表記が必要です。
kubectl top pod
kubectl top pod -n <namespace> --containersNAME CPU(cores) MEMORY(bytes)
web-7d4f8b9c5-abc12 180m 420Mi
web-7d4f8b9c5-def34 220m 480Mikubectl top は metrics-server がクラスタにインストールされていて初めて動作します。表示される値はコンテナ cgroup の実際の使用量なので単位はバイナリです。
requests / limits の組み合わせ4つ #
マニフェストに2つをどう書くかによって動作が大きく分かれます。4つの組み合わせを一表に整理します。
| 組み合わせ | 動作 | QoS | 運用適合性 |
|---|---|---|---|
| 両方書く | 最も安全。スケジューリング保証 + ランタイム上限がどちらも明確 | requests = limits なら Guaranteed、それ以外 Burstable | 推奨 |
requests だけ書く | スケジューリング保証はあるがランタイム上限なし。コンテナがノード全体のリソースを潜在的に占有 | Burstable | 事情上 limits を抜く場合のみ |
limits だけ書く | K8s が requests = limits とみなす。結果的に最も保守的な形 | Guaranteed | 無難だが明示して書くことを推奨 |
| 両方書かない | スケジューリング時に差し引きなし。ランタイム上限なし | BestEffort | 非推奨 |
最もよくぶつかる落とし穴が 両方書かない場合 です。このコンテナは BestEffort QoS になり、ノードがリソースの圧迫を受けたとき最初に eviction (退避) の対象になります。スケジューラもこの Pod のリソースを0とみなしてノードを選ぶので、1つのノードに BestEffort Pod がたくさん集まる形も生まれます。運用マニフェストでは小さくても requests / limits を常に書く方が安全です。
requests だけ書いて limits を抜くパターンは意図が明確な場合だけ使います — CPU limits の throttling 動作が応答遅延を大きくするので、意図的に CPU だけ limits を抜く運用者がいます (後で詳しく)。しかしメモリは limits を抜くと誤ったコードがノードのメモリを使い尽くせるので、ほぼ常に書いておきます。
QoS クラス — Guaranteed / Burstable / BestEffort #
K8s は Pod の requests / limits の形を見て 3等級の QoS クラス に分類します。この分類がノードリソースの圧迫時に誰が先に追い出されるかを決めます。
| QoS | 条件 | eviction 優先順位 |
|---|---|---|
Guaranteed | すべてのコンテナのすべてのリソースについて requests == limits で、両方明示 | 最後 (最も安全) |
Burstable | requests だけがあるか、requests / limits が異なるか、一部のコンテナにだけ書かれた場合 | 中間 |
BestEffort | すべてのコンテナで requests / limits がどちらもない | 1番目 (最も危険) |
eviction はノードのメモリ · ディスクの圧迫のような信号がしきい値を超えたとき kubelet が Pod を強制終了してリソースを回収する動作です。BestEffort → Burstable → Guaranteed の順で候補になります。同じ等級の中ではリソースをより多く使う Pod が先に候補になります。
kubectl get pod web-7d4f8b9c5-abc12 -o jsonpath='{.status.qosClass}'Burstable運用の標準パターンは次のとおりです。
- DB · メッセージキューのような stateful な核心ワークロード — Guaranteed にします。requests = limits で書いて eviction の可能性を最小化します。
- 一般的な stateless な Web / API サーバ — Burstable。普段使う量を requests に、バースト可能上限を limits にします。
- バッチ / 一時的なワークロード — Burstable または BestEffort。クラスタリソースが足りないとき先に譲ってもよいワークロードです。
BestEffort を運用で使うことはほとんどありませんが、短期デバッグ用に立ち上げた一時的な Pod 程度がその位置にあります。
CPU limit の落とし穴 — throttling #
ここからが運用事故の常連の部分です。CPU とメモリが limits を超えたときの動作が完全に異なります。
CPU limit は throttling で強制されます。コンテナ cgroup の CPU quota が毎サイクル (普通100ms) ごとに limits の分だけ割り当てられ、コンテナがその量を使い切ると次のサイクルが来るまで 演算を受けられません。 コンテナが死ぬわけではありません — ただしばらく止まってから次のサイクルで再び目覚めます。
例えば cpu: limits: 100m のコンテナがあると仮定します。このコンテナは毎100msサイクルで10msの CPU 時間だけ受けられます。ところがリクエスト1件が50msの CPU を必要とするなら — そのリクエストは最初の10msを使って90ms待ち、再び10msを使って90ms待つ、という具合に処理されます。本来50msで終わったはずの作業が約410msかかります。
この動作が運用で最もよく出会う事故が 応答遅延の急増 です。平均 CPU 使用率は limits よりずっと低いのに、p99 応答時間が突然跳ねるパターンがそれです。短期バーストが limits に達した瞬間 throttling がかかったのです。kubectl describe node や cAdvisor メトリクス (container_cpu_cfs_throttled_seconds_total) で throttling の累積時間を確認できます。診断の完成された流れは 第27章 kubectl デバッグパターン で整理します。
この負担のため CPU limits を意図的に抜く運用パターン も存在します。requests で保証量だけ取っておいて、ノードに余裕があるときはその上へ自由にバーストさせる、という具合です。このパターンは次の2つの条件が支えるとき使われます。
- ノードのリソースが十分に余裕があり、ワークロード同士が互いに適度な水準でバーストしてもノードが揺れません。
- requests が合理的に取られていて、1つのワークロードの暴走が別のワークロードの保証量を侵しません。
逆にメモリは limits を抜くパターンがほとんどありません — メモリの暴走はノード全体を危うくします。続けてその動作を見ます。
メモリ limit の落とし穴 — OOMKilled #
メモリ limit は throttling ではなく hard cap です。コンテナが limit 以上のメモリを割り当てようとするとリナックスカーネルの OOM Killer がそのコンテナのプロセスを即座に強制終了します。K8s はこの終了を検知してコンテナの終了理由を OOMKilled で記録します。
kubectl describe pod web-7d4f8b9c5-abc12Containers:
app:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Mon, 18 May 2026 14:22:10 +0900
Finished: Mon, 18 May 2026 14:35:42 +0900
Restart Count: 3Reason: OOMKilled と Exit Code: 137 (SIGKILL) が対で見える形が典型的です。Restart Count が速く上がりながら同じ理由が繰り返されるなら、メモリ limit がワークロードの実際の使用量より小さく取られているという信号です。OOMKilled の診断木の完成版は 第27章 kubectl デバッグパターン で整理します。
メモリ limits を抜くとどうなるでしょうか。コンテナ cgroup にメモリ限度がない状態になり、誤ったコード (メモリリーク、大きなファイルを丸ごとメモリに載せるなど) がノードの全体メモリを使い尽くせます。すると ノード次元のメモリ圧迫 が発生し、そのノード上の別の Pod たちが BestEffort → Burstable → Guaranteed の順で eviction の対象になります。1つのワークロードの事故が同じノードの別のワークロードまで揺らす形です。メモリ limits を常に書く理由がここにあります。
要約すると — CPU limits 超過は throttling (即座の終了ではない)、メモリ limits 超過は OOMKilled (即座の強制終了) です。
JVM と Go ランタイムの cgroup 認識 #
リソース limits を書くだけでは十分でないランタイムがあります。JVM と Go がその代表事例です。
JVM #
古い JVM はホストの /proc/cpuinfo と /proc/meminfo をそのまま読んでワーカースレッド数、ヒープサイズ、GC スレッド数などを決めていました。コンテナの cgroup limits は見られませんでした — cpu: limits: 500m のコンテナの中の JVM がホストの32コアを見て GC スレッドを32個作り throttling にかかる事故がよくありました。
Java 8u131+ / Java 10+ から -XX:+UseContainerSupport が導入され、Java 10+ からはこのオプションが基本有効です。このオプションがオンなら JVM が cgroup の CPU · メモリ limits を認識してスレッド数とヒープサイズを決めます。運用環境のコンテナイメージが古い JDK なら、このオプションを明示的にオンにするのが安全です。
Go #
Go ランタイムの GOMAXPROCS (並列実行可能な OS スレッド数) は基本値として runtime.NumCPU() に従います。ところがこの値は ホストのコア数 を返します — Go ランタイムは cgroup CPU limits を自動では認識しません。cpu: limits: 500m のコンテナの Go プロセスが32コアのホスト上で GOMAXPROCS=32 で立ち上がり throttling にかかるパターンが出ます。
解決は2つが標準です。
automaxprocsライブラリ —go.uber.org/automaxprocsパッケージを import するとプロセス開始時に cgroup CPU limits を読んでGOMAXPROCSを自動で合わせてくれます。運用標準に近いパターンです。- 環境変数の手動指定 — Pod マニフェストの
envにGOMAXPROCSを直接設定します。
env:
- name: GOMAXPROCS
value: "1"他の言語のランタイムも似た落とし穴があり得ます。Node.js の libuv スレッドプールのサイズ、Python の multiprocessing.cpu_count() のような部分がホスト基準で取られるか cgroup 基準で取られるかを一度ずつ確認しておく方が安全です。
メモリ使用量 vs メモリ limits の測定の微妙さ #
メモリ使用量をどう測定するかによって OOMKilled の時点に対する直観が違って来ます。cgroup が見るメモリは RSS (Resident Set Size) + ページキャッシュ のような値で、コンテナが扱うファイル I/O がページキャッシュを満たすとそれも limits に含まれます。kubectl top が表示する値は普通 working set (RSS に似ているが回収可能なキャッシュの一部を除外) なので、OOM 直前までの使用量をそのまま見せてくれるとは限りません。
運用で OOMKilled が繰り返されるなら、次の順序で見ていくのが安全です。
kubectl describe podのLast Stateで OOMKilled の事実と回数を確認。kubectl top pod --containersで普段の使用量を確認。- cAdvisor メトリクスまたは Prometheus の
container_memory_working_set_bytes、container_memory_rssで時系列を確認。 - アプリケーション次元のメモリリークの可能性と limits の引き上げ調整の両方を一緒に検討。
3段階の時系列モデルは 第19章 可観測性 で本格的に扱います。
LimitRange — ネームスペース単位の基本値 #
マニフェストごとに requests / limits を一つひとつ書くのは人が忘れやすいです。K8s はこの忘却を防ぐオブジェクトとして LimitRange を提供します。第7章 Namespace とラベル で押さえたネームスペース単位ポリシーの一軸として、基本値と許容範囲をかけておくオブジェクトです。
apiVersion: v1
kind: LimitRange
metadata:
name: default-resource-limits
namespace: dev
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"各フィールドの意味は次のとおりです。
| フィールド | 意味 |
|---|---|
default | コンテナに limits がなければ自動で与えられる基本値 |
defaultRequest | コンテナに requests がなければ自動で与えられる基本値 |
max | コンテナ1個のリソース上限。この値を超えるマニフェストは拒否される |
min | コンテナ1個のリソース下限。この値より小さいマニフェストは拒否される |
この LimitRange が dev ネームスペースに適用された状態で、誰かが requests / limits を抜かしたマニフェストを apply すると、K8s が自動で default / defaultRequest の値を埋めます。BestEffort QoS Pod を誤って作る事故が遮断されます。逆に1つのコンテナが max 以上を要求するとマニフェストの適用自体が拒否され、1人のミスでノード全体のリソースを占有する事故も防げます。
運用パターンは普通こうです。
- dev ネームスペース — 小さな default と小さな max にしておきます。開発者が気軽に立ち上げられるようにします。
- stage · prod ネームスペース — ワークロードの特性に合わせて default をたっぷり取りつつ、max は1つのコンテナがノード全体を占有できないように制限します。
ResourceQuota — ネームスペース単位の合計上限 #
LimitRange が コンテナ1個単位 のポリシーだとすれば、ResourceQuota は ネームスペース全体の合計 ポリシーです。1つのネームスペースの中のすべての Pod の requests / limits の合計がこの値を超えないように止めるオブジェクトです。
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"この ResourceQuota が適用された dev ネームスペースでは、すべての Pod の requests.cpu の合計が10コアを超えられず、Pod の個数が50個を超えられません。新しい Pod のマニフェストがこの限度を破ると apply が拒否されます。
ResourceQuota は LimitRange と対でかけるパターンが多いです — LimitRange がマニフェストの欠落した requests / limits を埋めてくれないと ResourceQuota が合計を計算できないからです。requests が空いている BestEffort Pod は ResourceQuota が0とみなしますが、運用では LimitRange でその0をあらかじめ防いでおく形が安全です。
ResourceQuota の本格的な活用は 第14章 RBAC / NetworkPolicy / ResourceQuota で扱います — セキュリティとリソースポリシーの一軸としてまとめて見る方が自然です。
運用パターン — どう始めてどう調整するか #
新しいワークロードの requests / limits を最初に決めるとき、正確な値を知る術はありません。運用で定着したパターンは次のような順序に従います。
- 保守的な requests + 余裕のある limits で始める — 最初の段階は推定です。似たワークロードの過去データを参考にするか、ローカルでの負荷テストでおおよその値を取ります。requests は普段の使用量の70 ~ 80%、limits はその2、3倍程度がよくある出発点です。
- 運用データの収集 — Prometheus + metrics-server、あるいは Datadog / New Relic のような APM が公開するコンテナごとの CPU · メモリの時系列を数日集めます。トラフィックが最も多い時間帯の p95 / p99 使用量を見ます。
- VPA recommender の活用 — Vertical Pod Autoscaler を
updateMode: Offで立ち上げておくと、実際のリソース変更なしで推奨値だけを受け取れます。K8s がワークロードの特性を学習して適切な requests / limits を提案してくれます。VPA の動作は 第13章 オートスケーリング で深く扱います。 - 調整と再配備 — 推奨値とモニタリングデータを合わせてマニフェストの requests / limits を更新し、次の配備に反映。requests を増やすと新しい Pod が立ち上がって初めて適用されるので、普通はローリングアップデートで自然に流れていきます。
このサイクルをワークロード単位で一度ずつ回すだけでも、クラスタ全体のリソース使用効率と安定性が大きく変わります。最初から正解を当てようと時間を使うより、速く適度な値で立ち上げてデータで調整する方が速いです。このサイクルを運用コストの観点で本格的に整える流れは 第28章 コスト最適化 で整理します。
次の章のテーマである liveness / readiness probe がリソース圧迫とも直接絡みます — Pod が throttling で応答が遅くなったり OOM 直前にメモリ GC で止まっているとき、probe がその状態をどう検知するかによってワークロードの回復行動が変わります。
練習問題 #
- 上の本文の
deployment-with-resources.yamlをそのまま適用したあと、kubectl get pod <name> -o jsonpath='{.status.qosClass}'で QoS クラスを確認します。その次にlimitsだけ残してrequestsを抜いて apply したとき、そしてrequestsとlimitsを両方抜いて apply したとき、それぞれ QoS がどう変わるかを表に整理します。§「requests / limits の組み合わせ4つ」のモデルと合わせて一段落にメモします。 - わざと CPU 使用量が limits によく達するシナリオを作ってみます —
cpu: limits: 100mのコンテナで短い時間に多くの演算をさせる負荷 (例:stress-ng --cpu 1 --timeout 60s) を回します。kubectl top podの CPU 値と cAdvisor メトリクスのcontainer_cpu_cfs_throttled_seconds_totalの累積がどう動くかを時間順に記録し、コンテナが死なずに応答遅延だけが急増するという §「CPU limit の落とし穴」のモデルを自分の表現で一段落にまとめます。 devネームスペースに LimitRange を適用したあと、requests / limits を抜かした Deployment とmaxを超える Deployment の2つを apply してみてください。各場合のkubectl describe podまたは apply の出力で K8s がどう反応するか (自動埋め vs 拒否) を記録し、§「LimitRange」と 第14章 の ResourceQuota がどう対をなすかを一段落に推論します。
一行まとめ:
requestsはスケジューラがノードを選ぶとき見る保証量、limitsは kubelet が cgroup で強制するランタイム上限。2つの組み合わせが QoS クラス (Guaranteed · Burstable · BestEffort) を決め、ノードリソースの圧迫時に eviction 優先順位になる。CPU limits 超過は throttling (応答遅延の急増)、メモリ limits 超過は OOMKilled (即座の終了) と動作が異なり、JVM · Go のようなランタイムは cgroup 認識を別途気にかけなければならない。ネームスペース単位ポリシーは LimitRange (コンテナ単位の基本値と範囲) と ResourceQuota (合計上限) が対をなす。
次の章 #
本章まで扱ったのは コンテナが受けるリソースの量 のモデルでした。次の章のテーマは視点をリソースから コンテナの生きていること へ移します — K8s がコンテナが正常に動作中かをどう知り、異常状態をどう検知して回復動作を始めるかのモデルです。
第12章 Health check では、3種類の probe を一連の流れで整理します。liveness probe がコンテナ再起動をトリガーする信号で、readiness probe が Service のエンドポイントから外し、加える信号で、startup probe が起動の遅いコンテナにグレース期間を与える信号です。3つの probe の責任がどう異なるか、HTTP / TCP / exec の3つの検査方式のうち何をいつ使うか、initialDelaySeconds / periodSeconds / failureThreshold のようなチューニングパラメータの意味、そして本章のリソースモデルとどう絡むかをマニフェスト1枚の形で追いかけます。