Kubernetes入門
第7回:リソース管理(Requests/Limits)
はじめに
同じホスト上の別VMが暴走して、自分の担当VMまで重くなった——こんな経験はありませんか?vSphereのリソースプールを調整して対処したものの、「もっとスマートな方法はないのか」と思ったことがあるかもしれません。
Kubernetesの世界でも、コンテナ同士は同じノード(物理/仮想マシン)上でリソースを奪い合います。しかしK8sには、この「隣人トラブル」を未然に防ぐ、洗練された仕組みが備わっています。
今回は、VMの「Reservation(予約)」「Limit(制限)」に相当するK8sの概念——Requests と Limits——を学び、実際にメモリを使い果たしたPodが強制停止される瞬間を目撃します。
7.1 リソースの壁:コンテナの暴走を防ぐガードレール
7.1.1 「隣のコンテナが重い…」は過去の話。K8sが守るリソースの聖域
VMware vSphereでは、リソースプールや個別VMの設定で「Reservation(予約)」と「Limit(上限)」を設定できました。これにより、特定のVMが暴走しても、他のVMに割り当てた「予約分」は守られる仕組みでしたね。
Kubernetesでも、まったく同じ発想の仕組みが存在します。それが Requests と Limits です。
| 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 vSphere | Kubernetes |
|---|---|---|
| 設定単位 | 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.memory | 64Mi | 最低64MiB(メビバイト)のメモリを確保 |
requests.cpu | 100m | 最低0.1コア(100ミリコア)のCPUを確保 |
limits.memory | 128Mi | 最大128MiBまでしかメモリを使えない |
limits.cpu | 200m | 最大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 が OOMKilled → Running に変化し、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: OOMKilled と Exit Code: 137 が記録されています。この情報はPodが再起動しても残るため、後からでも確実にOOMKilledを確認できます。
7.3.2 Reason: OOMKilled を読み解く:K8sが「被害を最小限に抑える」仕組み
先ほどの kubectl describe pod 出力で確認した Reason: OOMKilled と Exit Code: 137。これらは何を意味しているのでしょうか?
なぜK8sはこのPodを殺すのか?
「隣のコンテナのためです。」
Limitsを超えてメモリを使い続けるコンテナを放置すると、ノード全体のメモリが枯渇し、同じノードで動いている他のPodにも影響が及びます。最悪の場合、kubelet(ノードのエージェント)自体がOOMで死に、ノードがNotReadyになります。
K8sは「1つのPodを犠牲にして、他の多数を守る」という判断をします。これは、vSphereでメモリオーバーコミット時にバルーニングが効かなくなった際、ESXiがVMを停止させるのと同じ考え方です。
QoS(Quality of Service)クラスと優先度
複数のPodがオーバーコミット状態で動いているとき、K8sはQoSクラスに基づいて「誰を先に犠牲にするか」を決めます。
| QoSクラス | 条件 | 優先度(低いほど先に殺される) |
|---|---|---|
| BestEffort | Requests/Limitsが未設定 | 最低(真っ先に殺される) |
| Burstable | RequestsとLimitsが異なる、または一部のみ設定 | 中間 |
| Guaranteed | Requests = 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の設定指針(現場のベストプラクティス)
最後に、現場で使える設定指針をまとめます。
| 種類 | Requests | Limits | 理由 |
|---|---|---|---|
| 本番Webサーバー | 通常時の使用量 | 通常時の1.5〜2倍 | スパイク対応の余裕を持たせる |
| バッチ処理 | 低めに設定 | 必要最大量 | スケジュールしやすくし、実行時は全力で |
| データベース | Requests = Limits | 同左 | Guaranteedクラスで最優先保護 |
| 開発/検証環境 | 低めまたは未設定 | 緩めに設定 | リソース効率優先 |
まとめ
今回学んだことを整理します。
| 概念 | VMware vSphere での対応 | Kubernetes での実現 |
|---|---|---|
| 最低保証リソース | Reservation | Requests |
| 上限リソース | Limit | Limits |
| リソース超過時の挙動 | バルーニング、スワップ、VM停止 | CPU: スロットリング、メモリ: OOMKilled |
| 優先度による保護 | リソースプールのシェア | QoSクラス(Guaranteed/Burstable/BestEffort) |
重要なポイント:
- Requests はスケジューリングの判断基準
スケジューラは Requests を見て配置を決める。Limits は実行時の上限。 - CPUは絞られ、メモリは殺される
CPU超過 → スロットリング(遅くなる)
メモリ超過 → OOMKilled(即死、再起動) - QoSクラスで優先度が決まる
Guaranteed > Burstable > BestEffort の順で保護される。 - 設定は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してもデータが消えない」仕組み。コンテナの世界でも、ちゃんと用意されています。
