容器跑了两天,啪——退出码 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)
如果 OOMKilled 是 false 但退出码是 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 都查清楚,别吞了。