发布时间:2026-09-16 04:30 更新时间:2026-09-16 04:30 阅读量:0
容器跑着跑着突然重启,docker ps 里看到容器退出码是 137,日志最后一行没有异常堆栈,只有一个被强行掐断的现场——这多半就是被内核的 OOM Killer 干掉了。不少站长第一反应是去调大 JVM 的 -Xmx,结果内存占用反而更高、被杀得更勤。要搞清楚这件事,得先明白三件事:docker stats 显示的数字是什么、cgroup 的内存上限怎么起作用、以及 Java/Node 这类运行时实际用掉的内存和堆参数之间差在哪里。
docker stats 是最常用的入口,但它给的是 MEM USAGE / LIMIT 和 MEM%,这个 USAGE 里包含了 page cache,也就是文件读写缓存。缓存是内核可以随时回收的,所以 USAGE 逼近 LIMIT 时不一定马上出事,真正触发 OOM 的是「不可回收的匿名内存」加「内核认为来不及回收的缓存」超过上限。
要看到更细的 cgroup 数据,可以直接读容器对应的 cgroup 文件。Docker 默认在 /sys/fs/cgroup/system.slice/docker-<容器ID>.scope/(cgroup v2 环境)或 /sys/fs/cgroup/memory/docker/<容器ID>/(cgroup v1 环境)下,具体路径随发行版和 cgroup 版本不同,以实际环境为准。
# 找到容器 ID
CID=$(docker inspect -f '{{.Id}}' my-app)
cgroup v2 常见路径
cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-$CID.scope/memory.events
cgroup v1 常见路径
cat /sys/fs/cgroup/memory/docker/$CID/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/docker/$CID/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/$CID/memory.failcnt
其中 memory.events(v1 是 memory.failcnt)里的 oom_kill 计数如果大于 0,就说明这个容器确实被 OOM Kill 过,这比看退出码可靠。memory.current 是当前用量,memory.max 是上限。注意 memory.max 显示 max 表示没有限制,这时容器用的是宿主机内存,被杀往往是宿主机整体紧张导致的,排查思路不同。
以 JVM 为例,进程占用的内存大致等于:堆(-Xmx 附近)+ Metaspace + 线程栈(每个线程约 1MB,受 -Xss 影响)+ 直接内存(NIO、Netty 的 -XX:MaxDirectMemorySize)+ JVM 自身结构(代码缓存、GC 结构等)+ 本地库(JNI、glibc 等)。所以 -Xmx 绝不能等于容器上限,需要留出堆外空间。
经验做法是让 JVM 感知容器限制,而不是写死一个大值。JDK 10 之后默认开启 UseContainerSupport,堆的默认上限会按容器内存的约四分之一计算。如果显式设 -Xmx,就要自己保证它加堆外开销不超过容器上限。
# 容器上限 1G,堆给 600~700M,留出堆外空间
docker run -d --name my-app \
--memory=1g --memory-swap=1g \
-e JAVA_TOOL_OPTIONS="-Xms512m -Xmx700m -XX:MaxDirectMemorySize=128m -XX:MaxMetaspaceSize=192m" \
my-app:latest
或者不写死 -Xmx,只给比例,让 JVM 自己按容器上限算
-XX:MaxRAMPercentage=70.0
Node.js 的情况类似,默认堆上限受可用内存影响,在容器里老版本可能读不到 cgroup 限制。可以用 --max-old-space-size 显式约束堆,给堆外留余量。
# 容器上限 512M,堆给 320~384M 比较稳妥
node --max-old-space-size=384 server.js
也可以看实际生效的堆上限
node -e "console.log(require('v8').getHeapStatistics().heap_size_limit/1024/1024 + ' MB')"
两个坑要避开:一是把 -Xmx 设得接近容器上限,GC 之外的内存一涨就 OOM;二是 --memory-swap 没设或设得比 --memory 小,导致行为不符合预期——通常建议 --memory-swap 等于 --memory,即容器内不使用 swap,让超限直接暴露而不是拖慢。
第一步是确认而不是猜。容器退出码 137 也可能是 docker stop 超时被 SIGKILL,所以要看 cgroup 的 oom_kill 计数,同时用 dmesg 找内核记录(需要宿主机权限)。
# 宿主机上看 OOM 记录,含被杀进程名和内存信息
dmesg -T | grep -i -E 'oom|killed process'
持续观察容器内存曲线,确认是缓慢上涨还是瞬间飙升
docker stats --no-stream my-app
如果是缓慢上涨后被杀,多半是内存泄漏或缓存无限增长,要结合应用自身的监控看堆使用曲线、GC 频率;如果是启动后短时间内飙升,通常是堆参数过大、Metaspace 或直接内存超预期,先按上面公式重算参数。改完参数后不要只看一次,跑够一个业务周期再确认 memory.events 里的计数没有继续增加。
最后提醒一点:容器内存上限、JVM 堆上限、宿主机可用内存是三层关系,任何一层算错都会表现为「莫名其妙被杀」。把 docker stats、cgroup 文件和运行时参数三处数字对齐,再按业务峰值留出 20%~30% 余量,OOM Kill 这类问题基本就能收敛。具体到不同 JDK 版本对容器支持的差异,以官方文档和实际环境验证为准。
| 📑 | 📅 |
|---|---|
| Nginx 499 状态码排查实战:客户端断连与 upstream 超时的区别 | 2026-09-16 |
| 服务器定时任务实战:crontab 语法与不生效排查 | 2026-09-15 |
| MySQL慢查询日志开启与优化入门 | 2026-09-15 |
| Docker Compose 部署多容器应用实战:环境变量、数据卷与重启策略配置要点 | 2026-09-15 |
| Nginx 日志按天切割与过期清理:logrotate 实战 | 2026-09-15 |
| MySQL 主从延迟排查:Seconds_Behind_Master 忽高忽低怎么定位 | 2026-09-16 |
| rsync 增量同步与断点续传实战:exclude、--delete 与限速 | 2026-09-16 |
| Nginx 后端真实 IP 获取:X-Forwarded-For 与 real_ip 配置 | 2026-09-16 |
| Linux文件句柄耗尽排查:Too many open files 从ulimit到systemd | 2026-09-16 |
| PostgreSQL 连接数与内存调优:max_connections 与 shared_buffers 怎么配 | 2026-09-16 |