Kubernetes入門 第7回:リソース管理(Requests/Limits)

Kubernetes入門
第7回:リソース管理(Requests/Limits)


はじめに

同じホスト上の別VMが暴走して、自分の担当VMまで重くなった——こんな経験はありませんか?vSphereのリソースプールを調整して対処したものの、「もっとスマートな方法はないのか」と思ったことがあるかもしれません。

Kubernetesの世界でも、コンテナ同士は同じノード(物理/仮想マシン)上でリソースを奪い合います。しかしK8sには、この「隣人トラブル」を未然に防ぐ、洗練された仕組みが備わっています。

今回は、VMの「Reservation(予約)」「Limit(制限)」に相当するK8sの概念——RequestsLimits——を学び、実際にメモリを使い果たしたPodが強制停止される瞬間を目撃します。


7.1 リソースの壁:コンテナの暴走を防ぐガードレール

7.1.1 「隣のコンテナが重い…」は過去の話。K8sが守るリソースの聖域

VMware vSphereでは、リソースプールや個別VMの設定で「Reservation(予約)」と「Limit(上限)」を設定できました。これにより、特定のVMが暴走しても、他のVMに割り当てた「予約分」は守られる仕組みでしたね。

Kubernetesでも、まったく同じ発想の仕組みが存在します。それが RequestsLimits です。

VMware vSphere の用語Kubernetes の用語役割
Reservation(予約)Requests「最低限これだけは確保してほしい」という宣言
Limit(上限)Limits「これ以上は絶対に使わせない」という天井

K8sのスケジューラは、Podをどのノードに配置するか決める際、各Podの Requests を見て「このノードにはまだ余裕があるか?」を判断します。つまり、Requestsは「席の予約」のようなもの。予約した分は、他のPodに奪われることはありません。

7.1.2 CPUとメモリの「Requests(予約)」と「Limits(制限)」の決定的違い

ここで重要なのは、CPUとメモリでは「Limitsを超えたとき」の挙動が根本的に異なるという点です。

CPU:スロットリング(絞り込み)される

CPUは「圧縮可能なリソース(Compressible Resource)」と呼ばれます。Podが設定したLimitsを超えてCPUを使おうとすると、K8sはそのPodのCPU時間をスロットリング(絞り込み)します。

イメージとしては、高速道路の料金所で「あなたはここまで」と速度を落とされる感じ。車(Pod)は止まりませんが、遅くなります。

メモリ:強制終了(OOMKilled)される

一方、メモリは「圧縮不可能なリソース(Incompressible Resource)」です。いったん確保したメモリを「ちょっと返して」とは言えません。

PodがメモリのLimitsを超えると、LinuxカーネルのOOM Killer(Out of Memory Killer)が発動し、そのPod内のコンテナプロセスを強制終了します。K8sはこれを検知し、Podのステータスを OOMKilled とマークします。

CPU超過    → 遅くなる(スロットリング)→ Podは生き続ける
メモリ超過 → 即死する(OOMKilled)    → Podは再起動される

この違いを理解していないと、「CPUは余裕があるのに、なぜかPodが再起動を繰り返す」という現象に悩まされることになります。

7.1.3 VMの「Reservation / Limit」設定とどう違うのか?

VMwareエンジニアの方は、「結局、vSphereの予約/制限と同じでしょ?」と思われるかもしれません。概念としては非常に近いのですが、いくつか重要な違いがあります。

違い①:宣言の粒度

観点VMware vSphereKubernetes
設定単位VM単位Pod(厳密にはコンテナ)単位
設定場所vCenter GUIまたはPowerCLIマニフェスト(YAML)に記述

K8sでは、1つのPod内に複数のコンテナがある場合、コンテナごとにRequests/Limitsを設定します。Podの合計は、各コンテナの設定値を足し合わせたものになります。

違い②:オーバーコミットの考え方

vSphereでは、ホストの物理メモリを超えてVMにメモリを割り当てる「メモリオーバーコミット」が可能でした(バルーニングやスワップで吸収)。

K8sでは、Requestsの合計がノードの割り当て可能量を超えるPodは、そのノードにスケジュールされません。しかし、Limitsの合計はノードの物理量を超えて設定可能です(オーバーコミット状態)。

ノードのAllocatable Memory: 4Gi

