K8s 中級 #4 resources.requests / limits — Pod のリソース要求と上限

読了 17分

K8s 中級シリーズの 4 番目の記事です。#3 まで視点はクラスタの外にありました — Service、Ingress、Ingress Controller で外部トラフィックがどうクラスタ内のワークロードまで到達するかのモデルでした。この記事の視点は再び Pod の中に戻ります。入ってきたトラフィックを受けて働くコンテナが CPU とメモリをどう要求し、どう上限を与えられるか のモデル、すなわち resources.requestsresources.limits の話です。この 2 つのフィールドの分離が K8s のスケジューリングと安定性を同時に支える土台です。

このシリーズは K8s 中級 7 編です。

requests と limits — 2 つの値の役割が違う #

K8s マニフェストでリソースモデルを表現するフィールドは 2 つです。コンテナ 1 つの resources.requestsresources.limits です。2 つは似て見えますが、見る主体と見る時点が完全に違います。

フィールド見る主体見る時点意味
resources.requestsスケジューラ (kube-scheduler)Pod をどのノードに置くかを決定するときこのコンテナが立っているために保証されなければならない最小リソース
resources.limitskubelet (cgroup)コンテナが実際に動いているときこのコンテナが絶対に超えられない上限

スケジューラは新しい Pod をどこに置くか決めるときに、候補ノードたちの allocatable リソース(ノード全体のリソースからシステムデーモン・kubelet 分を引いた値)からすでに立っている Pod たちの requests の合計を差し引きます。新しい Pod の requests がその残量内に収まるノードだけが候補になります。limits はこの決定に入りません。 あるノードの limits 合計が allocatable を超えても K8s は Pod をその上に立てます — これを オーバーコミット (overcommit) と呼び、ノードのリソースを統計的に効率良く使うためのデフォルト動作です。

limits は次の層で働きます。Pod がノードに割り当てられコンテナが立ち上がると、kubelet がそのコンテナの cgroup に limits 値を設定します。コンテナがその限度を超えようとすると Linux カーネルが強制的に止めます。このときリソース種別で動作が分かれます — CPU は throttling(演算をしばらく受けられないようにする)、メモリは OOMKilled(コンテナの強制終了)です。この違いは後で別途扱います。

頭の中で 1 行に縮めると — requests は「保証されなければならない量」なのでスケジューリングが見る、limits は「絶対超えてはならない量」なのでランタイムが強制する です。この 2 つの値を別々に書けるという点が K8s リソースモデルの核心です。

CPU とメモリの単位 #

マニフェストでよく混乱する部分が単位表記です。CPU とメモリはそれぞれ違う表記法を使います。

CPU — コアとミリコア #

CPU は コア単位 です。1 は 1 コア、2 は 2 コアを意味します。1 コアより小さく分けたいときは ミリコア (millicore) 表記を使います。

表記意味
11 コア (1000 millicore)
500m0.5 コア
250m0.25 コア
100m0.1 コア
0.5500m と同じ

運用マニフェストでは 100m250m のように ミリコア整数型 で書く場合が多いです。0.1 のような小数表記は YAML パース段階で混乱の余地があるので避けるパターンです。CPU 単位はコンテナ cgroup の CPU quota にマッピングされます — 100m なら 100ms サイクルあたり 10ms の CPU 時間を受ける形です。

メモリ — バイナリ vs 十進数 #

メモリは単位接尾辞が 2 系列あって運用事故の常連原因になります。

表記備考
1Ki1024 バイトバイナリ
1Mi1024 KiB = 1,048,576 バイトバイナリ
1Gi1024 MiB = 1,073,741,824 バイトバイナリ
1K1000 バイト十進数
1M1,000,000 バイト十進数
1G1,000,000,000 バイト十進数

1Gi1G は約 7% 差があります(1GiB の方が大きい)。運用マニフェストの標準は バイナリ接尾辞 (MiGi) です。コンテナランタイムと OS がメモリを扱う単位がバイナリで、kubectl top のようなツールが表示する値もバイナリだからです。1G で書いたのに使用量表示は 0.93Gi で出る事故は単位の不一致から来ます。

マニフェスト 1 枚 #

上の 2 つの単位をそのままマニフェストに適用してみます。Deployment の Pod テンプレート内のコンテナ定義に resources キーを入れる形が標準です。

deployment-with-resources.yaml
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 を忘れず書く必要があります。

リソース使用量確認
kubectl top pod
kubectl top pod -n <namespace> --containers
出力例
NAME                   CPU(cores)   MEMORY(bytes)
web-7d4f8b9c5-abc12    180m         420Mi
web-7d4f8b9c5-def34    220m         480Mi

kubectl top は metrics-server がクラスタにインストールされていないと動作しません。表示される値はコンテナ cgroup の実際の使用量なので単位はバイナリです。

requests / limits の 4 つの組み合わせ #

マニフェストに 2 つをどう書くかで動作が大きく分かれます。4 つの組み合わせを 1 つの表にまとめます。

