概要
Kubernetes で、爆発的にメモリを消費する Pod を動かした場合、どうなるのか興味があり色々と設定を変えながら挙動の検証を行なってみた。
実験環境
EKS で実験を行った。
メモリ2GiBの Worker Node を用意し、その中にメモリを爆発的に使うような Pod (pythonが動くプロセス)を配置した。
なお、Swapは無効になっているとする。
memory.limit オーバーコミット時の OOM
実験内容
memory.limit の設定の不備により、OOM Killed が発生する条件が変わるかどうかを確かめてみる。例えば、memory.limitを4GiB としてみる。これは Node の物理メモリをオーバーコミットしているので明らかにおかしい。では、この状況で Pod 内に短時間に大量のメモリを消費するプロセスを起動してみる。
実験結果
その後、sudo journalctl -k | grep -Ei 'oom|out of memory|killed process' といったコマンドで Kernel のログを確認すると、
oom_memcg=/kubepods.slice の cgroup の memory.max を超えたメモリが使用されかつ回収が収まらないため OOM Kill が発生し、かつ Killed process 5301 (python3) とあることから、OOM Killer が kubepods.slice の中から該当の python プロセスを選んで kill していることがわかる。
cat /sys/fs/cgroup/kubepods.slice/memory.max を実行した結果が約1.5GiB であり、python プロセスがこの値よりも多くのメモリを使っていたことが原因である。なおこの1.5GiBは Capacity - kube-reserved - system-reserved であり、kubelet や containerd などを除いてアプリケーションの Pod が使える上限であり、Kubernetes の Pod 定義に書くmemory.limitとは別である。
これにより、Pod のmemory.limitがオーバーコミットにより機能していない場合でも、親の kubepods.slice により、結果的に該当 Pod の python プロセスが適切に kill されたことがわかる。
2026-10-10T02:16:15+0000 ip-10-0-1-79.ap-northeast-1.compute.internal kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-6c44f2ffd92fe1185f8aca37c58df3dbc1fc9b6ad573d465b64f9659abe96abb.scope,mems_allowed=0,oom_memcg=/kubepods.slice,task_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod0f0d7eee_7947_42b3_bb5b_4ee068d6f25a.slice/cri-containerd-6c44f2ffd92fe1185f8aca37c58df3dbc1fc9b6ad573d465b64f9659abe96abb.scope,task=python3,pid=5301,uid=0
2026-10-10T02:16:15+0000 ip-10-0-1-79.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 5301 (python3) total-vm:1666020kB, anon-rss:1501568kB, file-rss:528kB, shmem-rss:0kB, UID:0 pgtables:2996kB oom_score_adj:998
補足: OOM Group
cat /sys/fs/cgroup/kubepods.slice/memory.oom.group で OOM Group を確認した結果、0 であった。つまり、OOM スコアに応じて cgroup に所属するプロセスを選び、kill するという意図通りの挙動になっている。この値が1の場合、kubepods.slice に所属する他のプロセスも影響を受けてしまう。
実際、この値を1にした場合関係ない他の Pod も影響を受けていることがわかる。
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-618e2122460381d8a02de84148e1defa256a1cdb694b88477939f49b0fe1490b.scope,mems_allowed=0,oom_memcg=/kubepods.slice,task_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podd284a752_607b_4f84_9312_9199a418d73a.slice/cri-containerd-618e2122460381d8a02de84148e1defa256a1cdb694b88477939f49b0fe1490b.scope,task=python3,pid=6380,uid=0
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 6380 (python3) total-vm:1666020kB, anon-rss:1502208kB, file-rss:456kB, shmem-rss:0kB, UID:0 pgtables:3004kB oom_score_adj:998
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Tasks in /kubepods.slice are going to be killed due to memory.oom.group set
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 2308 (pause) total-vm:1020kB, anon-rss:0kB, file-rss:156kB, shmem-rss:0kB, UID:65535 pgtables:40kB oom_score_adj:-998
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 2575 (aws-vpc-cni) total-vm:1267628kB, anon-rss:2432kB, file-rss:188kB, shmem-rss:0kB, UID:0 pgtables:108kB oom_score_adj:-997
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 2592 (aws-k8s-agent) total-vm:1421848kB, anon-rss:20084kB, file-rss:868kB, shmem-rss:0kB, UID:0 pgtables:304kB oom_score_adj:-997
Oct 11 09:43:21 ip-10-0-1-105.ap-northeast-1.compute.internal kernel: Memory cgroup out of memory: Killed process 2589 (aws-vpc-cni) total-vm:1267628kB, anon-rss:2432kB, file-rss:188kB, shmem-rss:0kB, UID:0 pgtables:108kB oom_score_adj:-997
補足: cgroupの構造
tree -d /sys/fs/cgroup で確認したところ以下のようになった。(一部省略)
これを見たら分かるように、コンテナ単位では Limit に達していなくても、親の/kubepods.slice の上限に達すれば OOM Kill が発生する。今回はこのケースだった。
/sys/fs/cgroup
├── dev-hugepages.mount
├── dev-mqueue.mount
├── init.scope
├── kubepods.slice
│ ├── kubepods-besteffort.slice
│ └── kubepods-burstable.slice
│ ├── kubepods-burstable-pod5c89fd9f_6c09_48a3_90ff_d6aeb304ccfa.slice
│ │ ├── cri-containerd-7afa24da9342271310fea1659dd9ee6da1dfb82586aac152209864486487964c.scope
│ │ └── cri-containerd-cb710a08724dad22cd2b1ae21aa50a6d0b53b20ddd0e4c40c358acc80ee1cdbf.scope
│ ├── kubepods-burstable-podd284a752_607b_4f84_9312_9199a418d73a.slice
├── runtime.slice
│ ├── containerd.service
│ └── kubelet.service
├── system.slice
│ ├── amazon-ssm-agent.service
│ ├── auditd.service
memcg OOM が発生しない場合
実験内容
上記のように、cgroup 内のプロセスが kubepods.sliceや Kubernetes のmemory.limit の制限に到達したことにより OOM が発生する場合を、memcg OOM と呼ぶことにする。
では、memcg OOM が発生しなかった場合を考えてみよう。
ここでは、以下を実行し kubepods.sliceの上限を4GiBにしてみる。
sudo systemctl set-property --runtime kubepods.slice MemoryMax=4G
この状態で先ほどの python プロセスを起動すると、memcg OOM が発生せず Node 全体がメモリ枯渇の状態になることが予想される。では、この時の様子を確認してみる。
実験結果
先ほどとは変わり、python プロセスが OOM されることはなかった。その代わり、Node 全体が不安定な状況となる。具体的には、Status が NotReady となり、kubelet が機能せず Pod が削除できなかったり、SSM Agent も機能せず外部からの接続もできなくなった。
kubelet が動作していないため、Node の Event から OOM の記録を確認するのはあまり良くないと考え、Kernel のログを確認する。
再起動後、Kernel のログを確認し、OOM が発生したか否かをsudo journalctl -k -b -1 | grep -Ei 'oom|out of memory|killed process' で検証したところログはなく、残っているログの範囲内では OOM が発生していないということになった。
これでは何が起きているかわからないため、メモリ関連の統計データを確認できるよう以下のスクリプトを実行させておく。
/proc/vmstat、/proc/pressure/memory、/proc/meminfo は再起動で消えてしまうため、再起動後も確認できるようストレージに書き出している。
#!/bin/bash
LOG=/var/log/reclaim-monitor.log
while true; do
{
echo "===== $(date -u '+%F %T UTC') ====="
grep -E '^(pgscan_|pgsteal_|allocstall|workingset_refault_file|oom_kill)' /proc/vmstat
cat /proc/pressure/memory
grep -E '^(MemAvailable|MemFree|Cached|AnonPages|SReclaimable):' /proc/meminfo
} >> "$LOG"
sync -d "$LOG"
sleep 1
done
結果を以下に示す (メモリの単位はMiB)。
なお、監視ログは途中で途絶えているため、それ以降のメモリ状態については不明である点に注意が必要である。
グラフから、AnonPages(匿名ページ)の増加分は主に python プロセスが実行時に 動的確保しているメモリによるものと考えられ、最終的にはノード全体で約1.6GiBに達している。Nodeの物理メモリが2GiBであることを考えると、匿名ページがメモリの大部分を占めていることがわかる。
今回の環境では Swap が無効であるため、python が確保した匿名ページは通常のメモリ回収では解放できない。そのため、メモリ不足に伴いページキャッシュが回収され、Cached が減少していると考えられる。(ページキャッシュをメモリからディスクに書き出す)
なお、ページキャッシュとはファイルのデータをメモリ上にキャッシュするというものである。
また、pgscan_kswapd および pgsteal_kswapd の増加から、kswapd によるページの走査と回収が実際に行われていることも確認できる。

