更新于
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 -f | docker events 里有 kill,signal=9 | false |
docker stop、docker compose down 或重新部署,而应用在整个宽限期(默认 10 秒)里都没理会 SIGTERM | 先是 kill(signal=15),约 10 秒后又一条 signal=9 | false |
宿主机上的某个脚本、agent 或某个人对这个进程执行了 kill -9 | Docker 里只有一条 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
容器一旦重新启动,Docker 会把
ExitCode重置为 0、OOMKilled重置为false。 有重启策略时,你 inspect 到的往往是已经重新跑起来的容器(status=running exit=0 oom=false restarts=5),看上去一切正常。这时上一次退出的信息只能去docker events(第 2 步)和内核日志(第 4 步)里找。docker ps里的Restarting (137)只在 Docker 两次重启之间的等待期里才看得到。status=exited exit=137 oom=true:首要怀疑是容器自己的内存上限到了,仍然要用第 3、4 步确认。status=exited exit=137 oom=false:还不能排除内存(见第 4 步),但先看 Docker 自己的事件。容器还在运行却
oom=true,或者退出码不是 137 却oom=true:在较新的 Docker 版本里,只要内核杀掉了容器里的任何一个进程,这个标记就会被置上,不只是主进程。被杀的是某个 worker 或子进程时,容器可能继续运行,也可能主进程随后以自己的退出码(比如 1)结束。内存问题不一定以 137 收场。运行时先碰到自己的堆上限时,通常会打出报错再以别的方式退出:Node.js 会打印
JavaScript heap out of memory,常见退出码是 134(SIGABRT);JVM 会记录java.lang.OutOfMemoryError。finished是 UTC 时间(结尾带Z),和宿主机本地时间的日志对比之前先换算(北京时间 = UTC + 8 小时)。
这一步不能说明:是谁发的 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= 过滤条件之间是“或”的关系,所以这条命令只列出这四类事件。常见的几种组合:
oom,接着die(exitCode=137):内核在容器的内存上限内杀了进程,主进程随之退出。只有
oom,后面没有die:容器里某个进程被杀了,主进程还活着。去应用日志里找那个时间点前后有没有 worker 挂掉。kill(signal=15),约 10 秒后kill(signal=9),然后die(exitCode=137):停止请求等满了宽限期。这不是内存问题。kill(signal=9),然后die:有人或某个程序执行了docker kill或docker rm -f。只有
die(exitCode=137),之前没有 Docker 发出的kill:信号来自 Docker 之外,最常见的是内核。去看第 4 步。每个
die后面紧跟一条start,是重启策略在起作用。较新的 Docker 版本还会在die里带上execDuration(进程跑了多少秒);如果这些退出都是内存被杀,每次时长差不多,说明进程很可能是涨到一定程度就被杀。
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
memory_limit_bytes=0:没设上限,容器可以一直用宿主机的内存,直到整台机器吃紧。这种情况下,更可能走到宿主机层面的 OOM。memory_swap_bytes是“内存 + swap”的总量。和内存上限相同,表示这个容器不能用 swap;-1表示 swap 不限。docker stats只列运行中的容器。MemUsage显示用量和上限;没设上限时,上限那一栏是宿主机的总内存。把几个大户加起来和宿主机内存比一比:2 GB 的机器上跑着几个都没设上限的容器,它们抢的是同一块内存。free -h看available那一列,不要看free;顺便看看到底有没有 swap。ps按常驻内存列出宿主机上最大的几个进程,容器里的进程也在其中。
宿主机整体 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
时间窗口选在第 1 步的
finished(换算成本地时间后)或第 2 步某条die的前后。journalctl记录的是每行日志到达的时间,通常比dmesg -T换算出来的时间更可靠。dmesg只保存本次开机以来的内容,而且旧内容会被冲掉。如果宿主机重启过,sudo journalctl -k -b -1可以看上一次开机的日志,前提是 journal 配置了持久化保存。Memory cgroup out of memory: Killed process 2314 (node)表示碰到了 cgroup 的上限,一般就是容器自己的内存上限。不带“Memory cgroup”的Out of memory: Killed process …表示整台宿主机内存耗尽。较新的内核会在它前面多打一行
oom-kill::constraint=CONSTRAINT_MEMCG表示 cgroup 上限,CONSTRAINT_NONE加上global_oom表示宿主机整体。task_memcg=是被杀进程所在的 cgroup,大多数环境下路径里带着容器 ID(比如/system.slice/docker-<id>.scope或/docker/<id>)。用第三条命令拿到的 ID 前 12 位去搜。内核日志里的 PID 是宿主机上的 PID,不是容器里的 PID。
anon-rss大致是进程被杀时占着的内存。
对得上时间的内核日志一条都没有?看看有没有用户态的 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 的版本,所以下面这些是要逐一核对的可能,不是定律:
你 inspect 之前,容器已经重新启动过(见第 1 步)。
动手的是用户态的 OOM 守护进程,而不是内核。
整台宿主机内存耗尽。在 cgroup v1 上,Docker 监听的事件和容器自己的上限绑定,宿主机层面的 kill 往往不会把标记置为
true。在 cgroup v2 上,内核会按 cgroup 统计各种 OOM kill,标记更可能被置上,但较老的 containerd 和 Docker 版本在这方面有过缺陷。在 Docker Desktop 上,内存是在 Docker 的虚拟机里耗尽的,Mac 或 Windows 本机上的工具看不到。
查看宿主机用的是哪个 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. 选择下一步:由你决定的几种做法
按证据选做法。每一种都是需要你计划、并且留好回退办法的变更。
容器自己的上限到了,业务确实需要更多内存。 调大上限(Compose 里的
mem_limit或deploy.resources.limits.memory,docker run的--memory)。调之前先确认宿主机放得下:所有上限加起来,再加上系统和 Docker 之外的进程,要装得进物理内存。docker update --memory能改运行中的容器,但下次用 Compose 重建时,会回到 Compose 文件里的值。没设上限,宿主机整体耗尽。 设上限可以把一个容器的膨胀限制在它自己里面,被杀的是它,而不是旁边的数据库。如果这些服务加起来就是需要比宿主机更多的内存,现实的选择是换更大的机器,或者把部分服务挪走。
每次启动后内存都在稳定上涨。 这更像泄漏,或者没有上限的缓存、队列,调大上限只是把被杀推迟。先和最近一次部署对比,再用运行时自带的工具(堆快照、内存分析器)找出是什么在涨。服务器支持的话,让 worker 处理一定数量的请求后换成新进程,只是权宜之计,不能消除原因。
运行时的堆设置。 让运行时自己的上限低于容器上限,这样出问题时日志里会有明确的报错,而不是一个无声的 137。JVM:较新的 JDK(10 及以上、8u191 及以上)能读取容器上限,默认通常把堆限制在它的四分之一;可以用
-XX:MaxRAMPercentage或-Xmx明确指定。要留出余量,因为 metaspace、线程栈、直接内存都不在堆里。Node.js:--max-old-space-size=<MB>(比如通过NODE_OPTIONS设置);视版本而定,默认值不一定跟着容器上限走。多进程的服务(多个 Web worker、PHP-FPM 子进程)会把单个进程的内存成倍放大,worker 数量和单个进程的大小一样重要。swap,要注意代价。 宿主机上的 swap 能扛住短时间的峰值,但在持续的内存压力下,它会让一切都变慢,而不是尽快失败,还会让泄漏藏得更久。容器能不能用 swap 取决于
--memory-swap,有些内核不支持对容器限制 swap(这时docker info会打印警告)。先在不忙的机器上试。其实是停止超时,不是内存。 让应用处理 SIGTERM;
CMD/ENTRYPOINT用 exec 形式,或者在入口脚本里用exec启动应用,让它成为 PID 1;或者加一个最小的 init(--init,Compose 里的init: true)。只有当正常关闭确实需要更久时,才调大宽限期(docker stop -t、stop_grace_period)。是宿主机上别的东西发的。 用第 2 步的时间点,找出是哪个脚本、agent 或哪个人。这种情况改内存设置没有用。
在生产环境改任何东西之前,先写下:证据、要做的变更、怎么确认有效、怎么回退。
在 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。
边界
137 是线索,
OOMKilled是证据,都不是结论。 结论要同时有退出状态、Docker 事件和时间对得上的内核日志。快照看不出趋势。 判断是泄漏还是上限设小了,需要一段时间的内存历史,而不是一次
docker stats。本文的命令不会改动你的容器。 AI 以排查为主:危险命令会被拦截。内存上限、堆参数、swap、重启策略和停止设置,都是由你决定的变更。OpsMate 自己会做什么、不会做什么,以官网 常见问题 为准。
AI 的总结是排查起点,要用 inspect 输出、事件和内核日志核对。
内存压力很大时,SSH 本身也可能卡住。SSH 连不上宿主机时,OpsMate 也连不上,先去云厂商控制台处理。
说明性示例
说明性示例(不是真实客户案例):一台 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、重启策略或停止设置。如果需要变更,先给出最小步骤、风险、验证方法和回退方案,等我确认。
根据证据选择下一步
想查其他退出码(1、125–127、139、143)?看 Docker 退出码速查。
重启循环另有原因?看 Docker 容器反复重启怎么排查,里面讲了重启策略和日志时间窗口。
内存证据在实际中是什么样子:一台服务器内存升到 88% 的脱敏案例,以及这些证据不能证明什么。
想要内存历史而不是快照?看 小团队 VPS 监控清单。
试用
每月免费 500 次 AI 调用,服务器数量不限。桌面端默认把 SSH 凭证保留在本机。
需要帮助理解排查结果?
OpsMate 帮助开发者和运维人员用 AI 辅助排查,由你核对证据。点击「需要分析」后,命令输出会发送给云端 AI 分析,请先去除敏感信息。
免费开始 本地凭证桌面端