組み合わせ動作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 で、両方とも明示最後 (もっとも安全)
Burstablerequests のみ、requests / limits が異なる、または一部のコンテナだけに書かれている場合中間
BestEffortすべてのコンテナで requests / limits ともになし最初 (もっとも危険)

eviction はノードのメモリ・ディスク圧迫のような信号がしきい値を超えたときに kubelet が Pod を強制終了してリソースを回収する動作です。BestEffort → Burstable → Guaranteed の順に候補になります。同じ等級の中ではリソースをより多く使う Pod が先に候補になります。

Pod の QoS クラス確認
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 累積時間を確認できます。

この負担のため CPU limits を意図的に抜く運用パターン も存在します。requests で保証量だけ取っておき、ノードに余裕があるときはその上に自由にバーストさせる形です。このパターンは次の 2 つの条件が支えるときに使われます。

  • ノードのリソースが十分余裕があり、ワークロード同士が互いに適度なレベルでバーストしてもノードが揺らがないこと
  • requests が合理的に取られていて、1 つのワークロードの暴走が他のワークロードの保証量を侵害しないこと

逆にメモリは limits を抜くパターンがほぼありません — メモリ暴走はノード全体を危険にさらします。続いてその動作を見ます。

メモリ limit の罠 — OOMKilled #

メモリ limit は throttling ではなく hard cap です。コンテナが limit 以上のメモリを割り当てようとすると、Linux カーネルの OOM Killer がそのコンテナのプロセスを即座に強制終了します。K8s はこの終了を検知してコンテナの終了理由を OOMKilled として記録します。

OOMKilled 確認
kubectl describe pod web-7d4f8b9c5-abc12
出力例 — Last State 抜粋
Containers:
  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:  3

Reason: OOMKilledExit Code: 137(SIGKILL)が対で見える形が典型です。Restart Count が早く上がりながら同じ理由が繰り返されるなら、メモリ limit がワークロードの実際の使用量より小さく取られている信号です。

メモリ 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 マニフェストの envGOMAXPROCS を直接設定。
Go コンテナの GOMAXPROCS 手動設定
env:
  - name: GOMAXPROCS
    value: "1"

他の言語ランタイムにも似た罠があり得ます。Node.js の libuv スレッドプールサイズ、Python の multiprocessing.cpu_count() のような部分がホスト基準で取られるか cgroup 基準で取られるかを 1 度確認しておく方が安全です。

メモリ使用量 vs メモリ limits 測定の微妙さ #

メモリ使用量をどう測定するかで OOMKilled の時点に対する直感が異なります。cgroup が見るメモリは RSS (Resident Set Size) + ページキャッシュ のような値で、コンテナが扱うファイル I/O がページキャッシュを満たせばそれも limits に含まれます。kubectl top が表示する値は通常 working set(RSS と似ているが回収可能なキャッシュの一部を除く)なので、OOM 直前までの使用量をそのまま見せてくれない場合があります。

運用で OOMKilled が繰り返されるなら、次の順序で見る方が安全です。

  1. kubectl describe podLast State で OOMKilled の事実と回数を確認。
  2. kubectl top pod --containers で普段の使用量を確認。
  3. cAdvisor メトリクスまたは Prometheus の container_memory_working_set_bytescontainer_memory_rss で時系列を確認。
  4. アプリケーション次元のメモリリークの可能性と limits の上方修正の両方を一緒に検討。

LimitRange — namespace 単位のデフォルト #

マニフェストごとに requests / limits を 1 つずつ書くのは人が忘れやすいです。K8s はこの忘却を防ぐオブジェクトとして LimitRange を提供します。namespace 単位でデフォルト値と許容範囲をかけておくオブジェクトです。

limitrange-default.yaml
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 namespace に適用されている状態で、誰かが requests / limits を抜いたマニフェストを apply すると、K8s が自動で default / defaultRequest 値を埋め込みます。BestEffort QoS Pod を誤って作る事故が遮断されます。逆に 1 つのコンテナが max 以上を要求すると、マニフェスト適用自体が拒否されて、1 人の不注意でノード全体のリソースを占有する事故も防げます。

運用パターンは通常こうです。

  • dev namespace — 小さな default と小さな max にしておく。開発者が軽く立てられるように。
  • stage・prod namespace — ワークロード特性に合わせて default を余裕を持って取りつつ、max は 1 つのコンテナがノード全体を占有できないように制限。

ResourceQuota — namespace 単位の合計上限 #

LimitRange が コンテナ 1 つ単位 のポリシーなら、ResourceQuotanamespace 全体の合計 ポリシーです。1 つの namespace 内のすべての Pod の requests / limits 合計がこの値を超えないように防ぐオブジェクトです。

resourcequota-dev.yaml — 短い例
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 namespace では、すべての Pod の requests.cpu 合計が 10 コアを超えられず、Pod 個数が 50 個を超えられません。新しい Pod のマニフェストがこの限度を破ると apply が拒否されます。

