容器被 OOM 杀掉(exit 137):从内核日志到 cgroup 内存文件的排查链
先看退出码与内核日志
docker ps -a
# CONTAINER ID IMAGE STATUS
# 3f8a9c2b1d4e app:v1.0 Exited (137) 2 minutes ago
docker logs 3f8a9c2b1d4e --tail 50 # 应用日志,常见只剩一行 Killed
dmesg -T | grep -i "oom"
# [Mon Jan 15 14:45 2024] Memory cgroup out of memory: Killed process 12456 (java) total-vm:4194304kB
# [Mon Jan 15 14:45 2024] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null)
退出码 137 = 128 + 9,表示进程收到 SIGKILL,极大概率是 OOM Killer 干的。先不要急着重启,用 docker inspect 看重启策略和最后状态:
docker inspect web-app | grep -i oom
# "OomKilled": true,
# "OomScoreAdj": 0
直接读 cgroup 的限制与用量
cgroup v1 下可以直接进容器对应的 memory cgroup:
CONTAINER_ID=$(docker inspect --format='{{.Id}}' web-app)
CGROUP_PATH="/sys/fs/cgroup/memory/docker/$CONTAINER_ID"
cat $CGROUP_PATH/memory.limit_in_bytes # 8589934592 (8GB)
cat $CGROUP_PATH/memory.usage_in_bytes # 7516192768 (约7GB)
cat $CGROUP_PATH/memory.stat
# cache 1073741824 ← 页面缓存 1GB
# rss 6442450944 ← 真实物理内存 6GB
# mapped_file 268435456
# inactive_anon / active_anon / inactive_file / active_file ...
cat $CGROUP_PATH/memory.oom_control
# oom_kill_disable 0
# under_oom 0
# oom_kill 3 ← 已发生 3 次 OOM kill
关键点:不能用 memory.usage_in_bytes 当作真实占用,它包含可回收的 Page Cache。要看 memory.stat 里的 rss。这和 free 命令不能看 “free” 字段、要看去掉 Page Cache 的 “available” 是一个道理。
memory.oom_control 决定达到 limit 时是否触发 OOM Killer(默认触发,且只能杀控制组内的进程)。echo 1 > memory.oom_control 可以不杀进程,但正在申请物理内存页的进程会停住,它不是内存回收机制。
cgroup v2 环境下看三个文件:memory.current(当前用量)、memory.max(硬上限,显示 max 表示未设限额)、memory.events(oom、oom_kill 计数)。例如 memory.max=512MiB、memory.current=300MiB、memory.events: oom_kill 5,即使主机还剩 3GB,容器也可能因为 512MiB 上限被杀。
判定 OOM 类型
- cgroup 级 OOM:主机全局内存尚有余量,但
memory.events的oom_kill计数持续增长。优先核查容器/服务的资源限额、应用堆内存配置,以及当前 cgroup 下挂载的其他辅助进程占用。 - 主机级 OOM:多进程 RSS 同时上涨、可用内存长期低位、swap 读写活跃。优先排查流量放大、批处理任务并发、缓存占用和宿主机容量是否匹配。
排查命令
docker stats # 实时用量,含 Cache/Buffer,注意 USAGE 与 LIMIT 对比、MEM %
docker top web-app # 进程资源占用
ps aux --sort=-%mem | head -10
vmstat 1
CPU > 80% 或内存持续增长,可能过载。
两个常见误判与真因
- 误判一:
docker stats显示内存高,但应用没用多少。原因是 Linux 把空闲文件缓存算作内存占用。看USAGE对比LIMIT、看MEM %趋势,而不是绝对值。Overlay2 存储驱动的缓存有时被误报为内存泄漏,只要不频繁读写镜像层通常不是问题。 - 真因一:Java 只设了
-Xmx而忽略容器限制。JVM 内存 ≠ 堆内存,还包括线程(Thread)、Code Cache、GC、Other。示例:容器Memory: 2147483648(2GB),启动命令为java -Xmx1536m -Xms1024m -jar app.jar,jcmd 1 VM.native_memory summary显示 Heap 1.5GB + Thread 180MB + Code 90MB + GC 70MB + Other 60MB ≈ 1900MB,接近 2GB 上限。解决是在启动参数里加-XX:MaxRAMPercentage=75.0,让 JVM 感知容器限制。 - 真因二:死循环或无限增长的缓冲队列。日志处理程序如果消费速度跟不上生产速度,消息队列会无限膨胀,最终撑爆内存。
修复后要验证
- 服务 RSS 占用是否在任务执行完成后正常回落,而不是每轮任务跑完都把内存基线抬高。
memory.current是否始终和memory.max保留足够安全距离。memory.events的oom、oom_kill计数是否不再上涨。systemctl status的重启次数保持稳定,内核日志里没有新的 OOM 记录。
只单纯提高限额通常只是把故障推迟到下一次流量高峰。先定位短时内存峰值的实际来源,再选择修复代码、调整并发数、改成分批处理,或是提高资源限额。