容器内存超限被 OOM Kill 排查:指标、cgroup 与堆参数对应

    发布时间: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