更新于

Docker 退出码 137 与 OOMKilled:是内存不够,还是被别的东西杀掉了?

docker ps -a 里是 Exited (137) 或 Restarting (137),应用日志写到一半就断了,没有任何报错,而服务器是一台 1–2 GB 内存、跑着好几个服务的小 VPS。看起来像“内存不够”,很多时候也确实是,但退出码 137 本身只说明进程收到了 SIGKILL。本文写给自己管 Docker 主机的开发者,目标是查清楚是谁发的这个 SIGKILL:内核的 OOM Killer、Docker 自己,还是宿主机上的其他东西。它比 Docker 退出码速查 里 137 那一行讲得更深。如果容器是因为别的原因陷在重启循环里,先看 Docker 容器反复重启怎么排查。

下面的命令都是只读的。只在你有权限的服务器上执行,把 YOUR_CONTAINER 换成实际的容器名或 ID。查内核日志的命令需要 sudo。

退出码 137 是什么意思,为什么日志里什么都没有

137 = 128 + 9:主进程被 9 号信号 SIGKILL 终止。SIGKILL 不能被捕获,也不能延后处理,所以进程没有机会写一句“我要退出了”。日志突然中断、没有报错,和 137 是吻合的,但这不代表应用没问题。

好几种来源都会得到同一个 137:

谁发的 SIGKILL证据通常在哪Docker 的 OOMKilled
内核 OOM Killer,原因是容器自己的内存上限到了内核日志:Memory cgroup out of memory: Killed process …;docker events 里有 oom 事件通常是 true
内核 OOM Killer,原因是整台宿主机内存耗尽内核日志:Out of memory: Killed process …可能是 true 也可能是 false,取决于 cgroup 版本和 Docker / containerd 版本(见第 4 步)
用户态的 OOM 守护进程,比如 systemd-oomd,或 earlyoom(它可能先发 SIGTERM)这个守护进程自己的 journal 日志;内核里没有记录false
docker kill 或 docker rm -fdocker events 里有 kill,signal=9false
docker stop、docker compose down 或重新部署,而应用在整个宽限期(默认 10 秒)里都没理会 SIGTERM先是 kill(signal=15),约 10 秒后又一条 signal=9false
宿主机上的某个脚本、agent 或某个人对这个进程执行了 kill -9Docker 里只有一条 die;也许那个工具自己有日志false

所以内存是首要怀疑对象,但不是结论。下面几步用来缩小范围。

1. 趁重启还没覆盖,先记下退出状态

docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}} policy={{.HostConfig.RestartPolicy.Name}}' YOUR_CONTAINER

这一步不能说明:是谁发的 kill。OOMKilled 是 Docker 自己的一条记录,不是宿主机上发生过什么的完整记录。

2. 问问 Docker:是不是它发的信号

docker events --since 24h --until "$(date +%s)" --filter container=YOUR_CONTAINER --filter event=oom --filter event=kill --filter event=die --filter event=start
docker inspect --format 'stop_signal={{.Config.StopSignal}} stop_timeout={{.Config.StopTimeout}}' YOUR_CONTAINER

多个 event= 过滤条件之间是“或”的关系,所以这条命令只列出这四类事件。常见的几种组合:

docker events 只能返回 daemon 还在内存里保留的最近事件(最后 256 条),daemon 重启后就没了,所以出事后尽早查。

关于停止超时:stop_signal 为空、stop_timeout=<nil> 表示用的是默认值(SIGTERM,10 秒)。如果 PID 1 是一个不会把 SIGTERM 转发给应用的 shell 包装脚本,或者应用本身没有处理 SIGTERM,那么每次停止都会以 SIGKILL 收场。要看 PID 1 到底是什么,可以用 退出码速查 里查看启动命令的那条 inspect。

3. 看内存上限,以及内存都被谁占着

docker inspect --format 'memory_limit_bytes={{.HostConfig.Memory}} memory_swap_bytes={{.HostConfig.MemorySwap}}' YOUR_CONTAINER
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'
free -h
ps -eo pid,user,rss,comm --sort=-rss | head -n 10

宿主机整体 OOM 时,内核主要按内存占用和 oom_score_adj 挑选要杀的进程,所以被杀的进程不一定是造成内存压力的那个。一个容器可能因为邻居涨上去而被杀。

这些都是快照,反映的是现在,不是被杀那一刻;已经停止的容器也没有 stats 数据。想分清是一次性的峰值还是持续上涨,可以在容器重新起来之后连续取几次:

for i in 1 2 3 4 5 6 7 8 9 10; do date '+%H:%M:%S'; docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' YOUR_CONTAINER; sleep 60; done

一直往上走、始终不见平稳,指向内存泄漏,或者没有上限的缓存、队列;只在某些请求或任务期间猛涨,指向业务峰值。十分钟只能给个提示,真正的趋势需要按小时、按天的监控数据。

4. 看内核日志,以及 OOMKilled 什么时候会是 false

sudo dmesg -T | grep -i -E 'oom-kill|out of memory|killed process' | tail -n 20
sudo journalctl -k --since "2026-09-29 14:00" --until "2026-09-29 14:30" --no-pager | grep -i -E 'oom-kill|out of memory|killed process'
docker inspect --format '{{.Id}}' YOUR_CONTAINER

对得上时间的内核日志一条都没有?看看有没有用户态的 OOM 守护进程,它们发 SIGKILL 时内核不会记 OOM:

