Docker容器退出码137是什么意思?内存限制与进程终止排查

容器跑着跑着突然没了,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 之后,按这个思路预防:

  1. 先量后限。不要拍脑袋给内存,先用监控(cAdvisor、Prometheus、docker stats)观察一周的峰值,再设限制。实时查看:
docker stats --no-stream
  1. 留出余量。容器内存限制建议设为峰值内存的 1.3 到 1.5 倍,但不超过宿主机可用内存的合理份额。

  2. 监控 OOM 事件。把 dmesg 或 Prometheus 的 container_oom_events_total 指标接入告警,OOM 一发生就能收到通知,而不是等用户投诉。

  3. 区分语言特性调参。JVM 用 MaxRAMPercentage,Node 用 --max-old-space-size,Python 注意大批量数据处理的内存峰值。

  4. 避免过度超售。宿主机上所有容器 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」把限制设得极大,那会破坏集群稳定性。

总结

  1. 退出码 137 = 128 + 9,即进程被 SIGKILL,最典型的原因是内存超限触发 OOM Killer。
  2. 用 docker inspect --format '{{.State.OOMKilled}}' 一票判定是否 OOM,再用 dmesg -T | grep -i oom 拿内核证据。
  3. 分清「容器 cgroup 超限」和「宿主机全局 OOM」,前者调容器限制,后者要扩容或降低密度。
  4. OOMKilled=false 的 137 通常来自 docker stop 超时强制 SIGKILL 或手工 kill,重点检查应用的 SIGTERM 处理。
  5. 内存限制要靠监控数据来定,留 30% 到 50% 余量;JVM/Node 等语言要显式配置容器感知的内存参数,并给 OOM 事件配告警。