OOMKilled / 終了コード137
コンテナが「OOMKilled」終了コード137で突然死する原因を、cgroupメモリ上限とプロセス全体のOOMという二つの経路まで切り分けて逆引きできる。requests/limits設計とリーク特定で再発を止められる。
- 終了コード137は 128+9 を意味し、プロセスがSIGKILL(シグナル9)で強制終了されたことを示す。OOMKilledはその代表例で、メモリ枯渇を検知したカーネルのOOM Killerがプロセスを問答無用で殺した状態。
- コンテナではcgroupのメモリ上限超過が主因だ。ホストに空きがあっても上限を超えれば、そのcgroup内でOOMが起き、oom_scoreが最も高いプロセスを終了する。
- kubectl describeのLast Stateとdmesg/journalのOOMログを確認する。limit調整、リーク修正、requests/limits適正化が対策で、上限だけ増やすのは対症療法だ。
一言でいうと(このエラーが何を意味するか)
OOMKilled は「Out Of Memory Killed」の略で、メモリが枯渇したためにカーネルの OOM Killer がプロセスを強制終了した状態を指します。終了コード(exit code)137 はその副産物です。Unix のシェル慣習では、シグナルで終了したプロセスの終了コードは 128 + シグナル番号 になります。SIGKILL はシグナル番号 9 なので、128 + 9 = 137。つまり 137 を見たら「何かが SIGKILL でこのプロセスを殺した」 と読めます。その「何か」が OOM Killer であれば OOMKilled、というわけです。
なぜ OOM Killer が存在するのか。Linux は既定でメモリをオーバーコミットします。malloc が成功しても物理ページはまだ割り当てられず、プロセスがそのページに初めて書き込んだ瞬間にページフォルト経由で実割り当てされます(デマンドページング)。約束したメモリを全プロセスが一斉に使い出すと物理メモリ+スワップが尽き、カーネルはページを割り当てられなくなります。ここで固まる代わりに、OOM Killer が「生贄」を選んで SIGKILL を送り、まとまったメモリを一気に回収して系全体のデッドロックを防ぎます。
137 は「SIGKILL で殺された」ことしか意味しません。OOM Killer 以外にも、Kubernetes が terminationGracePeriodSeconds 経過後に送る強制 kill、docker kill、kubectl delete --grace-period=0、liveness probe 失敗後の kill なども 137 になり得ます。OOM かどうかは後述の reason: OOMKilled や dmesg のログで確定させます。文言と併せて切り分けてください。
横にスクロール
よくある原因(複数を具体的に、頻度順)
コンテナ環境での OOMKilled は、ほぼ次のいずれかです。頻度の高い順に挙げます。
- cgroup メモリ上限(limit)の超過: 最頻出。コンテナには cgroup で
memory.max(cgroup v2)/memory.limit_in_bytes(v1)が設定され、その cgroup 内の使用量が上限に達すると、ホストに空きがあってもその cgroup 限定で OOM(memcg OOM)が発火する。Kubernetes のresources.limits.memoryがこの値になる。 - メモリリーク: 解放されない参照が積み上がり、使用量が単調増加して最終的に上限へ到達。JVM のヒープ外(Metaspace・スレッドスタック・ダイレクトバッファ)、Go の goroutine リーク、Node.js のクロージャ滞留、キャッシュの無制限成長などが典型。時間経過で 137 が周期的に出るのが特徴。
- 上限が未設定または過小:
limitsを設定していないコンテナはノードのメモリを食い尽くし、今度はノード全体の OOMを招いて巻き添えで別 Pod が殺される。逆に上限が実ワークロードに対して低すぎると、正常動作でも起動直後やピーク時に即 137。 - JVM / ランタイムがコンテナ上限を認識していない: 古い JVM や設定次第で、コンテナの cgroup 上限ではなくホスト全体の物理メモリを基準にヒープ最大値を決めてしまい、cgroup 上限を超えて確保 → OOMKilled。
-XX:MaxRAMPercentageやコンテナ対応 JVM が要る。 - 一時的なスパイク: バッチの大きな入力、巨大レスポンスのバッファリング、想定外の同時実行数などで瞬間的に上限を突破。平均は低いのに
limitに張り付いて殺される。
Kubernetes の requests.memory はスケジューリング(どのノードに置くか)の予約値で、limits.memory がcgroup 上限として実際の kill 判定に使われます。requests を上げても OOM は防げません。効くのは limits です。ただし limits を上げすぎるとノードのオーバーコミットが過大になり、ノード全体 OOM のリスクが増します。
診断の手順(切り分け方・見るべきログ/コマンド)
まず「本当に OOM か」「どの経路の OOM か(コンテナ単位か、ノード全体か)」を確定させます。Kubernetes なら Pod の直近終了状態を見ます。
kubectl describe pod <pod名>
# Containers → Last State: Terminated / Reason: OOMKilled / Exit Code: 137 を確認
# Restart Count が増え続けていればリーク or 過小 limit の周期的 kill を疑う
より機械可読に取るなら jsonpath で終了理由と直近状態を抜きます。
kubectl get pod <pod名> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
ノード側では、カーネルの OOM Killer ログが一次情報です。どのプロセスが、どの cgroup で殺されたかが残ります。
dmesg -T | grep -i -E 'killed process|oom-kill|out of memory'
# 例: Memory cgroup out of memory: Killed process 1234 (java) ...
# "Memory cgroup out of memory" ならコンテナ(memcg)単位のOOM
# この一語が無く単に "Out of memory" ならノード全体のOOM
journalctl -k | grep -i 'out of memory' # systemd環境
OOM Killer が誰を殺すかは oom_score という指標で決まります。基準はメモリ使用量(RSS+スワップ+ページテーブル等)で、そこに oom_score_adj(-1000〜1000、大きいほど殺されやすい)を加味して算出されます。-1000 は事実上の免除です。生きているプロセスの値は次で確認できます。
cat /proc/<PID>/oom_score # 実効スコア。高いほど次の生贄になりやすい
cat /proc/<PID>/oom_score_adj # 調整値。kubeletが重要Podを保護するため負値を入れることがある
| 観測できる事実 | 示唆される経路 | 次に見るもの | 有力な原因 |
|---|---|---|---|
| describeがOOMKilled + exit 137 | コンテナが上限で殺された | 使用量の時系列とlimit | limit過小 or リーク or スパイク |
| dmesgが Memory cgroup out of memory | memcg(cgroup単位)OOM | その cgroup=そのコンテナのlimit | コンテナのlimit超過 |
| dmesgが Out of memory(cgroup記載なし) | ノード全体のメモリ枯渇 | limits未設定のPod、ノードのallocatable | 上限未設定Podの暴走 |
| 使用量が時間で単調増加 | リーク | ヒープ/goroutine/オブジェクト数 | メモリリーク |
| 起動直後や特定入力で即137 | 初期確保orバッチが上限超え | 起動時ピークとlimitの差 | limit過小 or 巨大入力 |
使用量の時系列は、kubectl top pod や Prometheus の container_memory_working_set_bytes(kubelet が OOM 判定の実質基準にするメトリクス)を見ます。working set が limit に張り付いた直後に再起動しているなら、それが OOM の動かぬ証拠です。
解決と予防(対処と再発防止)
対処は診断で切り分けた「経路」で分岐します。上限をただ上げるのは、リークが原因なら kill の間隔を延ばすだけで根治しません。
- 経路がリーク: まず修正が本筋。JVM ならヒープダンプ(
jmap/ OOM 時の-XX:+HeapDumpOnOutOfMemoryError)、Go ならpprofの heap プロファイル、Node.js なら--inspectのヒープスナップショットで増え続けるオブジェクトを特定する。キャッシュには必ず最大サイズと TTL を入れる。 - 経路が過小 limit / スパイク: 実測ピークに安全マージンを乗せて
limits.memoryを引き上げる。同時にrequests.memoryを実際の定常使用量に合わせ、スケジューリング精度を上げる。 - 経路がノード全体 OOM: すべての Pod に
limitsを設定し、暴走を封じ込める。LimitRangeで名前空間に既定・上限を強制し、ResourceQuotaで総量を制限する。 - ランタイムのコンテナ非対応: JVM は
-XX:MaxRAMPercentage=75.0のように cgroup 上限に対する割合でヒープを決め、limitとヒープ外(スタック・Metaspace)の合計が上限を超えないよう配分する。
# Kubernetes: requests/limits の適正化例(実測に基づき数値を決める)
resources:
requests:
memory: "256Mi" # 定常使用量。スケジューリングの予約に使われる
limits:
memory: "512Mi" # cgroup上限。これを超えるとOOMKilled(137)
QoS クラスも効きます。requests == limits(Guaranteed)の Pod はノードのメモリ逼迫時に最後に退避され、requests < limits(Burstable)や未設定(BestEffort)は先に狙われます。落ちてほしくない基幹 Pod は Guaranteed に寄せます。
- すべての本番コンテナに
limits.memoryを設定し、LimitRangeで未設定を許さない container_memory_working_set_bytes / limitを監視し、80% でアラート(100%=OOMでは手遅れ)- リーク検出は「使用量の傾き」を継続監視し、単調増加に早期に気づく
- limit はリークの隠蔽に使わない。まず原因を直し、その上で実測ピーク+マージンで決める
メモリのオーバーコミット・デマンドページング・OOM Killer といった仕組みの原理は OS、cgroup によるリソース隔離やコンテナのメモリ制限の背景は DevOps を参照してください。
- 終了コード137 = 128 + 9(SIGKILL)。128+N は「シグナルNで終了」を表すシェル慣習
- コンテナの主因は cgroup メモリ上限(memory.max / limit_in_bytes)超過。ホストに空きがあってもmemcg単位でOOMが起きる
- requests はスケジューリング予約、limits が cgroup 上限で kill 判定に使われる。OOMを防ぐのは limits
- 誰を殺すかは oom_score で決まり、oom_score_adj(-1000〜1000)で調整。-1000は免除
- dmesg の "Memory cgroup out of memory" ならコンテナ単位、"Out of memory" 単体ならノード全体のOOM
エラー辞典の記事ガイド
OOMKilled / 終了コード137を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Kubernetes
比較で見る軸
難易度: advanced / カテゴリ: エラー辞典 / タグ数: 6
導入後に効く点
コンテナではcgroupのメモリ上限超過が主因だ。ホストに空きがあっても上限を超えれば、そのcgroup内でOOMが起き、oom_scoreが最も高いプロセスを終了する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- エラー辞典
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Kubernetes / コンテナ」に近いか確認する。
- 強みである「終了コード137は 128+9 を意味し、プロセスがSIGKILL(シグナル9)で強制終了されたことを示す。OOMKilledはその代表例で、メモリ枯渇を検知したカーネルのOOM Killerがプロセスを問答無用で殺した状態。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。