一、事故现场:好端端的服务器,怎么就“凝固”了?
💡 推荐阅读:WordPress 用 Docker Compose 一键部署总报错?这 4 个高频坑和排查思路,我帮你踩平了
相信不少用 Docker 跑生产环境的朋友,都经历过这种让人后背发凉的瞬间:SSH 连不上,ping 得通但响应极慢,网站打不开,连阿里云/腾讯云的控制台 VNC 都像幻灯片一样卡顿。等你好不容易重启完机器,登上服务器一看,free -m 显示 Swap 爆满,dmesg 里全是 Out of memory: Kill process 的记录。
这种“卡死”的元凶,十有八九就是某个容器里的进程把内存吃光了。笔者作为常年跟 Docker 打交道的站长,今天就把这套排查思路和实操命令,完完整整地分享给大家。这不是什么高深的理论,而是能直接救命的“战场手册”。
二、动手前的准备:你得有这些“武器”
💡 延伸阅读:反向代理的终极形态:用 Docker 优雅落地 Nginx Proxy Manager,这才是内网穿透与多站点管理的正确姿势
在开始排查之前,请确保你的服务器满足以下条件,否则后面很多命令会执行不了:
- 服务器环境: Linux 系统(CentOS 7.x / Ubuntu 20.04+ 均可),并且你拥有 root 或具备 sudo 权限的账号。
- Docker 版本: 建议 19.03 及以上版本,因为旧版本对 cgroup 内存统计的支持不够完善。
- 必备工具: 系统自带的
top、ps、free,以及 Docker 自带的docker stats命令。如果没装,请先执行yum install -y procps-ng或apt install -y procps。
温馨提示: 如果你用的是 1核2G 的小内存机器,建议先买台高配点的服务器再折腾,不然排查过程中机器再次卡死,心态容易炸。笔者目前用的腾讯云 4C8G 的机器,跑容器集群才稍微安心点。
三、核心排查实战:五步定位“内存杀手”
💡 深度技术指南:云服务器公网IP和私网IP傻傻分不清?端口配置踩坑实录,手把手教你一次搞定
第一步:先看宿主机整体水位,确认是不是真的内存耗尽
别急着进容器里翻,先看一眼宿主机全局状态。如果连 docker ps 都敲不动,说明系统已经处于极端饥饿状态。
# 查看内存和 Swap 使用情况
free -h
# 查看系统负载和 CPU 队列
top -bn1 | head -20
如果看到 Mem 那一行的 available 值极低,且 Swap 的 used 值很高,基本可以实锤是物理内存不够用了。这时候别慌,赶紧进入第二步,把“罪魁祸首”揪出来。
第二步:使用 docker stats 动态监视容器内存实时占用
这是最直观的一步。docker stats 会实时刷新所有运行中容器的 CPU、内存、网络 IO 情况,而且不需要进入容器内部。
# 实时刷新所有容器的资源占用,按内存排序查看
docker stats --format "table {{.Name}}\t{{.Container}}\t{{.MemUsage}}\t{{.MemPerc}}"
如果容器太多刷屏太快,建议加个 --no-stream 参数,只取当前一瞬间的快照:
# 获取一次性快照,避免持续刷屏
docker stats --no-stream --format "table {{.Name}}\t{{.Container}}\t{{.MemUsage}}\t{{.MemPerc}}" | sort -k3 -h
看到哪个容器的 MEM USAGE 数值异常偏高(比如占了几个 G),或者 MEM % 超过了 80%,那它大概率就是元凶。
第三步:深入容器内部,定位具体的“内存黑洞”进程
光知道哪个容器不行,我们还得知道是容器里的哪个进程在疯狂吃内存。这时候需要用到 nsenter 或者直接 docker exec 进入容器。
# 进入目标容器(假设容器名为 killer-app)
docker exec -it killer-app /bin/bash
# 在容器内部查看进程内存占用排行
ps aux --sort=-%mem | head -20
如果容器里没有 ps 命令,可以用 cat /proc/meminfo 或者直接看宿主机的 /proc 文件系统。更高级一点,我们可以用 nsenter 直接进入容器的命名空间查看:
# 获取容器的 PID
docker inspect -f '{{.State.Pid}}' killer-app
# 使用 nsenter 进入该 PID 的进程命名空间
nsenter -t 12345 -p -m -- ps aux --sort=-%mem | head -20
如果看到类似 java、node、python 这类进程的 %MEM 值达到了 90% 以上,那么基本可以断定是应用本身有内存泄漏,或者是 JVM 堆内存设置过大。
第四步:查看容器内存 cgroup 限制与真实占用(关键一步)
有时候 docker stats 显示的内存占用并不完全准确,因为 Docker 默认的 stats 命令统计的是 cgroup 的 memory.usage_in_bytes,这包含了页面缓存(Page Cache)。我们需要查看更精确的 memory.stat 文件,区分出 匿名内存(AnonPages) 和 页缓存(Page Cache)。
# 找到容器的 cgroup 路径
CGROUP_PATH=$(docker inspect -f '{{.HostConfig.CgroupParent}}' killer-app)
# 或者直接使用
cat /sys/fs/cgroup/memory/docker/<容器完整ID>/memory.stat | head -20
重点关注 total_anon(匿名内存,即真正被进程占用的内存)和 total_cache(文件缓存,系统可回收)。如果 total_anon 接近你设置的 Memory limit,那就是真的内存不足了。
同时,检查一下是否设置了内存限制:
# 查看容器的内存限制
docker inspect killer-app | grep -A 5 "Memory"
如果这里显示 Memory 为 0,说明容器没有设置内存上限,这意味着它可以无限吞噬宿主机内存,直到触发 OOM Killer 把宿主机干趴下。
第五步:查看系统日志,确认 OOM Killer 的击杀记录
如果机器已经卡死过,重启后一定要看日志,确认是不是内核 OOM 机制杀了进程,以及杀了谁。
# 查看内核日志中的 OOM 记录(如果被 systemd 覆盖,看 journalctl)
dmesg | grep -i -E "killed process|out of memory" | tail -20
# 或者查看系统日志
journalctl -k | grep -i -E "oom|killed process" | tail -20
你会看到类似这样的日志:
java: invoked oom-killer: gfp_mask=0x100cca, order=0, oom_score_adj=0
Killed process 12345 (java), total-vm: 4194304kB, anon-rss: 3072000kB, file-rss: 0kB
这就彻底实锤了:Java 进程因为内存耗尽被内核强杀,同时因为内存耗尽,系统为了回收内存,导致整个宿主机陷入僵死状态。
四、避坑清单:这些坑我替你踩过了
排查完问题,我们得总结一下,避免下次再掉进同样的坑里。以下是笔者多年运维生涯中总结的血泪经验:
- 坑一:永远不要在生产环境不加
-m限制就跑容器。 这是最致命的错误。Docker 默认是不限制容器内存的,一旦容器里的应用有内存泄漏,它能把整台物理机拖垮。务必在docker run或docker-compose.yml中设置mem_limit。 - 坑二:盲目信任
docker stats的数值。 就像前面说的,它包含了缓存。有时候看到占用 5G,实际匿名内存只有 1G,虚惊一场。一定要去看memory.stat里的total_anon。 - 坑三:忽略了 Swap 的配置。 如果宿主机本身 Swap 很小或者没有,内存一紧张,内核会直接 OOM,而不是先换页。建议至少配置 2G 的 Swap 作为缓冲,但注意,容器内默认是看不到 Swap 的,需要在
docker run时加--memory-swap参数。 - 坑四:Java 应用不设置堆内存上限。 如果是 Java 应用,JVM 默认堆大小是物理内存的 1/4,这在容器里是致命的。一定要在启动参数里显式指定
-Xmx和-Xms。 - 坑五:日志文件无限增长。 有时候不是进程本身内存高,而是容器内应用疯狂打印日志,导致
stdout文件占满磁盘,进而引发诡异问题。记得配置--log-opt max-size=10m限制日志大小。
五、终极解决方案与预防措施
找到问题只是第一步,我们还需要进行治理。针对上述排查结果,给出以下解决方案:
1. 立即止血(针对正在卡死的机器)
如果现在还能敲命令,立刻把内存占用最高的容器停掉:
# 强制停止内存占用最高的容器
docker stop killer-app
# 或者直接重启 docker 服务(谨慎操作,会中断所有容器)
systemctl restart docker
2. 治本之策(修改配置并重启容器)
在 docker-compose.yml 中为所有服务添加内存限制:
version: '3.8'
services:
web-app:
image: nginx:latest
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# 对于非 swarm 模式,使用下面的写法
mem_limit: 512m
memswap_limit: 1g
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
对于 Java 应用,启动命令务必加上堆内存限制:
docker run -d --name java-app -m 1g \
-e JAVA_OPTS="-Xms256m -Xmx512m" \
your-java-image
3. 建立监控告警机制
不要等到卡死了才去排查。建议使用 cAdvisor + Prometheus + Grafana 搭建一套轻量级监控,当容器内存使用率超过 80% 时,立即通过钉钉或邮件告警。
# 快速启动一个 cAdvisor 容器(仅供临时排查,生产环境建议用二进制)
docker run -d \
--name=cadvisor \
--restart=always \
-p 8080:8080 \
-v /:/rootfs:ro \
-v /var/run:/var/run:ro \
-v /sys:/sys:ro \
-v /var/lib/docker/:/var/lib/docker:ro \
gcr.io/cadvisor/cadvisor:latest
六、写在最后
Docker 容器内存排查其实并不复杂,核心思路就是:宿主机全局 -> 容器统计 -> 内部进程 -> cgroup 细节 -> 内核日志。只要按照这个链路一步步来,再狡猾的内存杀手也能被揪出来。
最后再啰嗦一句:服务器硬件资源是有限的,但问题排查的思路是无限的。 如果你不想整天提心吊胆地守着服务器,建议还是选择靠谱的云服务商,并且养成给所有容器加内存限制的好习惯。毕竟,数据无价,稳定压倒一切。
希望这篇实战手册能帮到正在被容器内存问题折磨的你。如果你在实操中遇到了其他怪问题,欢迎在评论区留言,咱们一起探讨。
相关技术专题与延伸阅读
- WordPress 用 Docker Compose 一键部署总报错?这 4 个高频坑和排查思路,我帮你踩平了
- 反向代理的终极形态:用 Docker 优雅落地 Nginx Proxy Manager,这才是内网穿透与多站点管理的正确姿势
- 云服务器公网IP和私网IP傻傻分不清?端口配置踩坑实录,手把手教你一次搞定
- 别再租第三方网盘了!手把手教你用 Docker 自建一套高可用私有云盘,彻底掌控数据主权
📦 【资源免费领】本文全套实操配置文件与避坑手册下载
本文涉及的全套 Docker Compose 配置文件、服务器运维避坑清单及 AI 提效指令库已完整打包,可免费极速转存:
💡 提示:推荐使用手机【夸克网盘 App】打开保存,新用户首月免费赠送 1TB 超大空间与免流量极速下载特权。