容器跑着跑着突然没了,docker ps -a 一看状态是 Exited (137)。这个数字不是随便来的:137 = 128 + 9,代表进程收到了信号 9,也就是 SIGKILL。SIGKILL 是「强杀」,无法被进程捕获或忽略,所以 137 通常意味着有人从外部把进程打死了——最常见的凶手是 Linux 内核的 OOM Killer,其次可能是 docker stop 超时或被手工 docker kill。
新手常把它当成「程序自己崩溃」,去翻应用日志却什么都看不到,因为进程是被内核直接干掉的,应用根本没机会写日志。所以要解决 137,重点不在应用代码,而在内存、cgroup 限制和终止方式上。
下面从退出码含义讲起,带你分清「容器内存超限被杀」和「宿主机内存不足被杀」两种场景,给出真实的排查命令、docker inspect 的关键字段解读,以及如何通过调整内存限制和 OOM 处理策略来根治问题。
一、退出码 137 到底代表什么
Docker 容器的退出码遵循 Unix 约定:如果是被信号终止,退出码 = 128 + 信号编号。
| 退出码 | 信号 | 含义 | 常见原因 |
|---|---|---|---|
| 0 | 无 | 正常退出 | 主进程正常结束 |
| 1 | — | 应用错误 | 程序抛异常/配置错 |
| 137 | 9 (SIGKILL) | 被强杀 | OOM、内存超限、docker kill |
| 139 | 11 (SIGSEGV) | 段错误 | 程序非法内存访问 |
| 143 | 15 (SIGTERM) | 优雅终止 | docker stop 正常超时前退出 |
| 126/127 | — | 无法执行 | 入口命令/权限问题 |
所以看到 137,第一反应是「谁发了 SIGKILL」。这个「谁」基本是两类:
1. 容器 cgroup 内存超限,内核在容器内部触发 OOM Killer,杀掉容器主进程;
2. 宿主机物理内存耗尽可能触发全局 OOM,或运维执行了 docker kill。
docker stop 默认先发 SIGTERM,等待 10 秒后若进程没退出,就补一个 SIGKILL——这种情况下也会得到 137。所以如果应用不处理 SIGTERM 信号,docker stop 超过宽限期就会变成 137,这属于「优雅退出没做好」。
二、先确认是不是 OOM
最快的判断方式是 docker inspect:
docker inspect <容器名> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
如果输出 137 true,基本可以确定是容器被 OOM Killer 干掉了。
docker inspect 里几个关键字段:
docker inspect <容器名> | grep -A5 '"State"'
其中 OOMKilled: true 是决定性证据。再看内存限制:
docker inspect <容器名> --format 'Memory(limit)={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
Memory 就是 docker run -m 设置的上限,单位是字节。比如 -m 512m 会显示为 536870912。
宿主机层面也能验证内核日志,这是最硬的证据:
# 查看内核 OOM 记录
dmesg -T | grep -i -E "oom|killed process"
# systemd 系统可用 journalctl
journalctl -k --since "1 hour ago" | grep -i oom
典型输出会包含 Out of memory: Killed process 12345 (node) 这样的行,说明内核杀了哪个进程,以及当时的可用内存。
三、区分「容器超限」和「宿主机内存不足」
这两种 OOM 的处置方式完全不同,必须分清。
场景 A:容器内存超限(cgroup OOM)
容器设了 -m 512m,进程实际用到 512MB 就被容器自己的 cgroup 拦住,内核只在容器范围内触发 OOM。表现:OOMKilled=true,宿主机整体内存可能还很宽裕。
场景 B:宿主机内存不足(全局 OOM)
整台机器的物理内存被多个容器加宿主机进程吃光,内核从全局选一个进程杀。表现:OOMKilled 也可能是 true,但宿主机 free -h 显示内存告急,dmesg 里能看到全局内存耗尽。
判断命令:
free -h
# 关注 available 列,而不是 free 列
free -h 里的 available 才是「真正可分配」的内存,包含可回收的 page cache。若 available 长期很低,说明要为整机扩容或减少容器密度。
四、给容器设置合理的内存限制
跑容器时用 -m 限制内存,是个好习惯,但设得太小反而会把正常应用逼死。启动示例:
docker run -d \
--name app \
-m 512m \
--memory-swap 512m \
--oom-kill-disable=false \
myapp:1.0
参数解释:
- -m 512m:容器最多用 512MB 物理内存;
- --memory-swap 512m:与 -m 相等,表示不允许使用 swap(若设为 -1 则可用无限 swap,通常不推荐);
- --oom-kill-disable=false:允许 OOM Killer 终止容器,保持默认即可,不要轻易设为 true。
内存量级参考(经验值):
- 一个常驻的 Java 服务,堆内存 1GB 时,容器整体预留建议 1.5 到 2GB,因为堆外内存、Metaspace、线程栈都要额外占用;
- Node.js 单实例常驻内存在几十 MB 到几百 MB 之间,取决于负载,-m 512m 是常见起点;
- Nginx/PHP-FPM 这类,按 worker 数量乘以单 worker 内存估算,留 30% 余量。
注意 JVM 的一个大坑:老版本 JVM 默认不识别 cgroup 内存限制,会按宿主机总内存设置堆大小,导致容器频繁被 OOM。现代 JDK(8u191+、11+)支持容器感知,建议显式指定:
docker run -d -m 1g -e JAVA_OPTS="-XX:MaxRAMPercentage=70.0" myjava:1.0
五、Docker Compose 与 Kubernetes 中的内存配置
Compose 里对应的是 deploy.resources:
services:
app:
image: myapp:1.0
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
limits 是硬上限,超过会被 OOM;reservations 是调度预留,保证有这么多可用。
Kubernetes 里则是 resources.limits 和 requests:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
需要特别注意:K8s 里如果只设 limits 不设 requests,requests 默认等于 limits,会导致调度利用率低下;反过来,如果 memory 的 requests 和 limits 相差过大,Pod 可能被调度到内存紧张节点上被 OOMKill。经验做法是老练的运维会把 min(requests)设得接近实际常驻内存,按经验留 20% 到 40% 余量,然后再设置一个 1.5 到 2 倍的 limits 作为峰值上限。
查看 Pod 是否被 OOM:
kubectl describe pod | grep -A3 "Last State"
kubectl get pod -o jsonpath='{.status.containerStatuses[*].lastState}'
若看到 "reason": "OOMKilled",就确认是内存超限了。
六、非 OOM 的 137:优雅退出与手工 kill
如果 OOMKilled=false 却是 137,就要往信号方向想。
docker stop 超时。默认 docker stop 给 10 秒宽限期:
# 显式指定 30 秒宽限期
docker stop -t 30 <容器名>
如果应用不响应 SIGTERM,10 秒后仍被 SIGKILL,退出码就是 137。改进方式是让应用正确处理 SIGTERM(Java 用 Runtime.addShutdownHook,Go 用 signal.NotifyContext,Nginx 用 nginx -s quit)。K8s 里对应 terminationGracePeriodSeconds:
spec:
terminationGracePeriodSeconds: 30
手工 docker kill。有人直接 docker kill 就必然是 137,这属于人为操作,日志里会体现。
排查历史:
docker inspect <容器名> --format '{{json .State}}' | python3 -m json.tool
State 里有 StartedAt、FinishedAt、ExitCode、OOMKilled、Error。
七、复盘与预防
定位到 137 之后,按这个思路预防:
- 先量后限。不要拍脑袋给内存,先用监控(cAdvisor、Prometheus、
docker stats)观察一周的峰值,再设限制。实时查看:
docker stats --no-stream
-
留出余量。容器内存限制建议设为峰值内存的 1.3 到 1.5 倍,但不超过宿主机可用内存的合理份额。
-
监控 OOM 事件。把
dmesg或 Prometheus 的container_oom_events_total指标接入告警,OOM 一发生就能收到通知,而不是等用户投诉。 -
区分语言特性调参。JVM 用
MaxRAMPercentage,Node 用--max-old-space-size,Python 注意大批量数据处理的内存峰值。 -
避免过度超售。宿主机上所有容器 limits 之和如果远超物理内存,一旦同时达到峰值就会触发全局 OOM,殃及别的容器。
八、把 137 当成信号,而不是当成结论
最后强调一个观念:137 只是「进程被 SIGKILL」这个事实,不是根因。拿到 137 之后,真正要做的是回答两个问题——谁发的 SIGKILL,为什么发。
回答第一问靠 OOMKilled 字段和内核日志;回答第二问要结合业务上下文。同样是 OOM,有的应用是内存真的不够(配置偏小),有的是内存泄漏(曲线持续上升),有的是突发流量瞬间打满(峰值超预期)。三者的处置完全不同:第一个调限制,第二个修代码,第三个要么加资源要么加限流。如果只看到 137 就盲目加大内存限制,很可能只是把崩溃时间往后推,问题并没有解决。
另外要建立「留痕」意识。容器被 SIGKILL 后现场就没了,事后进去看什么都看不到。所以生产环境必须提前把内存监控、OOM 事件告警和集中日志配好,让每一次 137 都留下可追溯的记录。等出了事故再去翻 dmesg,往往已经错过了关键窗口——内核日志环形缓冲区容量有限,旧的记录会被新日志冲掉。把这些基础观测手段做在前面,137 就从「神秘崩溃」变成了「有据可查的资源问题」。
常见问题
Q1:容器被 OOM 杀,应用日志为什么没有报错?
A:SIGKILL 不给进程任何处理机会,日志不会留下「为什么被杀」。必须靠 docker inspect 的 OOMKilled 字段和内核 dmesg 来判断。
Q2:给容器加大内存限制就一定能解决吗?
A:不一定。如果是程序内存泄漏,加限制只是延后死亡时间。应该先用监控确认内存是持续增长(泄漏)还是到达峰值后被杀(配置偏小),前者要修代码,后者才调限制。
Q3:OOMKilled=false 但退出码是 137,怎么查?
A:大概率是 docker stop 超时后 SIGKILL,或有人手工 docker kill。检查应用是否处理 SIGTERM,并适当加大 docker stop -t 或 K8s 的 terminationGracePeriodSeconds。
Q4:容器设置 --oom-kill-disable=true 会怎样?
A:容器内进程不会被 OOM Killer 杀,但可能导致宿主机内存耗尽引发更大的故障,强烈不建议在生产使用。
Q5:K8s 里 Pod 反复 OOMKilled 重启,怎么快速缓解?
A:短期提高 limits.memory 或降低负载;中期接入监控定位内存峰值来源;长期排查是否有泄漏或缓存无上限。不要单纯为了「不 OOM」把限制设得极大,那会破坏集群稳定性。
总结
- 退出码 137 = 128 + 9,即进程被 SIGKILL,最典型的原因是内存超限触发 OOM Killer。
- 用
docker inspect --format '{{.State.OOMKilled}}'一票判定是否 OOM,再用dmesg -T | grep -i oom拿内核证据。 - 分清「容器 cgroup 超限」和「宿主机全局 OOM」,前者调容器限制,后者要扩容或降低密度。
OOMKilled=false的 137 通常来自docker stop超时强制 SIGKILL 或手工 kill,重点检查应用的 SIGTERM 处理。- 内存限制要靠监控数据来定,留 30% 到 50% 余量;JVM/Node 等语言要显式配置容器感知的内存参数,并给 OOM 事件配告警。
企业QQ咨询