Pod A: Requests 1Gi, Limits 2Gi
Pod B: Requests 1Gi, Limits 2Gi  
Pod C: Requests 1Gi, Limits 2Gi

→ Requestsの合計: 3Gi ≤ 4Gi → スケジュール可能
→ Limitsの合計:   6Gi > 4Gi → オーバーコミット状態

この状態で全Podが同時にLimits近くまでメモリを使うと、誰かがOOMKilledされます。K8sは「QoS(Quality of Service)クラス」という優先度に基づいて、どのPodを犠牲にするか決定します(詳細は後述)。

違い③:インフラをコードで管理できる

これが最大の違いかもしれません。vSphereではGUIやPowerCLIでリソース設定を変更していましたが、K8sではYAMLファイルにすべて記述します。

つまり、リソース設定はGitで管理でき、レビューでき、自動デプロイできるのです。「本番で誰かがGUIから予約値を変えちゃった」という事故が起きにくくなります。


7.2 実践:CPU/メモリ制限の設定と挙動確認

理論はここまで。実際に手を動かして、Requests/Limitsの挙動を確認しましょう。

7.2.1 マニフェストへの resources セクションの追加方法

まず、シンプルなNginx Podに対してリソース制限を設定してみます。

[Execution User: developer]

# 作業ディレクトリを作成
mkdir -p ~/k8s-resources-demo
cd ~/k8s-resources-demo

以下の内容で nginx-with-resources.yaml を作成します。

[Execution User: developer]

cat << 'EOF' > nginx-with-resources.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-limited
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.27
    resources:
      requests:
        memory: "64Mi"
        cpu: "100m"
      limits:
        memory: "128Mi"
        cpu: "200m"
    ports:
    - containerPort: 80
EOF

設定値の読み方

項目設定値意味
requests.memory64Mi最低64MiB(メビバイト)のメモリを確保
requests.cpu100m最低0.1コア(100ミリコア)のCPUを確保
limits.memory128Mi最大128MiBまでしかメモリを使えない
limits.cpu200m最大0.2コアまでしかCPUを使えない

CPUの単位「m」(ミリコア)について:
1000m = 1コア です。100m は0.1コア、つまり1コアの10%に相当します。vSphereの「CPU制限: 1000MHz」のような絶対値指定ではなく、コア数に対する相対的な割合で指定する点が異なります。

メモリの単位について:
Mi(メビバイト)は 1024 × 1024 バイトM(メガバイト)は 1000 × 1000 バイト です。K8sでは一般的に Mi を使います。

Podをデプロイしてみましょう。

[Execution User: developer]

kubectl apply -f nginx-with-resources.yaml

リソース設定が正しく適用されているか確認します。

[Execution User: developer]

kubectl get pod nginx-limited -o jsonpath='{.spec.containers[0].resources}' | jq .

期待される出力:

{
  "limits": {
    "cpu": "200m",
    "memory": "128Mi"
  },
  "requests": {
    "cpu": "100m",
    "memory": "64Mi"
  }
}

7.2.2 kubectl top によるリソース使用状況の可視化

Podの現在のリソース使用状況をリアルタイムで確認するには、kubectl top コマンドを使います。ただし、このコマンドは Metrics Server がクラスターにインストールされている必要があります。

kind環境へのMetrics Serverの導入

kind環境では、Metrics Serverがデフォルトでインストールされていないため、手動で導入します。

[Execution User: developer]

# Metrics Serverのマニフェストをダウンロードして適用
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

kindはローカル環境のため、TLS証明書の検証をスキップする設定が必要です。

[Execution User: developer]

# Metrics Serverのデプロイメントにオプションを追加
kubectl patch deployment metrics-server -n kube-system --type='json' -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/args/-",
    "value": "--kubelet-insecure-tls"
  }
]'

Metrics Serverが起動するまで少し待ちます(1〜2分程度)。

[Execution User: developer]

# Metrics Serverの起動を確認
kubectl wait --for=condition=Available deployment/metrics-server -n kube-system --timeout=120s

これで kubectl top が使えるようになりました。

[Execution User: developer]

# ノードのリソース使用状況を確認
kubectl top nodes

# Podのリソース使用状況を確認
kubectl top pods

期待される出力例:

NAME                 CPU(cores)   MEMORY(bytes)
kind-control-plane   125m         512Mi

