Certified Kubernetes Administrator (CKA) #15 リソース管理: requests/limits、QoS、LimitRange、ResourceQuota

#14 Scheduling 2 では taints/tolerations と PriorityClass で どの Pod をどのノードにどんな優先度で置くか を決めました。この記事はもう一歩踏み込み、その Pod がノードの cpu と memory を どれだけ予約し、どこまで使えるか を扱います。requests/limits で個々のコンテナの資源を束ね、QoS クラスでノードが圧迫されたときに誰が先に追い出されるかを理解し、LimitRange と ResourceQuota でネームスペース単位の運用ポリシーを強制します。

このテーマは Workloads and Scheduling ドメインの最後のピースであると同時に、トラブルシューティングでよく出会う OOMKilled と Pending の根本原因でもあります。資源設定を知らないと、#22 Troubleshooting 1 の OOM・Pending 問題を最後まで追跡できません。

requests と limits: 予約と上限 #

Kubernetes でコンテナの資源は 2 つの値で表現されます。requests は予約limits は上限 です。この 2 つを明確に区別できないと、スケジューリングとトラブルシューティングの両方が揺らぎます。

  • requests: このコンテナが正常動作に最低限必要とする量です。scheduler はノードの 割り当て可能 (allocatable) な資源の中に requests 合計が入る余地 があるノードにだけ Pod を配置します。つまり requests はスケジューリング決定の基準です。
  • limits: このコンテナが使える最大量です。ランタイムが cgroup でこの上限を強制します。limits を超えようとすると cpu はスロットルされ、memory はプロセスが死にます。

scheduler は limits ではなく requests だけを見て配置する という点が核心です。limits の合計はノード容量を超えても配置され、これを オーバーコミット (overcommit) と呼びます。

単位表記 #

cpu と memory は単位表記が異なります。

資源表記意味
cpu10.5500m1 は vCPU 1 コア、500m は 0.5 コア (millicore)
memory128Mi256Mi1GiMi = メビバイト (1024²、2^20)、Gi = ギビバイト

cpu は millicore 単位で細かく刻めるので 250m は 0.25 コアを意味します。memory は MiGi (2 進接頭辞) と MG (10 進接頭辞) が異なるため、運用では混同を避けるために MiGi に統一する方が安全です。

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: "250m"      # 0.25 コア予約
          memory: "128Mi"  # 128Mi 予約
        limits:
          cpu: "500m"      # 0.5 コア上限
          memory: "256Mi"  # 256Mi 上限

上のコンテナは 0.25 コアと 128Mi を予約され、負荷が集中すると 0.5 コアと 256Mi まで使えます。requests と limits の間の区間が バースト余裕 です。

CPU スロットル vs memory OOMKilled #

limits を超過したときの動作は cpu と memory で 根本的に異なります。この違いが試験と運用で最も混同しやすいポイントです。

資源limits 超過時の動作結果
cpuスロットル (throttle)。cgroup が CPU 時間を削るプロセスは死なず遅くなる
memoryOOMKilled。カーネル OOM killer がプロセスを終了コンテナ再起動 (restartPolicy に従う)

cpu は 圧縮可能 (compressible) な資源 です。あるコンテナが cpu limit に達すると、カーネルはそのコンテナに割り当てる CPU 時間を減らすだけで、プロセスを殺しはしません。そのため cpu limit が低すぎると、アプリが死ぬ代わりに 応答が遅くなる 症状として現れます。

memory は 圧縮不可能 (incompressible) な資源 です。すでに割り当てられたメモリを回収する方法はないため、コンテナが memory limit を超えるとカーネル OOM killer がそのプロセスを終了します。このとき kubectl describe pod の状態が OOMKilled と表示され、restartPolicy に従って再起動と CrashLoopBackOff につながります。

# OOMKilled の確認
k describe pod web | grep -A3 "Last State"
#   Last State:     Terminated
#     Reason:       OOMKilled
#     Exit Code:    137

終了コード 137 (= 128+9、SIGKILL) が見えたらメモリ超過を疑います。解決はメモリ limit を上げるか、アプリのメモリ使用を減らす方向です。このパターンは #22 Troubleshooting 1 で再び扱います。

QoS クラス: 誰が先に追い出されるか #