また、観測できた範囲内では OOM Killed の発生を示すログはなかった。理由として考えられることは、後述するが kswapd 回収量自体は増え続けている という点である。
OOM が発生せずにどうなっていたのか
現在のように、匿名ページのメモリ使用量が増えているのにも関わらず、OOM Kill が発動しないのは以下の状況になっていることが考えられる:
OOM Killer がページキャッシュの回収を延々と試みて、回収に進捗があれば OOM Kill が行われないことからページの回収とメモリアクセス(再読み込み)を繰り返し Thrashing の状態になっている可能性がある。参考
この Thrashing の状態になると、システムの性能は著しく低下するため、先ほどのような全体的な応答性能の低下に繋がった可能性がある。実際、グラフを見ると kswapd による回収量は増え続けている。
状況としては、swap-thrashingと同じような状況になっている可能性がある。
memory.minを設定してみる
実験
興味深いことに、memory.min を設定すると、Node 単位での OOM を発生させることができた。当初の目的は、kubelet などの必要なプロセスをページ回収から保護するためだったが、memory.minを設定すると回収できる絶対的なページ数が少なくなるため OOM が発生しやすくなる可能性がある。
なお、memory.minのデフォルト値は0であり、設定したい階層だけでなくその親の階層にも明示的に設定する必要がある点には注意が必要である。参考
この時、先ほどと同じように計測を行いグラフを表示させたところ以下のようになった。
04:35:01 の直前ほどのタイミングで一つ目のグラフはメトリクスが途切れており、ここまでは先ほどのグラフと同じような形となっている。
04:35:04 で python プロセスが kill され、匿名ページのメモリ量が急激に低下した。それと同時に利用可能なメモリ量も急激に増えていることがわかる。
OOM Kill が発生する直前、ページを回収するための kswapd の走査量は急激に増えているものの回収量は頭打ちとなっていることがわかる。つまり、回収できるページがなくなりOOM Killerが発動したと解釈でき、これは OOM Killer の実装通りで、それをグラフから読み取ることができたことになる。
設定値に応じた挙動の変化
以下のように、system.sliceのmemory.minを128M としたところ、上記のような結果となり OOM Kill が発生した。
しかし、この値を半分である64M としたところ、OOM Killが発生せず Node 全体が応答不能になる状態となった。128M のメモリを保護したところ他に回収できるページキャッシュがなくなったが、64M の場合はまだ回収できる余地があったということである。
確かに、回収から保護するメモリ量を増やせば増やすほど、回収できるページ数は少なくなるので OOM Kill は発生しやすいのは理論上事実であり、この閾値が64M~128Mの間であったということなのだろう。ちなみに、もう少し調査を続けたところ、84M の場合 OOM Kill が発生したが、72M の場合は発生せず Node 全体が応答不能となった。
sudo systemctl set-property --runtime system.slice MemoryMin=128M
OOM がギリギリ発生しない場合の状況
memory.minを72M としたところ、OOM は発生しなかったものの、実験の結果 OOM が発生するか否かの閾値あたりであることがわかった。この状況での調査をすることで、OOM がギリギリ発生しないものの Node 全体が応答不能になる際の状況の手がかりを掴むことができるのではと考えた。
以下のグラフを見ると、MemAvailable が0となっている中で、kswapd の回収量は少しずつであるが右肩上がりになっていることがわかる。グラフからは外しているものの、走査量はより急激に増えていた。つまり、kswapd によるメモリ走査量は増え続けており実際の回収量も少しずつ増え続けている場合、延々と回収を繰り返すということである。
これは、Linux開発者による議論における、以下の記述と合致するシチュエーションになっており、改善しようとする動きが見られているようだ。
It's possible for reclaim to make minimal or no progress on an LRU type despite having
ample reclaimable pages
This can cause the reclaim path to loop indefinitely.
次に、メモリが Stall 状態となっているかどうかをみるために、PSI を確認してみる。
このグラフは、メモリ回収とページキャッシュの再読み込みが延々と繰り返される、激しいReclaim Thrashing状態を示している。
full avg10が約95%であることは、直近10秒間の約95%で、測定対象のすべての非idleタスクがメモリ待ちによって停止していたことを意味する。
つまり、Nodeが物理的に停止しておらずカーネルは動き続けているものの、ユーザー空間の処理がほとんど進めない状態となっている。
加えて、pgscan_kswapdは増え続け、oom_killは増えていないことから、OOM Kill による回復が起こらない一方で、メモリ回収処理だけは継続していることが確認できる。
まとめ
- OOM Kill は、cgroup で所属する階層の
memory.maxを超え、かつメモリを回収できなくなった場合に発生する。- 自階層の
memory.maxがオーバーコミットしてしまった場合でも、その親の階層(Kubernetes であればkubepods.slice)のmemory.maxにより OOM Kill が発生するはずである。
- 自階層の
- 上述の場合の OOM Kill を memcg OOM と呼ぶとする。
memory.maxの設定不備により memcg OOM が発生しない場合、Node 単位でメモリが逼迫する状況となり、回収可能なメモリが存在しなくなったらGlobal OOM が発生するはずである。 - メモリを爆発的に使用しているプロセスがあるのにも関わらず、回収できるページが存在することが理由で OOM Kill がなかなか発動しないことがある。この場合、PSI を確認するとほぼ全てのタスクがメモリ不足のためブロックされている状態になっている可能性が高い。メモリの Thrashing と呼ばれ、ページキャッシュの回収(メモリからディスクへの書き出し)とページキャッシュの読み込み(メモリへの読み込み)が繰り返されている状態となっていることが考えられる。
-
memory.minを設定することでページキャッシュの回収からメモリを保護することができる。この値が相対的に大きくなればなるほど、回収可能なメモリ量は減少するため、回収可能なメモリがなくなる状態になる可能性が高くなり、OOM Kill が発動されやすくなる。
終わりに
- グラフの描画には ChatGPT を使っています。
- 個人的に興味がある範囲内での検証をしており、体系的な記事にはなっていません。
- 裏付けが取れておらず推測をもとに記述している箇所もあるので、参考程度に見ていただけると助かります。
参考
以下サイト・記事などを参考にしました:


