上周一位读者遇到个头疼的问题:他的 VPS 上跑了 5 个 Docker 容器,服务器 4G 内存,莫名其妙就触发 OOM(Out of Memory),系统直接杀掉容器进程。更诡异的是,他明明给每个容器都加了内存限制。今天把整个排查过程复盘一遍,教你遇到 OOM 时如何快速定位根因。
一、先确认:到底是谁在吃掉内存
OOM 发生时,系统日志里会有记录。先看 dmesg:
dmesg | grep -i "killed process"
输出类似这样:
Out of memory: Killed process 12345 (java) total-vm:2.5GB, anon-rss:1.8GB
这里能看到被杀死的是 PID 12345 的 Java 进程,用了 1.8G 实际内存。
但 Docker 容器里的进程 PID 是宿主机上的,怎么对应到具体容器?用这个命令:
# 查看所有容器的内存使用
docker stats --no-stream
# 输出示例
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
abc123def456 web-app 12.5% 1.8GiB / 2GiB 90.0%
def789ghi012 redis 0.3% 512MiB / 512MiB 100.1%
如果 MEM % 接近或超过 100%,说明这个容器触发了内存限制,但问题可能不止于此。
二、深入排查:容器内到底什么在占内存
进入容器内部,用 top 或 ps 看进程内存:
docker exec -it web-app /bin/sh
top -o %MEM
如果发现某个进程内存持续增长,可能是内存泄漏。但 Docker 容器里还有个隐藏杀手——缓存和缓冲区。
容器内的 free -m 看到的内存是宿主机的,不准确。正确做法是在宿主机上看:
# 查看容器 cgroup 内存统计
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.stat
# 关键指标
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
重点关注 cache 和 rss(常驻内存)。如果 cache 很大,说明是文件缓存占用了内存,加 --memory-swap 限制可能有效。
三、常见内存泄漏场景与修复
场景 1:Java 应用堆内存设置过大
很多 Java 容器启动参数里 -Xmx 设得比容器内存限制还大:
# 错误示例:容器限制 1G,但 JVM 堆设了 1.5G
docker run -m 1g my-java-app -Xmx1536m
JVM 不知道自己在容器里,会按宿主机的内存来分配。正确做法是用 -XX:+UseContainerSupport(Java 8u191+ 默认开启)或显式设置:
docker run -m 1g -e JAVA_OPTS="-XX:MaxRAMPercentage=75.0" my-java-app
这样 JVM 会自动按容器内存的 75% 设置堆上限。
场景 2:Nginx/PHP-FPM 进程数失控
PHP-FPM 的 pm.max_children 如果设得太大,并发一上来内存就爆:
; php-fpm.conf
pm = dynamic
pm.max_children = 50 ; 如果每个子进程 50MB,50 个就是 2.5G
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
根据容器内存调整:
pm.max_children = 10 ; 2G 内存的容器,10 个子进程比较安全
场景 3:Redis 没有设置 maxmemory
Redis 默认会吃掉所有可用内存:
# redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru # 内存满了淘汰最近最少使用的 key
场景 4:日志文件在容器内无限增长
如果应用把日志写到容器内的文件系统,而没做切割,日志文件会撑爆容器存储层(虽然存储层和内存是分开的,但某些情况下会影响整体性能):
# 更好的做法:日志输出到 stdout/stderr,让 Docker 管理
# 或者挂载宿主机目录,配合 logrotate
docker run -v /var/log/myapp:/app/logs my-app
四、给容器加正确的内存限制
很多人只加 -m,但忽略了 swap:
# 只限制内存,不限制 swap(容器可能用 swap 规避限制)
docker run -m 512m my-app
# 正确做法:内存和 swap 一起限制
docker run -m 512m --memory-swap 512m my-app # 禁用 swap
# 或
docker run -m 512m --memory-swap 768m my-app # 允许 256m swap
在 docker-compose.yml 中:
services:
web:
image: my-app
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
五、监控与告警
亡羊补牢不如未雨绸缪,加个简单的监控脚本:
#!/bin/bash
# /usr/local/bin/docker-memory-check.sh
THRESHOLD=90
ALERT_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
docker stats --no-stream --format "{{.Name}}\t{{.MemPerc}}" | while read name mem; do
mem_num=$(echo $mem | sed 's/%//')
if (( $(echo "$mem_num > $THRESHOLD" | bc -l) )); then
curl -s -X POST $ALERT_WEBHOOK \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"告警:容器 $name 内存使用率 $mem_num%,请检查!\"}}"
fi
done
加到 crontab 每 5 分钟执行一次:
*/5 * * * * /usr/local/bin/docker-memory-check.sh
六、总结
Docker OOM 排查三步走:
- 看日志:
dmesg+docker stats定位肇事容器 - 进容器:
top/ps找内存大户,检查应用配置(JVM、PHP-FPM、Redis) - 加限制:
-m和--memory-swap一起用,配合监控告警
内存泄漏不可怕,可怕的是不知道怎么查。收藏这篇,下次遇到 OOM 不慌张。