NAME            CPU(cores)   MEMORY(bytes)
nginx-limited   1m           3Mi

アイドル状態のNginxは、ほとんどリソースを使っていないことがわかります。では、意図的に負荷をかけてみましょう。

7.2.3 負荷試験ツールの導入:Podにわざとストレスをかけてみる

メモリを意図的に消費する「暴走コンテナ」を作成し、Limitsを超えるとどうなるかを観察します。

stress-ng というLinuxの負荷試験ツールを使用します。このツールは、CPU、メモリ、I/Oなど様々なリソースにストレスを与えることができます。

ストレスをかけるPodのマニフェスト

[Execution User: developer]

cat << 'EOF' > stress-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: stress-test
  labels:
    purpose: resource-testing
spec:
  containers:
  - name: stress
    image: polinux/stress-ng:latest
    # 起動後、何もせず待機(後から手動でストレスをかける)
    command: ["sleep", "infinity"]
    resources:
      requests:
        memory: "64Mi"
        cpu: "100m"
      limits:
        memory: "128Mi"
        cpu: "200m"
EOF

Podをデプロイします。

[Execution User: developer]

kubectl apply -f stress-test-pod.yaml

# 起動を待つ
kubectl wait --for=condition=Ready pod/stress-test --timeout=60s

まず、Limits内に収まる負荷をかけてみます。

リソース監視の準備(別ターミナル)

負荷をかける前に、別のターミナルを開いて watch コマンドでリソース使用状況をリアルタイム監視します。

[Execution User: developer]

# 別ターミナルで実行:2秒ごとにリソース使用状況を更新表示
watch -n 2 kubectl top pod stress-test

Note: Metrics Serverはデフォルトで約15秒間隔でメトリクスを収集します。 そのため、短時間の負荷テストでは kubectl top の表示が更新されない場合があります。 確実にメトリクスを観測するには、負荷を一定時間継続させる必要があります。

負荷をかける(元のターミナル)

[Execution User: developer]

# 50MiBのメモリを使用するワーカーを1つ起動(60秒間)
kubectl exec stress-test -- stress-ng --vm 1 --vm-bytes 50M --vm-hang 0 --timeout 60s --verbose

Note: 実行中に oom_score_adj ... Permission denied というメッセージが表示されますが、 これはコンテナ内でOOM Killer優先度の変更権限がないためです。 テスト自体には影響しないので、無視して問題ありません。

別ターミナルの watch 出力を確認すると、メモリ使用量が増加していることがわかります。

出力例:

NAME          CPU(cores)   MEMORY(bytes)
stress-test   45m          52Mi

50MiB程度のメモリ使用なら、Limits(128Mi)の範囲内なので問題なく動作します。

監視が確認できたら、watch コマンドは Ctrl+C で終了してください。


7.3 体験:OOMKilledの発生とK8sによる自動トリアージ

いよいよ本章のクライマックスです。Limitsを超えるメモリを要求して、OOMKilledを発生させます。

7.3.1 メモリ制限(Limits)を超えた瞬間、Podに何が起きるか

先ほどの stress-test Podに、Limits(128Mi)を超える200MiBのメモリ負荷をかけてみましょう。

監視の準備(別ターミナル)──これが重要!

重要: K8sはOOMKilledを検知すると非常に高速にPodを再起動します。 OOMKilled ステータスが表示されるのはほんの数秒間です。 事前に watch で監視していないと、この瞬間を見逃してしまいます。

必ず先に別ターミナルを開き、以下のコマンドを実行して監視を開始してください。

[Execution User: developer]

# 別ターミナルで実行:Podの状態をリアルタイム監視(1秒間隔)
watch -n 1 kubectl get pod stress-test

監視画面が表示されたことを確認してから、次のステップに進んでください。

負荷をかける(元のターミナル)

[Execution User: developer]

# 200MiBのメモリを使用しようとする(Limitsの128Miを超過)
kubectl exec stress-test -- stress-ng --vm 1 --vm-bytes 200M --vm-hang 0 --timeout 60s

このコマンドを実行すると、数秒以内にPodとの接続が切れるはずです。

command terminated with exit code 137

Exit code 137 は、プロセスがシグナル9(SIGKILL)で強制終了されたことを意味します(128 + 9 = 137)。これはOOM Killerによる強制終了のサインです。

監視ターミナルで確認できること

watch で監視していると、以下のような変化が見られます。

