OOMKilledが実際に意味すること
コンテナが設定されたメモリ上限を超えて使用しようとしました。LinuxカーネルのOOMキラーが介入してプロセスを終了し、Kubernetesは終了理由をOOMKilledとして報告します。
これはクラッシュではありません。アプリケーションが例外をスローしたわけでも、バグに当たったわけでもありません。カーネルが外部からプロセスを強制終了したのです。kubectl describe podで以下のように表示されます:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Mon, 20 Mar 2026 10:12:00 +0000
Finished: Mon, 20 Mar 2026 10:14:33 +0000
終了コード137 = 128 + 9(SIGKILL)。これが決定的なサインです。
何かする前に診断する
単純に上限を引き上げてうまくいくことを祈るだけではいけません。まず、コンテナが実際に必要とするメモリ量を把握しましょう。
現在のPodのステータスを確認する
kubectl describe pod <pod-name> -n <namespace>
LimitsとRequestsのセクションまでスクロールします。次にLast Stateブロックを探してください。OOMKilledはそこに表示されます。
リアルタイムのメモリ使用量を確認する
# namespace内の全Podの使用量
kubectl top pods -n <namespace>
# コンテナ単位(Podにサイドカーがある場合に便利)
kubectl top pod <pod-name> --containers -n <namespace>
過去のメモリメトリクスを取得する
Prometheusがある場合は、ワーキングセットをクエリして時間経過によるピーク使用量を確認します:
container_memory_working_set_bytes{pod="<pod-name>", container="<container-name>"}
例えば、コンテナのピークが420Miなのに上限が256Miの場合、差分は164Miです。これで推測ではなく、具体的な数値をもとに対処できます。
修正1:メモリ上限を引き上げる
多くの場合、これが解決策です。Deploymentを編集してlimits.memoryを増やします:
kubectl edit deployment <deployment-name> -n <namespace>
またはマニフェストを直接更新します:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi" # 256Miから2倍に増加
cpu: "500m"
kubectl apply -f deployment.yaml
目安として、上限は観測されたピークワーキングセットの1.5〜2倍に設定します。アプリのピークが420Miであれば、640Miを試してみてください。4Giに跳び上げるのは避けましょう。ノードのメモリが枯渇するまで本質的な問題を隠すだけです。
修正2:上限を完全に削除する(リスクを理解した上で)
バッチジョブ、ML推論、動画処理など、メモリ使用量が本質的にスパイクするワークロードもあります。そのような場合、リクエストのみを設定して上限を設けないことが合理的な場合があります:
resources:
requests:
memory: "256Mi"
cpu: "250m"
# limitsブロックなし
上限がなければ、コンテナはノード上の空きメモリを自由に使用できます。ただし、リスクも現実として存在します。1つのPodのメモリリークが、同じノード上の他のすべてのPodのリソースを枯渇させる可能性があります。ノードのキャパシティプランニングがしっかりしており、使用量を積極的に監視している場合にのみこの方法を選択してください。
修正3:メモリリークを発見して修正する
上限を2度引き上げてもOOMKilledが続く場合、アプリがリークしています。まず、強制終了されたコンテナのログを確認しましょう:
# 以前の(強制終了された)インスタンスのログ
kubectl logs <pod-name> -n <namespace> --previous
確認箇所はランタイムによって異なります:
- Java/JVM:
-Xmxを設定し忘れると、カーネルが強制終了するまでJVMが肥大化します。ヒープサイズを明示的に設定してください:-Xms256m -Xmx384m。メタスペースも制限します:-XX:MaxMetaspaceSize=128m。 - Node.js:デフォルトのV8ヒープ上限は64ビット環境で約1.5GBと、ほとんどのコンテナには高すぎます。起動コマンドに
--max-old-space-size=384を追加してください。 - Python:大きなDataFrame、無制限のキャッシュ、またはGCが回収できない循環参照が原因です。
tracemallocやmemory-profilerで確認してください。
JVMユーザーが陥りやすい典型的なトラップがあります。コンテナ上限を512Miに設定し、-Xmxも512Miに設定して、なぜコンテナが強制終了されるのか疑問に思うケースです。JVMはヒープ以外にもメモリを必要とします。メタスペース、スレッドスタック、GCオーバーヘッドなど、通常100〜150Mi追加で必要です。ヒープが512Miであれば、コンテナ上限は少なくとも650〜700Mi必要です。
# 512Miコンテナ向けの安全なJVM設定
ENV JAVA_OPTS="-Xms128m -Xmx384m -XX:MaxMetaspaceSize=128m"
修正4:VPAに適切な数値を教えてもらう
どの上限を設定すべきか迷っている場合、VPAを推奨モードで実行しましょう。実際の使用量を監視して値を提案します。自動適用はしません:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updatePolicy:
updateMode: "Off" # 推奨のみ、Podを再起動しない
kubectl apply -f vpa.yaml
# 1〜2時間後に確認する
kubectl describe vpa my-app-vpa -n <namespace>
VPAは観測した内容に基づいて推奨されるrequestsとlimitsを表示します。それをベースラインとして使用してください。
修正を確認する
ロールアウトを監視します:
kubectl rollout status deployment/<deployment-name> -n <namespace>
新しいPodでOOMKilledが解消されていることを確認します:
kubectl describe pod <new-pod-name> -n <namespace> | grep -A5 "Last State"
Last Stateが空またはクリーンな終了であれば問題ありません。その後、メモリが徐々に増加していないことを確認するため、1時間ほど監視を続けましょう:
watch kubectl top pods -n <namespace>
予防策
- **必ずrequestsとlimitsの両方を設定する。**上限のないPodはノードのメモリをすべて消費し、問題とは無関係だったワークロードでも連鎖的なOOMKillを引き起こす可能性があります。
- **強制終了が発生する前にアラートを出す。**Prometheusでは、ワーキングセットが上限の80%を5分以上超えた場合にアラートを発火させます。これにより対処する時間的余裕が生まれます。
- 設定を忘れたチームを検知するためにLimitRangeを使用する:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
spec:
limits:
- default:
memory: 256Mi
defaultRequest:
memory: 128Mi
type: Container
- **デプロイ前に負荷テストを実施する。**現実的なトラフィック下でアプリを実行してメモリのピークを計測します。勘ではなく、計測データをもとに上限を設定しましょう。
- **ピークだけでなくトレンドを監視する。**メモリがスパイクして元に戻るのは正常です。6〜12時間にわたって頭打ちすることなく着実に上昇し続けるのはリークのサインです。

