冒頭まとめ

OOMKilled は、コンテナメモリの都合で外から止められたことを示します。ただし、止めた理由は2通りあり、対処が正反対になります。

1つ目は、そのコンテナ自身に設定された上限を超えた場合です。この場合、超えたコンテナだけが止められます。機器に空きがあっても関係ありません。

2つ目は、機器全体のメモリが尽きた場合です。この場合、自分の上限を超えていないコンテナでも止められることがあります。誰が選ばれるかは、あらかじめ設定された優先度で決まります。

そして、この優先度を決めているのは上限ではありません。要求量です。kubelet のソースには計算式が定義されており、要求量を保証する種類なら最も殺されにくい値、上限も要求量も書かない種類なら最も殺されやすい値、その中間の種類なら「1000 から、要求量を機器のメモリ量で割った比率に1000を掛けた値を引いた数」になります。注釈には、要求量を超えて使うコンテナは実質的に最も殺されやすい値になり、優先的な対象になる、と書かれています。

ここが実務上いちばん重要な点です。「上限を上げる」という対処は、1つ目にしか効きません。2つ目に対しては、要求量を正しく設定することが効きます。上限だけを大きくして要求量を書かないままにすると、2つ目の状況ではむしろ殺されやすいままです。

なお、kubelet が能動的に対象を退去させる仕組みは別物で、状態名も違います。こちらの既定のしきい値は、空きが100メガバイトを下回った時点です。

エラーの概要

一覧では、繰り返せば待機中の表示になります。判断の材料は詳細のほうにあります。

Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137
  Started:      Wed, 29 Jul 2026 11:02:14 +0900
  Finished:     Wed, 29 Jul 2026 11:19:48 +0900

終了コードの 137 は、128に9を足した値です。9番は強制終了の信号なので、ソフトウェアが自分で終了したのではなく、外から止められたことを示します。理由の欄が OOMKilled になっていれば、止めた理由はメモリです。

開始から終了までの時間も重要です。数分から数時間かけて止まっている場合、使用量が徐々に増えている可能性があります。起動直後に止まる場合は、初期化の時点で上限を超えています。

種類の判定は、要求量と上限の書き方で決まります。

kubectl get pod <ポッド名> -o jsonpath='{.status.qosClass}'

要求量と上限が等しければ最も保護される種類、どちらも書いていなければ最も保護されない種類、その中間はそれ以外です。

まず最初に:自分の上限か、機器の枯渇か

第一に、止められたコンテナの使用量が、自分の上限に達していたかを確認します。達していれば1つ目です。

第二に、同じ時刻に、同じ機器の上で他のコンテナも止まっていないかを確認します。複数が同時に止まっていれば、2つ目の可能性が高くなります。

第三に、機器全体のメモリの状況を確認します。空きが逼迫していた形跡があれば2つ目です。

この区別を付けずに上限だけを上げると、1つ目なら直り、2つ目なら悪化します。上限を上げたコンテナがより多くを使うようになり、機器全体の逼迫が早まるためです。

よくある原因と解決手順

原因1:自分の上限を超えている

最も分かりやすい形です。設定した上限に対して、実際の使用量が足りていません。

まず、上限と実測を並べます。

kubectl get pod <ポッド名> \
  -o jsonpath='{range .spec.containers[*]}{.name}{"  req="}{.resources.requests.memory}{"  lim="}{.resources.limits.memory}{"\n"}{end}'
kubectl top pod <ポッド名> --containers

Before(上限が実態に対して低い):

resources:
  limits:
    memory: "128Mi"

After(実測に基づいて要求量と上限の両方を書く):

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"

上限を上げる前に、使用量が時間とともに増え続けていないかを確認してください。増え続けている場合、上限を上げても止まる時刻が遅くなるだけです。開始から終了までの時間が回を追うごとにほぼ一定なら、増え続けている可能性が高い形です。

原因2:機器全体が尽きて巻き添えになっている

自分の上限には達していないのに止められた場合です。同じ機器の上の他のコンテナメモリを使い切り、その結果として選ばれています。

前述のとおり、誰が選ばれるかは要求量で決まります。したがって対処は、要求量を正しく設定することです。

Before(上限だけを書いている):

resources:
  limits:
    memory: "512Mi"

After(要求量も書く):

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "512Mi"

要求量と上限を等しくすると、最も保護される種類になります。計算式ではなく固定の値が与えられるため、機器が逼迫しても最後まで残ります。ただし、この設定は配置の際に要求量ぶんの空きを必ず確保するため、機器の使用効率は下がります。すべてに適用する設定ではありません。

止まってほしくないものだけをこの種類にし、残りは要求量を実測に近づける、という配分が現実的です。

原因3:要求量も上限も書いていない

どちらも書いていないコンテナは、最も殺されやすい種類になります。ソースでも、この種類には固定で最大の値が与えられます。機器が逼迫したとき、真っ先に選ばれます。

書いていないコンテナを洗い出してください。

kubectl get pods -A -o json | python3 -c "
import json,sys
d=json.load(sys.stdin)
for p in d['items']:
    for c in p['spec']['containers']:
        r=c.get('resources',{})
        if not r.get('requests',{}).get('memory'):
            print(p['metadata']['namespace'], p['metadata']['name'], c['name'])
"

区画ごとに既定値を与える仕組みもあります。書き忘れを防ぐには、そちらのほうが確実です。

原因4:要求量と上限が離れすぎている