① OOMKilled発生直後(一瞬):

NAME          READY   STATUS      RESTARTS      AGE
stress-test   0/1     OOMKilled   1 (2s ago)    5m

② 数秒後(自動再起動):

NAME          READY   STATUS    RESTARTS      AGE
stress-test   1/1     Running   1 (10s ago)   5m

STATUS が OOMKilledRunning に変化し、RESTARTS の数値が増えていることが確認できます。これがK8sの自己修復能力です。

Note: watch で監視していなかった場合、後から kubectl get pod を実行しても すでに Running に戻っていることがほとんどです。 ただし、RESTARTS の数値が増えていることでOOMKilledが発生したことはわかります。

監視が確認できたら、watch コマンドは Ctrl+C で終了してください。

OOMKilledの確実な確認方法

OOMKilled を見逃してしまった」という場合でも、心配はいりません。kubectl describe pod を使えば、過去の終了理由を確実に確認できます。

[Execution User: developer]

kubectl describe pod stress-test

出力の中から Last State セクションを探してください。

    State:          Running
      Started:      Sun, 19 Jan 2026 10:30:15 +0900
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Sun, 19 Jan 2026 10:28:05 +0900
      Finished:     Sun, 19 Jan 2026 10:30:10 +0900

Reason: OOMKilledExit Code: 137 が記録されています。この情報はPodが再起動しても残るため、後からでも確実にOOMKilledを確認できます。

7.3.2 Reason: OOMKilled を読み解く:K8sが「被害を最小限に抑える」仕組み

先ほどの kubectl describe pod 出力で確認した Reason: OOMKilledExit Code: 137。これらは何を意味しているのでしょうか?

なぜK8sはこのPodを殺すのか?

「隣のコンテナのためです。」

Limitsを超えてメモリを使い続けるコンテナを放置すると、ノード全体のメモリが枯渇し、同じノードで動いている他のPodにも影響が及びます。最悪の場合、kubelet(ノードのエージェント)自体がOOMで死に、ノードがNotReadyになります。

K8sは「1つのPodを犠牲にして、他の多数を守る」という判断をします。これは、vSphereでメモリオーバーコミット時にバルーニングが効かなくなった際、ESXiがVMを停止させるのと同じ考え方です。

QoS(Quality of Service)クラスと優先度

複数のPodがオーバーコミット状態で動いているとき、K8sはQoSクラスに基づいて「誰を先に犠牲にするか」を決めます。

QoSクラス条件優先度(低いほど先に殺される)
BestEffortRequests/Limitsが未設定最低(真っ先に殺される)
BurstableRequestsとLimitsが異なる、または一部のみ設定中間
GuaranteedRequests = Limits(全リソースで)最高(最後まで守られる)

先ほどの stress-test Podは、Requests ≠ Limits なので Burstable クラスです。

[Execution User: developer]

# QoSクラスを確認
kubectl get pod stress-test -o jsonpath='{.status.qosClass}'

出力:

Burstable

本番環境で「絶対に落としたくない」Podには、Requests = Limits に設定して Guaranteed クラスにすることを検討しましょう。

7.3.3 【次回予告】サーバーを消してもデータは消さない「永続ストレージ(PVC/CSI)」

今回、OOMKilledで再起動したPodは、何も失っていませんでした。なぜなら、Nginxもstress-ngも、重要なデータをどこにも保存していなかったからです。

しかし、データベースやファイルサーバーのようなアプリケーションでは、話が違います。Podが再起動したり、別のノードに移動したりしても、データは消えてほしくないですよね。

次回は、K8sにおける「永続ストレージ」の仕組み——PersistentVolume (PV)PersistentVolumeClaim (PVC)、そしてCSI(Container Storage Interface)——を学びます。

vSphereで言えば、「VMDK を共有ストレージに置いて、vMotion してもデータは消えない」という概念に相当します。お楽しみに。


7.4 トラブルシューティングのTips

現場で遭遇しやすいリソース関連のトラブルと、その解決法をまとめます。

Podが Evicted(退去)されてしまった場合

OOMKilled と似て非なるステータスが Evicted です。これは、ノードレベルでリソース(ディスク、メモリ、PID)が枯渇したとき、kubeletが「優先度の低いPodを追い出す」ことで発生します。

[Execution User: developer]

