Docker 容器随机重启、退出码 137(OOM 被 kill)

容器以退出码 137 重启。OOM killer 撞上了 --memory 上限。定位泄漏、profile 堆、设合理上限、止血。

容器跑了两天,啪——退出码 137 重启。日志半句话戛然而止,没异常、没栈。退出码 137 是 128 + 9 = SIGKILL,容器里几乎只有一种来源:内核 OOM killer 撞上了你的 --memory 上限。要么是上限设太紧、撑不住正常流量;要么是进程在漏、给多少都迟早 OOM。修法:先确认是 OOM、profile 后再决定要不要提高上限、用堆 dump 找真正的泄漏、加护栏防止悄无声息复发。

常见原因

按踩坑频率排序。

1. 上限对正常峰值太紧

你从某个 Helm chart 抄了 --memory=256m,实际工作集峰值 400 MB。每次流量上来就 OOM。

怎么判断docker inspect 显示 Memory: 268435456;流量峰值时 RSS 贴着上限。

2. 应用代码慢泄漏

进程 RSS 几小时或几天一路涨,最后撞上限。重启周期变得很有规律(比如每 36 小时一次)。

怎么判断docker stats 时序图是单调上扬。

3. 进程内无界缓存

Map/dict 当缓存、没有 LRU。每个新 key 都长一条。内存无止境涨。

怎么判断:堆 dump 显示一个 Map 实例几十万条 entry。

4. 没上限的连接池

ORM 每个请求新建连接、不归还。每条连接吃几 MB,并发越高内存越大。

怎么判断:指标里池大小超过文档上限;堆里有大量 DB driver 对象。

5. 原生内存分配,堆 profiler 看不到

Node Buffer、Python NumPy 数组、Go cgo 分配——这些往往语言层 profiler 看不见,但 OS 看得清清楚楚。

怎么判断:语言堆很健康,但 RSS 翻倍。

最短修复路径

Step 1: 确认是 OOM killer

docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' my-container
# 期望:true 137

# 内核日志
dmesg | grep -i 'oom\|killed process'
# 找:Memory cgroup out of memory: Killed process 1234 (node)

如果 OOMKilledfalse 但退出码是 137,那是别的东西 SIGKILL 了——orchestrator liveness probe、手动 kill、或者别的内核原因。

Step 2: 看清当前上限和实际占用

docker stats --no-stream my-container
# MEM USAGE / LIMIT,比如 245.3MiB / 256MiB

# 容器里
cat /sys/fs/cgroup/memory.max          # cgroup v2
cat /sys/fs/cgroup/memory.current

Kubernetes:

kubectl top pod my-pod
kubectl describe pod my-pod | grep -A2 -i memory

Step 3: profile 堆

Node.js——拍堆快照,在 Chrome DevTools 里看。

# 启动时开 --inspect
node --inspect=0.0.0.0:9229 server.js

# 或者进程内主动拍快照
node -e "require('v8').writeHeapSnapshot('/tmp/heap.heapsnapshot')"

按 retained size 看大头。常见嫌疑:缓存、没移除的 EventEmitter listener、闭包拽着大数组。

Python——tracemalloc

import tracemalloc
tracemalloc.start(25)

# ... 跑一会儿 ...

snap = tracemalloc.take_snapshot()
for stat in snap.statistics('lineno')[:20]:
    print(stat)

或者 memray,火焰图直观:

pip install memray
memray run -o out.bin my_app.py
memray flamegraph out.bin

Go——pprof:

import _ "net/http/pprof"
go func() { http.ListenAndServe(":6060", nil) }()
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap

Step 4: 设合理上限、留 buffer

limit = p99_RSS * 1.5 来。Node 还要顺手限 V8。

# docker-compose.yml
services:
  api:
    image: my/api
    deploy:
      resources:
        limits:
          memory: 768M
    environment:
      NODE_OPTIONS: "--max-old-space-size=512"

Kubernetes 同时设 request 和 limit:

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "768Mi"

requests 调度用,limits OOM cutoff。limits 控制在 requests 的 1.5 倍内,避免 noisy neighbor。

Step 5: 修泄漏、不只提上限

如果 RSS 不管流量都单调上扬,提上限只是推迟下一次 OOM。常见修法:

  • 无界 Map 缓存换成 LRU(Node 用 lru-cache、Python 用 cachetools.LRUCache)。
  • DB 池设上限,确认错误路径上连接也归还。
  • 关闭时移除 EventEmitter listener;长连 socket 限制 setMaxListeners
  • Buffer.alloc() 池复用,别每次请求都新分配大 Buffer。

Step 6: 加护栏

RSS 超过 80% 上限就告警——别等 OOM 才发现泄漏。

# Prometheus 规则
- alert: ContainerNearOOM
  expr: container_memory_working_set_bytes / container_spec_memory_limit_bytes > 0.85
  for: 10m
  labels:
    severity: warning

重启次数告警捕捉静默 OOM 重启循环:

- alert: PodRestartLoop
  expr: increase(kube_pod_container_status_restarts_total[1h]) > 3
  for: 0m

预防

  • 每个容器都有内存上限;85% 处告警。
  • 至少观察一周 p99 RSS 再定上限。
  • 缓存和池都有显式上限。
  • 镜像里带堆 profile 工具(或 sidecar 可挂载),生产能 dump。
  • 每个服务跟重启次数,每次 OOM 都查清楚,别吞了。

标签: #后端 #排查 #docker