systemctl is-active systemd-oomd earlyoom
sudo journalctl -u systemd-oomd -u earlyoom --since "2026-09-29 14:00" --no-pager

输出 active 表示这台机器上正在运行对应的守护进程。

确实是内存导致的,OOMKilled 却是 false。 Docker 怎么得知 OOM kill,取决于内核、cgroup 版本以及 containerd 和 Docker 的版本,所以下面这些是要逐一核对的可能,不是定律:

查看宿主机用的是哪个 cgroup 版本:

docker info --format 'cgroup={{.CgroupVersion}} driver={{.CgroupDriver}}'

对还在运行的容器,它自己的 cgroup 计数器记录着上次启动以来的 kill 次数(重启后通常会清零)。cgroup v2 下,如果镜像里有 cat:

docker exec YOUR_CONTAINER cat /sys/fs/cgroup/memory.events

oom_kill 不为 0,说明自上次启动以来,内核杀过这个容器里的进程。cgroup v1 的文件名不一样:多数较新的内核上,/sys/fs/cgroup/memory/ 下的 memory.oom_control 里有一个 oom_kill 计数。

顺带一提:Kubernetes

在 Kubernetes 上,容器超过内存上限时,kubectl describe pod POD_NAME 会显示 Last State: Terminated、Reason: OOMKilled、Exit Code: 137。Evicted 是另一回事:节点资源吃紧,kubelet 把 Pod 驱逐了。存活探针失败,或者删除 Pod 时超过了终止宽限期,也可能得到 137,但不会有 OOMKilled。起作用的上限是容器的 resources.limits.memory。

5. 选择下一步:由你决定的几种做法

按证据选做法。每一种都是需要你计划、并且留好回退办法的变更。

在生产环境改任何东西之前,先写下:证据、要做的变更、怎么确认有效、怎么回退。

在 OpsMate 里怎么做

OpsMate 把 SSH 终端和 AI 放在同一个服务器页面。上面的 docker inspect、docker events、docker stats 你都可以直接手敲;查内核日志的命令需要 sudo,自己手敲是最直接的办法。也可以用一句话把问题告诉 AI,比如“查一下 YOUR_CONTAINER 为什么反复以 137 退出,总结它今天每次重启前的最后几行日志”。AI 会提出排查命令,除了看日志,也可以用 docker、journalctl、ps、df、ss 这类命令做检查;命令跑完后,点击「需要分析」,AI 会给出结论。命令和输出都留在终端里,你可以拿原始日志核对 AI 的结论。点击「需要分析」后,命令输出会发送给云端 AI 分析;桌面端默认保证的只是 SSH 凭证留在本机。日志里有客户数据或密钥时,建议自己手敲命令,只把脱敏后的片段交给 AI。

边界

说明性示例

说明性示例(不是真实客户案例):一台 2 GB 的 VPS 用 Compose 跑着 api(Node.js)、postgres 和 nginx。用户反映接口一天里会断好几次,每次几秒钟。docker ps -a 显示 api 是 Up 12 minutes,inspect 输出 status=running exit=0 oom=false restarts=5,看上去没问题,其实只是上一次启动把这两个值重置了。docker events --since 24h 里有 5 条 die,都是 exitCode=137,每条后面紧跟一条 start,之前都没有 kill 事件,说明不是 docker kill 也不是 docker stop 发的信号。memory_limit_bytes=0。其中一条 die 在 06:05:41 UTC,也就是北京时间 14:05:41。sudo journalctl -k 在这一分钟里有一行带 global_oom 的 oom-kill:,task_memcg 路径里是 api 容器的 ID,紧接着是 Out of memory: Killed process 2314 (node) … anon-rss:1398212kB。重启后每隔一分钟取一次 docker stats,连续十次,api 每分钟涨 15 MB 左右,postgres 基本不动。合起来看:宿主机内存耗尽,内核选中了 api 里的 Node 进程,而 api 每次启动后都在稳定上涨,更像泄漏或没有上限的缓存,而不是一次性的峰值。还没有证实的是:到底是哪段代码在涨。下一步由人来决定:给 api 设内存上限,让压力留在它自己里面;把 Node 的堆上限设得比容器上限低,让失败能在日志里留下记录;再从最近一次部署入手,在应用里找内存增长的来源。

AI 排查提示词

这台服务器上有个容器以退出码 137 退出,或者一直在重启。请先只做检查、不做任何改动,看看:它的 State.Status、ExitCode、OOMKilled、FinishedAt、RestartCount 和内存上限,列出最近 24 小时它的 Docker 事件(oom、kill、die、start),并汇总每次退出前的最后几行日志(带时间戳)。告诉我证据支持的是哪一种 SIGKILL 来源,以及还不能排除什么。内核日志需要 sudo:请把要执行的命令原样列给我,由我来执行,不要自己运行。请区分已证实的事实和推测,列出还缺少的信息。不要重启、删除或更新容器,不要修改内存上限、swap、重启策略或停止设置。如果需要变更,先给出最小步骤、风险、验证方法和回退方案,等我确认。

根据证据选择下一步

试用

每月免费 500 次 AI 调用,服务器数量不限。桌面端默认把 SSH 凭证保留在本机。

需要帮助理解排查结果?

OpsMate 帮助开发者和运维人员用 AI 辅助排查,由你核对证据。点击「需要分析」后,命令输出会发送给云端 AI 分析,请先去除敏感信息。

免费开始 本地凭证桌面端

排障指南