ノードの memory が不足すると、kubelet は Pod を eviction (追い出し) して資源を回収します。このときどの Pod を先に追い出すかは QoS (Quality of Service) クラス が決めます。QoS はユーザーが直接指定する値ではなく、requests と limits をどう設定したかから自動的に導出 されます。

QoS クラス条件eviction 優先度
Guaranteedすべてのコンテナが cpu・memory の requests と limits を持ち、それぞれ requests == limits最後 (保護される)
Burstable1 つ以上のコンテナに requests があるが Guaranteed 条件に未達中間
BestEffortどのコンテナにも requests/limits が一切ない最初 (優先して追い出し)

ノードが memory 圧迫を受けると、kubelet は BestEffort → Burstable → Guaranteed の順で Pod を追い出します。つまり資源を明確に予約した Pod ほど保護されます。運用上の含意は明確です。重要なワークロードほど requests と limits を明示 して Guaranteed に近づけておけば、ノード圧迫の状況で生き残ります。

Guaranteed を作る方法 #

Guaranteed はすべてのコンテナで requests と limits が完全に同じとき に付与されます。

spec:
  containers:
    - name: app
      image: nginx
      resources:
        requests:
          cpu: "500m"
          memory: "256Mi"
        limits:
          cpu: "500m"      # requests と同じ
          memory: "256Mi"  # requests と同じ

limits だけ指定して requests を省略すると、Kubernetes が requests を limits と同じ値に自動補正します。そのため limits だけ書いても Guaranteed になれます。QoS クラスは次で確認します。

k get pod web -o jsonpath='{.status.qosClass}'
# Guaranteed

LimitRange: ネームスペースのデフォルト値と境界 #

個々の Pod ごとに requests/limits を逐一書かせるのは運用で非現実的で、抜けると BestEffort になって危険です。LimitRange はネームスペース単位で コンテナのデフォルト値と許容範囲 を強制するオブジェクトです。

フィールド役割
defaultコンテナが limits を省略したら埋める既定 limit
defaultRequestコンテナが requests を省略したら埋める既定 request
min許容する最小値 (これより小さいと作成拒否)
max許容する最大値 (これより大きいと作成拒否)
apiVersion: v1
kind: LimitRange
metadata:
  name: container-limits
  namespace: dev
spec:
  limits:
    - type: Container
      default:           # limits 未指定時に埋める値
        cpu: "500m"
        memory: "256Mi"
      defaultRequest:    # requests 未指定時に埋める値
        cpu: "250m"
        memory: "128Mi"
      min:               # 許容最小
        cpu: "100m"
        memory: "64Mi"
      max:               # 許容最大
        cpu: "1"
        memory: "1Gi"

この LimitRange が適用された dev ネームスペースでは、resources を空にしたまま Pod を作っても自動的に defaultRequestdefault が埋まります。また max1Gi を超える memory limit を要求すると API server が作成を拒否します。LimitRange は 作る時点で検証する ので、適用前にすでに立っていた Pod には遡及しません。

k apply -f limitrange.yaml
k describe limitrange container-limits -n dev

ResourceQuota: ネームスペース総量の制限 #

LimitRange が コンテナ 1 つ の境界なら、ResourceQuotaネームスペース全体の総量 を制限します。1 つのチームがクラスター資源を独占しないようにネームスペース単位で上限をかける、マルチテナンシー運用の核心ツールです。

ResourceQuota は大きく 2 つを制限します。

  • 資源総合: ネームスペース内のすべての Pod の cpu・memory requests/limits の合計
  • オブジェクト数: Pod、Service、ConfigMap、Secret、PVC などの個数
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: dev
spec:
  hard:
    # 資源総合
    requests.cpu: "4"          # requests cpu 合計最大 4 コア
    requests.memory: "8Gi"     # requests memory 合計最大 8Gi
    limits.cpu: "8"            # limits cpu 合計最大 8 コア
    limits.memory: "16Gi"     # limits memory 合計最大 16Gi
    # オブジェクト数
    pods: "20"                 # Pod 最大 20 個
    services: "5"
    configmaps: "10"
    persistentvolumeclaims: "4"

重要な制約が 1 つあります。ResourceQuota が requests.*limits.* を制限するネームスペースでは、すべてのコンテナが該当する requests/limits を必ず指定しなければなりません。 指定しないとクォータ集計が不可能なので Pod 作成が拒否されます。そのため ResourceQuota は LimitRange とペアで 使うのが定石です。LimitRange がデフォルト値を埋めてくれれば、requests/limits を忘れた Pod もクォータ検証を通過します。