要求量を小さく、上限を大きく書いている場合です。配置は通りやすくなりますが、実際の使用量が要求量を超えた時点で、優先度の計算上は最も殺されやすい値に近づきます。

注釈にも、要求量を超えて使うコンテナは優先的な対象になる、と書かれています。上限まで使えるつもりで設計していると、機器が逼迫した瞬間に選ばれます。

対処は、要求量を実際に定常的に使う量へ近づけることです。上限は瞬間的な増加を吸収する余裕として置き、要求量は普段の使用量に合わせる、という考え方になります。

原因5:退去と混同している

理由の欄が OOMKilled ではなく、対象そのものが退去させられている場合です。これは kubelet が能動的に行う別の仕組みで、機器の空きが設定したしきい値を下回ったときに動きます。既定では空きが100メガバイトを下回った時点です。

見分け方は、対象がまだ存在するかどうかです。強制終了の場合はコンテナが止まって再起動しますが、退去の場合は対象そのものが機器から外されます。

kubectl get events -A --field-selector reason=Evicted

こちらが記録されている場合、対処は個々の上限ではなく、機器全体の余裕の作り方になります。

補足:似ているが別のもの

終了コードが 137 でも、理由の欄が OOMKilled でなければメモリ以外の理由です。生存確認に失敗して止められた場合も、同じ終了コードになります。理由の欄で区別してください。

繰り返し止まって待機の状態に入った場合は、別の表示になります。原因はメモリのままですが、表示は待たせている状態の名前に変わります(Kubernetes の CrashLoopBackOff の記事)。同じ問題の、時間軸での見え方の違いです。

イメージの取得に失敗している場合は、コンテナが起動していないのでメモリとは無関係です(Kubernetes の ImagePullBackOff の記事)。

利用者向けの通信への影響は、状態コードとして現れます。止まっている最中は転送先から外れるため 503 になります(Kubernetes の 503 の記事)。

切り分けの順序

  1. 理由の欄が OOMKilled かを確認する。終了コード 137 だけではメモリとは限らない。
  2. 止められたコンテナの使用量が、自分の上限に達していたかを確認する。
  3. 同じ時刻に同じ機器の上で他も止まっていないかを見る。複数なら機器全体の枯渇を疑う。
  4. 自分の上限に達していたなら、上限を上げる前に、使用量が増え続けていないかを確認する。
  5. 上限に達していないなら、要求量の設定を見直す。上限ではなく要求量が優先度を決める。
  6. 要求量も上限も書いていないコンテナを洗い出す。最も殺されやすい種類になっている。
  7. 対象そのものが機器から外されている場合は、別の仕組みなので対処も別。

確認コマンド集

# 1. 直前の終了の理由と終了コードを取り出す
kubectl get pod <ポッド名> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"  reason="}{.lastState.terminated.reason}{"  exit="}{.lastState.terminated.exitCode}{"\n"}{end}'

# 2. 要求量・上限・種類を並べて見る
kubectl get pod <ポッド名> \
  -o jsonpath='{.status.qosClass}{"\n"}{range .spec.containers[*]}{.name}{"  req="}{.resources.requests.memory}{"  lim="}{.resources.limits.memory}{"\n"}{end}'

# 3. 実際の使用量を確認する
kubectl top pod <ポッド名> --containers

# 4. 同じ機器の上で他にも止まっていないかを見る
kubectl get pods -A -o wide --field-selector spec.nodeName=<機器名>

# 5. 機器全体の割り当て状況を見る
kubectl describe node <機器名> | sed -n '/Allocated resources/,/^Events/p'

# 6. 退去の記録を確認する(別の仕組みかどうかの判定)
kubectl get events -A --field-selector reason=Evicted

# 7. 要求量を書いていないコンテナを洗い出す
kubectl get pods -A -o json | python3 -c "
import json,sys
d=json.load(sys.stdin)
for p in d['items']:
    for c in p['spec']['containers']:
        if not c.get('resources',{}).get('requests',{}).get('memory'):
            print(p['metadata']['namespace'], p['metadata']['name'], c['name'])
"

Editor’s Note

機器が逼迫したときに誰が選ばれるかを決める計算式には、ソースに長い注釈が添えられています。読むと、この仕組みの性格がよく分かります。

注釈はまず、中間の種類は保護される種類と保護されない種類の間に位置し、理想としては要求量より少なく使っているコンテナを守りたい、と目的を述べます。そのうえで、以下の式は経験則である、と明言します。機器のメモリの10パーセントを要求するコンテナは900という値になり、10パーセントを超えて使えば1000になる、という具体例も添えられています。狙いは、要求量を超えて使うコンテナを優先的な対象にすることだ、と書かれています。

そして最後に、正直な但し書きが付きます。これは経験則なので、コンテナが多数の小さな処理単位を抱えている場合には機能しない、というものです。

この但し書きは実務で効きます。優先度はコンテナ単位ではなく、その中の処理単位ごとに評価されます。1つの大きな処理が動いているなら計算どおりに働きますが、小さな処理を大量に抱えていると、個々の処理が使う量は小さいため、狙った順序で選ばれるとは限りません。

つまり、要求量を正しく設定することは有効ですが、絶対の保証ではありません。止まってほしくないものについては、計算式に頼らず、要求量と上限を等しくして固定の値を得る形にするほうが確実です。仕組みが自ら経験則だと述べている以上、その前提で設計する価値があります。


免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。