胖叔网络科技 网络科技 · 技术笔记
部署运维

Docker 容器内存限制与 OOM 排查实战

容器被 OOM Killer 干掉时往往只有一句 "Killed",没有堆栈没有日志。这篇文章讲清楚怎么提前设限、怎么定位真凶。

# Docker# OOM# 内存# 故障排查

#OOM 的典型症状

容器突然消失,docker ps 里看不到,日志最后一行只有:

CODE
Killed

没有堆栈、没有异常、没有退出码解释。 这是 OOM Killer 的典型特征 —— 进程被内核直接发信号杀掉,来不及做任何收尾。

#先分清是哪种 OOM

这里有三个不同层面的内存限制,排查时必须分清

层面 限制来源 症状
容器级 docker run -m / compose 的 mem_limit 容器内进程被杀,容器重启
系统级 宿主机物理内存耗尽 任意进程被杀,dmesg 有记录
cgroup 级 内核 cgroup 内存限制 与容器级类似

判断方法:

Bash
# 看内核有没有杀进程的记录
dmesg | grep -i -E 'oom|killed process'

# 输出示例:
# [12345.678] Out of memory: Killed process 12345 (node) total-vm:1234567kB
# [12345.678] oom_reaper: reaped process 12345 (node)

如果 dmesg 里有记录,说明确实是 OOM。如果没有,那问题在别处(比如应用自己崩溃、容器健康检查失败被重启)。

#关键指标:看 available 而不是 free

这是最容易误判的一点:

Bash
free -h
#               total        used        free      shared  buff/cache   available
# Mem:          982Mi       399Mi       138Mi        10Mi       444Mi       432Mi

free 列的 138 MB 会让人以为"内存只剩 138 MB 了",但 buff/cache 的 444 MB 是可回收的文件缓存。真正决定会不会 OOM 的是 available 列的 432 MB。

内核视角的更精确数据:

Bash
grep -E 'MemTotal|MemAvailable|Committed_AS|CommitLimit' /proc/meminfo
  • MemAvailable:不触发 swap 的前提下还能分配多少
  • Committed_AS:所有进程已承诺(但未必实际使用)的内存总量
  • CommitLimit:允许承诺的上限

如果 Committed_AS 接近 CommitLimit,即使 available 看起来很充足,也可能随时 OOM。 这是内存超卖场景下的典型陷阱。

#怎么给容器设合理的内存限制

#原则一:一定要设上限

不设限制的容器会吃掉宿主机所有可用内存,然后拖垮同机器的其他服务。

YAML
# docker-compose.yml
services:
  api:
    image: my-api:latest
    deploy:
      resources:
        limits:
          memory: 256M
        reservations:
          memory: 128M

或者命令行:

Bash
docker run -d --memory=256m --memory-swap=256m my-api:latest

--memory-swap 要和 --memory 设成一样,表示禁用 swap。否则容器会把内存压力转成 swap 交换,表现为"服务还在但慢得不能用",比直接 OOM 更难排查。

#原则二:留出安全余量

不要按"实测峰值"设限制,要留 30%–50% 余量:

应用类型 实测峰值 建议限制
Node.js 轻服务 110 MB 256 MB
Node.js + 构建 400 MB 768 MB
Nginx 30 MB 128 MB
MySQL 380 MB 640 MB

Node.js 的 V8 堆内存和 RSS 是两回事。 V8 默认堆上限在 64 位系统上约 2 GB,容器限制为 256 MB 时,V8 并不知道这个限制,可能在堆远未达到上限时 RSS 就超了。

解决办法是显式告诉 V8:

Bash
NODE_OPTIONS="--max-old-space-size=180" node server.js

180 MB 的堆上限配合 256 MB 的容器限制,是留了合理余量的组合。

#原则三:把限制写进编排文件

临时 docker run 设的参数,下次重建就丢了。写进 compose 或 Dockerfile 才持久。

#排查流程

按这个顺序走,基本能定位到真凶:

Bash
# 1. 确认是否真的 OOM
dmesg | grep -i oom | tail -20

# 2. 看容器当前内存占用
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'

# 3. 看容器退出码(137 = 128 + 9,即被 SIGKILL)
docker inspect <container> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'

# 4. 与宿主机 ps 交叉验证
ps -eo pmem,rss,comm,args --sort=-rss | head -12

第 3 步的 OOMKilled 字段是最直接的证据,返回 true 就实锤了。

第 4 步很重要docker stats 的统计口径包含 page cache,而宿主机 ps 的 RSS 不含。两者差距很大时,说明容器在大量读写文件 —— 这种情况下降的是 cache,实际内存压力没那么大。

#常见误判

#误判一:把 cache 当成泄漏

Node 进程 RSS 缓慢上涨然后趋于平稳,这通常是 V8 的垃圾回收策略,不是泄漏。

判断方法:观察 24 小时,如果 RSS 在高位稳定震荡而不是持续线性上涨,就是正常的。

真泄漏的特征是:每次请求后 RSS 都上一个台阶,且不回落。

#误判二:忽略子进程

Node 的 child_process、PM2 的 cluster 模式,都会让子进程的内存不计入父进程

Bash
# 看整个进程树
ps -eo pid,ppid,rss,comm --forest | grep -A5 node

#误判三:忘记 swap 的影响

Bash
swapon --show
cat /proc/sys/vm/swappiness

swappiness 默认 60,意味着内核比较积极地使用 swap。对延迟敏感的服务建议调低:

Bash
sysctl -w vm.swappiness=10
# 持久化
echo 'vm.swappiness=10' >> /etc/sysctl.conf

但要注意:swappiness=0 不是"禁用 swap",而是"尽量不用"。 内存极度紧张时仍然会 swap,甚至可能触发 OOM。

#相关资源

以下资料可直接下载:

SH

服务器只读巡检脚本

一条命令采集系统、CPU、内存、磁盘、Docker、端口与防火墙信息,不修改任何系统状态,适合部署前体检。

server-inspect.sh 1.4 KB 82 次下载

#小结

环节 关键动作
预防 容器必须设 --memory,并配 NODE_OPTIONS
判断 dmesg + docker inspect .State.OOMKilled
定位 docker stats 与宿主机 ps 交叉看
优化 swappiness,禁用容器 swap

核心认知就一句:free 列的数值会骗人,MemAvailableCommitted_AS 才反映真实压力。