k apply -f resourcequota.yaml
k describe resourcequota team-quota -n dev
# Resource          Used   Hard
# --------          ----   ----
# requests.cpu      1500m  4
# requests.memory   3Gi    8Gi
# pods              7      20

UsedHard に達すると、その資源をさらに要求する Pod は forbidden エラーで拒否されます。クォータ超過で作成が止まったのに原因が分からないとトラブルシューティングが長引くので、Pending や作成失敗に出会ったら describe resourcequota で先に使用量を確認する習慣が役立ちます。

運用の観点: ネームスペースポリシーセット #

実務で新しいチームネームスペースを作るときは、次の 3 つを 1 まとまりで適用します。

  1. Namespace: 隔離単位を作ります。
  2. LimitRange: コンテナのデフォルト値と min/max を強制し、requests 抜けによる BestEffort と過大な単一コンテナを防ぎます。
  3. ResourceQuota: ネームスペースの総資源とオブジェクト数に上限をかけ、1 つのチームがクラスターを独占しないようにします。

この 3 つのオブジェクトが揃ってこそマルチテナンシーが安定して回ります。LimitRange なしで ResourceQuota だけかけると requests を書かない Pod が全部拒否されて開発者が混乱し、ResourceQuota なしで LimitRange だけかけると個々のコンテナは適正でもネームスペースの総量が無限に増えます。

この資源ポリシーの基礎概念は Kubernetes 中級 #4 で requests/limits と QoS を入門の観点で先に整理してあるので、概念が曖昧なら一緒に見ると役立ちます。

試験ポイント #

CKA でこのテーマは次の形でよく出ます。

  • requests/limits の追加: 既存の Deployment や Pod に cpu・memory の requests/limits を明示する作業。k edit やマニフェスト修正で処理し、単位表記 (mMiGi) を正確に書くのが採点ポイントです。
  • OOMKilled の診断: Pod が繰り返し再起動する原因を探すトラブルシューティング。describeOOMKilled と終了コード 137 を確認し、memory limit を上げます。
  • LimitRange の作成: ネームスペースに default/defaultRequest/min/max を持つ LimitRange を作る作業。type: Container を落とさないようにします。
  • ResourceQuota の作成: ネームスペースに資源総合やオブジェクト数のクォータをかける作業。requests.cpu のようにドット (.) が入ったキー表記を正確に書きます。
  • QoS クラスの確認: k get pod <name> -o jsonpath='{.status.qosClass}' でクラスを確認したり、特定の QoS になるよう requests/limits を合わせる作業。

素早い作成コマンドも一緒に覚えておくと時間を節約できます。

# requests/limits を指定して Pod 作成
k run web --image=nginx \
  --requests='cpu=250m,memory=128Mi' \
  --limits='cpu=500m,memory=256Mi'

# ネームスペースに ResourceQuota を素早く作成
k create quota team-quota -n dev \
  --hard=requests.cpu=4,requests.memory=8Gi,pods=20

まとめ #

この記事で押さえたこと:

  • requests は予約、limits は上限。scheduler は requests だけを見て配置し、limits の合計がノード容量を超えるオーバーコミットが可能です。
  • cpu はスロットル、memory は OOMKilled。cpu は圧縮可能な資源なので遅くなるだけで死なず、memory は圧縮不可能なので limit 超過時に終了 (コード 137) します。
  • QoS 3 種。Guaranteed (requests==limits)・Burstable・BestEffort の順で保護の強さが高く、eviction は BestEffort から起こります。
  • LimitRange。ネームスペースのコンテナ単位で default・defaultRequest・min・max を強制します。
  • ResourceQuota。ネームスペースの総資源とオブジェクト数を制限し、LimitRange とペアで使ってこそ安定します。

次へ: Storage 1 #

資源の cpu と memory を束ねたので、次はデータを入れる ストレージ です。

#16 Storage 1: Volume types、PV、PVC 静的プロビジョニング では、emptyDir・hostPath のような Volume タイプから、PersistentVolume と PersistentVolumeClaim でストレージを宣言的に要求しバインドする静的プロビジョニングまで、そして accessModes と reclaim policy が何の違いを生むかを、自分でマニフェストを書きながら扱います。

X