ResourceQuota は LimitRange と対でかけるパターンが多いです — LimitRange がマニフェストの欠落した requests / limits を埋めてくれないと ResourceQuota が合計を計算できないからです。requests が空の BestEffort Pod は ResourceQuota が 0 と見ますが、運用では LimitRange でその 0 を事前に防いでおく形が安全です。

ResourceQuota の本格的な活用は #7 RBAC / NetworkPolicy / ResourceQuota で扱います — セキュリティとリソースポリシーの 1 軸として束ねて見る方が自然です。

運用パターン — どう始めてどう調整するか #

新しいワークロードの requests / limits を最初に決めるときに正確な値を知る術はありません。運用で定着したパターンは次のような順序に従います。

  1. 保守的な requests + 余裕のある limits で始める — 最初のステップは推定です。似たワークロードの過去データを参考にするか、ローカルでの負荷テストで大体の値を取ります。requests は普段の使用量の 70~80%、limits はその 2~3 倍程度が一般的な出発点です。
  2. 運用データ収集 — Prometheus + metrics-server、あるいは Datadog / New Relic のような APM が公開するコンテナ別 CPU・メモリ時系列を数日集めます。トラフィックがもっとも多い時間帯の p95 / p99 使用量を見ます。
  3. VPA recommender 活用 — Vertical Pod Autoscaler を updateMode: Off で立てておけば、実際のリソース変更なしに推奨値だけ受け取れます。K8s がワークロード特性を学習して適切な requests / limits を提案してくれます。VPA の動作は #6 で深く扱います。
  4. 調整と再デプロイ — 推奨値とモニタリングデータを合わせてマニフェストの requests / limits を更新、次のデプロイに反映。requests を増やすと新しい Pod が立たないと適用されないので、通常はローリングアップデートで自然に流れます。

このサイクルをワークロード単位で 1 回ずつ回すだけでもクラスタ全体のリソース使用効率と安定性が大きく変わります。最初から正解を当てようと時間を使うより、早く適度な値で立ててデータで調整する方が早いです。

次回のテーマである liveness / readiness probe がリソース圧迫とも直接絡みます — Pod が throttling で応答が遅くなったり OOM 直前にメモリ GC で止まっていたりするときに、probe がその状態をどう検知するかでワークロードの回復行動が変わります。

まとめ #

この記事で押さえた流れをまとめます。

  • requestslimits の役割が違います — requests はスケジューラがノードを選ぶときに見る保証量で、limits は kubelet が cgroup で強制するランタイム上限です。2 つは別の層のポリシーです。
  • 単位 — CPU はコアとミリコア(1500m100m)。メモリはバイナリ(MiGi)が運用標準。1Gi1G は 7% の差。
  • 4 つの組み合わせ — 両方書くのが標準。limits だけ書くと requests = limits とみなされて Guaranteed。両方抜くと BestEffort、eviction 1 番手。
  • QoS クラス — Guaranteed(requests = limits) / Burstable(その間) / BestEffort(両方なし)。リソース圧迫時に BestEffort → Burstable → Guaranteed の順で eviction。
  • CPU limits 超過は throttling — 即時終了ではない、応答遅延急増の一般的な原因。意図的に CPU limits だけ抜く運用パターンも存在。
  • メモリ limits 超過は OOMKilled — 即時強制終了。kubectl describe podLast State: Terminated + Reason: OOMKilled + Exit Code: 137 がシグナル。
  • JVM は -XX:+UseContainerSupport(Java 10+ デフォルト有効)で cgroup を認識Go の GOMAXPROCS は cgroup を認識しないので automaxprocs ライブラリまたは env の手動設定が必要。
  • LimitRange — namespace 単位のデフォルト(default / defaultRequest)と許容範囲(min / max)。requests / limits が抜けたマニフェストに自動付与。
  • ResourceQuota — namespace 全体の合計上限。LimitRange と対でかけるパターン。詳しい活用は #7
  • 運用サイクル — 保守的な requests + 余裕のある limits で始め、モニタリングと VPA recommender で調整、再デプロイで反映。

このモデルまで手に入れば、マニフェストの resources ブロックに出会ったときに、その 1 つのコンテナがどの QoS でノードリソース圧迫時にどう振る舞うかを 1 行で読めます。

次 — Health check (liveness / readiness / startup probe) #

この記事まで扱ったのは コンテナが受けるリソースの量 のモデルでした。次回のテーマは視点をリソースから コンテナの生存 に移します — K8s がコンテナが正常動作中かをどう知り、異常状態をどう検知して回復動作を始めるかのモデルです。

#5 Health check — liveness / readiness / startup probe では 3 種類の probe を 1 サイクルでまとめます。liveness probe がコンテナの再起動をトリガする信号で、readiness probe が Service のエンドポイントから抜き差しする信号で、startup probe が起動が遅いコンテナにグレース期間を与える信号です。3 つの probe の責務がどう違うか、HTTP / TCP / exec の 3 つの検査方式のうち何を何時使うか、initialDelaySeconds / periodSeconds / failureThreshold のようなチューニングパラメータの意味、そしてこの記事のリソースモデルとどう絡むかをマニフェスト 1 枚の形で追います。

X