# Evictedなpodを確認
kubectl get pods --field-selector=status.phase=Failed | grep Evicted

確認の優先順位

トラブルシューティングは以下の順序で行います。

① ディスク容量の確認

ノードの /var/lib/kubelet/var/lib/containerd が満杯になっていないかを確認します。

[Execution User: developer]

kubectl describe node | grep -A 5 "Conditions:"

DiskPressure: True と表示されていたら、ディスク容量不足です。

② メモリプレッシャーの確認

同じ出力で MemoryPressure: True と表示されていたら、ノードのメモリが不足しています。

③ PID枯渇の確認

PIDPressure: True は、プロセス数の上限に達している状態です。フォーク爆弾のような攻撃や、プロセスリークを疑いましょう。

kubectl describe node でクラスター全体の「余力」を確認する作法

ノードにPodをスケジュールできない(Pending状態が続く)場合、ノードのリソース状況を確認します。

[Execution User: developer]

kubectl describe node kind-control-plane

注目すべきセクションは以下の3つです。

① Allocatable(割り当て可能なリソース)

Allocatable:
  cpu:                2
  memory:             3956Mi
  ephemeral-storage:  46080Mi
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  pods:               110

これは「K8sがPodに割り当てられるリソースの上限」です。ノードの物理リソースから、システム予約分を引いた値です。

② Allocated resources(現在の割り当て状況)

Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
  --------           --------     ------
  cpu                750m (37%)   1200m (60%)
  memory             384Mi (9%)   768Mi (19%)

Requests の列が重要です。これが100%に近づくと、新しいPodをスケジュールする余地がなくなります。

③ Non-terminated Pods(稼働中のPod一覧)

Non-terminated Pods:          (8 in total)
  Namespace    Name                         CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------    ----                         ------------  ----------  ---------------  -------------
  default      nginx-limited                100m (5%)     200m (10%)  64Mi (1%)        128Mi (3%)
  default      stress-test                  100m (5%)     200m (10%)  64Mi (1%)        128Mi (3%)
  kube-system  coredns-76f75df574-xxxxx     100m (5%)     0 (0%)      70Mi (1%)        170Mi (4%)
  ...

どのPodがどれだけリソースを「予約」しているかが一目でわかります。

Requests/Limitsの設定指針(現場のベストプラクティス)

最後に、現場で使える設定指針をまとめます。

種類RequestsLimits理由
本番Webサーバー通常時の使用量通常時の1.5〜2倍スパイク対応の余裕を持たせる
バッチ処理低めに設定必要最大量スケジュールしやすくし、実行時は全力で
データベースRequests = Limits同左Guaranteedクラスで最優先保護
開発/検証環境低めまたは未設定緩めに設定リソース効率優先

まとめ

今回学んだことを整理します。

概念VMware vSphere での対応Kubernetes での実現
最低保証リソースReservationRequests
上限リソースLimitLimits
リソース超過時の挙動バルーニング、スワップ、VM停止CPU: スロットリング、メモリ: OOMKilled
優先度による保護リソースプールのシェアQoSクラス(Guaranteed/Burstable/BestEffort)

重要なポイント:

  1. Requests はスケジューリングの判断基準
    スケジューラは Requests を見て配置を決める。Limits は実行時の上限。
  2. CPUは絞られ、メモリは殺される
    CPU超過 → スロットリング(遅くなる)
    メモリ超過 → OOMKilled(即死、再起動)
  3. QoSクラスで優先度が決まる
    Guaranteed > Burstable > BestEffort の順で保護される。
  4. 設定はYAMLで管理
    Gitで管理し、レビューし、自動デプロイ。「GUIで誰かが変えた」事故を防げる。

後片付け

検証で作成したリソースを削除します。

[Execution User: developer]

kubectl delete pod nginx-limited stress-test --ignore-not-found=true

次回予告

第8回:永続ストレージ(PVC/CSI)

今回は「コンテナが死んでも、また起動すれば元通り」という状況でした。しかし、データベースやファイルサーバーでは、データの永続化が必須です。

次回は、Kubernetesにおける永続ストレージの仕組み——PersistentVolume、PersistentVolumeClaim、そしてCSI——を学びます。

vSphereで言えば「共有ストレージ上のVMDKをvMotionしてもデータが消えない」仕組み。コンテナの世界でも、ちゃんと用意されています。