跳到主要内容

技术教程

Docker Exit 137:如何证明是 OOM,而不是一次普通 SIGKILL

验证与风险信息

适用环境
Docker 24.0.7 / Compose 2.40.0 / cgroup v2 / Node.js 22.20.0 Alpine
验证耗时
约 10 分钟
风险与回滚
只读诊断为低风险;实验只在具有 48 MiB 硬限制的隔离容器中执行
最后复核
2026-08-01
验证证据
隔离容器记录 OOMKilled=true、ExitCode=137;memory.events 记录 oom=1 与 oom_kill=1;同限制下有界负载退出 0。

容器显示 Exited (137) 时,最常见的误判是直接得出“内存不够”。137 只表示进程最终收到了 SIGKILL:它可能来自容器内存上限触发的 OOM,也可能来自宿主机内存压力、人工执行 docker kill,或编排系统强制终止。正确做法是先把退出码、Docker 状态、cgroup 计数和事发前内存曲线连成一条证据链,再决定扩容还是修应用。

适用范围与先给结论

本文适用于 Docker Engine 与使用 Linux cgroup 的容器环境。V2CE 复现实验使用 Docker 24.0.7、Compose 2.40.0、cgroup v2 和 node:22.20.0-alpine,为实验容器设置 48 MiB 硬限制,并禁止额外 swap 空间。

判断顺序应当是:

  1. 确认容器实际退出时间、退出码和是否被 Docker 标记为 OOMKilled
  2. 确认生效的内存上限,而不是只看 Compose 文件。
  3. 检查 cgroup v2 的 memory.events,区分达到上限与真正发生 OOM kill。
  4. 把容器证据与宿主机内核日志、监控时间线对齐。

只有退出码 137,没有 OOMKilled=true 或其他同时段证据,不能单独证明 OOM。

第一步:保存容器退出状态

先不要立即删除或重新创建故障容器。重建会丢失原容器的状态和 cgroup 上下文。用容器 ID 检查完整状态:

docker inspect --format \
'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} finished={{.State.FinishedAt}}' \
my-container

docker inspect my-container > /tmp/my-container-inspect.json
docker logs --timestamps --since 30m my-container > /tmp/my-container.log 2>&1

ExitCode=137 对应 128 + 9,也就是被 SIGKILL 终止。OOMKilled=true 是更直接的容器级证据,但仍要继续确认限制值和宿主机情况,尤其是在节点整体内存紧张时。

第二步:确认真正生效的内存限制

不要假定配置文件里的值已经应用到当前容器。读取 Docker 保存的 HostConfig:

docker inspect --format \
'memory={{.HostConfig.Memory}} memory_swap={{.HostConfig.MemorySwap}} oom_kill_disable={{.HostConfig.OomKillDisable}}' \
my-container

这里的数值单位是字节。也可以用下面的命令观察运行时用量与限制;故障很快时应由监控系统连续采样,而不是事后只执行一次:

docker stats --no-stream my-container

Docker 官方把 --memory 定义为硬限制。进程越过可用范围且无法回收时,内核可能终止容器内进程。不要通过关闭 OOM killer 来“修复”问题;这会把风险从单个容器转移到宿主机。

第三步:读取 cgroup v2 的 OOM 计数

在 cgroup v2 环境中,容器对应目录下的 memory.events 会记录内存事件。目录布局随 systemd、cgroup driver 和 Docker 版本变化,因此生产环境应先从容器 PID 或现有监控确定 cgroup 路径,不要把某个固定路径写死到所有机器。

container_id=$(docker inspect --format '{{.Id}}' my-container)
find /sys/fs/cgroup -type f -name memory.events -path "*${container_id}*" -print
sudo cat /sys/fs/cgroup/.../memory.events

关注以下计数:

  • max:内存使用尝试超过 memory.max 的次数;
  • oom:cgroup 内发生 OOM 的次数;
  • oom_kill:OOM killer 实际终止进程的次数。

计数是累计值,应保存故障前后的差值或至少记录采样时间。容器退出后 cgroup 目录可能很快消失,所以最好由节点监控持续采集。

第四步:排除另外两类 137

人工或自动化发送 SIGKILL

docker kill 默认发送 SIGKILL,也会产生 137,但通常不会让 State.OOMKilled 变成 true。检查部署平台事件、审计日志、定时任务和运维操作记录。若应用没有在优雅停止期限内退出,编排系统也可能在超时后发送 SIGKILL;这时应检查停止信号与终止宽限期。

宿主机整体 OOM

当节点整体内存耗尽时,内核可能选择某个容器进程作为牺牲对象。把容器结束时间与内核日志对齐:

journalctl -k --since "30 minutes ago" --no-pager | grep -Ei 'out of memory|killed process|oom'
dmesg -T | grep -Ei 'out of memory|killed process|oom'

如果同一时间多个容器异常、节点可用内存接近零,处理范围就不能只局限于单个容器限制。

修复:先控制工作集,再调整上限

修复目标不是让“137 消失”,而是让正常峰值工作集与容器上限之间保留可解释的余量。按以下顺序执行:

  1. 从监控中确定稳定工作集、峰值、增长速率和并发量。
  2. 检查应用自身的堆上限、缓存、队列、批处理大小和并发数。应用堆之外还存在 native memory、线程栈、共享库和页缓存,不能把应用堆设置成与容器限制完全相同。
  3. 若工作集合理但限制过低,再逐级提高 --memory,并为节点保留系统与其他服务的空间。
  4. 若内存持续增长,先定位泄漏或无界队列;自动重启只能降低中断时间,不能替代修复。

改动后在与生产相同的并发和数据规模下做压力复核,至少观察一个完整业务高峰。确认 oom_kill 不再增长、延迟没有因频繁回收显著恶化,并验证停止和滚动发布仍可正常完成。

调整前应保存原镜像、应用内存参数、容器的 Memory/MemorySwap 和节点容量基线。若调整后出现节点内存压力、延迟恶化或其他工作负载被挤压,应恢复原限制与应用参数并使用上一版镜像回滚;不能为了保住单个容器而把 OOM 风险转移到整台节点。

V2CE 复现实验记录

实验容器被设置为 48 MiB 内存和 48 MiB memory+swap 上限。Node.js 进程每 100 毫秒分配 4 MiB 并写入内存。Docker 最终记录 OOMKilled=trueExitCode=137;在进程退出前采样到的 memory.eventsmax 19oom 1oom_kill 1

随后在同样 48 MiB 限制下运行有界工作负载。应用只分配 8 MiB 工作集,输出完成信息并以 0 退出。实验结束后 Compose 网络和容器均被清理。这证明本实验中的 137 来自受控 cgroup OOM,而不是人工发送 SIGKILL;同时说明限制本身不是故障,越过限制的工作集才是触发条件。

复核清单

  • 保存原容器的 inspect、日志和结束时间;
  • 确认 ExitCodeOOMKilled,不只看管理面板文案;
  • 确认当前容器真正生效的 MemoryMemorySwap
  • 保存 memory.events 差值和宿主机内核日志;
  • 调整后用真实负载复测,并观察完整高峰期。

参考资料