容器内存告急,宿主机直接卡死?这份 Docker 内存排查实战手册请收好

一、事故现场:好端端的服务器,怎么就“凝固”了?

💡 推荐阅读:WordPress 用 Docker Compose 一键部署总报错?这 4 个高频坑和排查思路,我帮你踩平了

相信不少用 Docker 跑生产环境的朋友,都经历过这种让人后背发凉的瞬间:SSH 连不上,ping 得通但响应极慢,网站打不开,连阿里云/腾讯云的控制台 VNC 都像幻灯片一样卡顿。等你好不容易重启完机器,登上服务器一看,free -m 显示 Swap 爆满,dmesg 里全是 Out of memory: Kill process 的记录。

这种“卡死”的元凶,十有八九就是某个容器里的进程把内存吃光了。笔者作为常年跟 Docker 打交道的站长,今天就把这套排查思路和实操命令,完完整整地分享给大家。这不是什么高深的理论,而是能直接救命的“战场手册”。

二、动手前的准备:你得有这些“武器”

🔥 【限时特惠福利】高性价比独享云服务器限时 1 折起

搭建网站或部署容器推荐选择高稳定性独享云服务器。限时特惠通道开放中,点击即可领取专属满减代金券与特惠折扣:

👉 立刻领取云服务器限时优惠券

💡 延伸阅读:反向代理的终极形态:用 Docker 优雅落地 Nginx Proxy Manager,这才是内网穿透与多站点管理的正确姿势

在开始排查之前,请确保你的服务器满足以下条件,否则后面很多命令会执行不了:

  • 服务器环境: Linux 系统(CentOS 7.x / Ubuntu 20.04+ 均可),并且你拥有 root 或具备 sudo 权限的账号。
  • Docker 版本: 建议 19.03 及以上版本,因为旧版本对 cgroup 内存统计的支持不够完善。
  • 必备工具: 系统自带的 toppsfree,以及 Docker 自带的 docker stats 命令。如果没装,请先执行 yum install -y procps-ngapt install -y procps

温馨提示: 如果你用的是 1核2G 的小内存机器,建议先买台高配点的服务器再折腾,不然排查过程中机器再次卡死,心态容易炸。笔者目前用的腾讯云 4C8G 的机器,跑容器集群才稍微安心点。

三、核心排查实战:五步定位“内存杀手”

💡 深度技术指南:云服务器公网IP和私网IP傻傻分不清?端口配置踩坑实录,手把手教你一次搞定

第一步:先看宿主机整体水位,确认是不是真的内存耗尽

别急着进容器里翻,先看一眼宿主机全局状态。如果连 docker ps 都敲不动,说明系统已经处于极端饥饿状态。

# 查看内存和 Swap 使用情况
free -h

# 查看系统负载和 CPU 队列
top -bn1 | head -20

如果看到 Mem 那一行的 available 值极低,且 Swapused 值很高,基本可以实锤是物理内存不够用了。这时候别慌,赶紧进入第二步,把“罪魁祸首”揪出来。

第二步:使用 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

如果看到类似 javanodepython 这类进程的 %MEM 值达到了 90% 以上,那么基本可以断定是应用本身有内存泄漏,或者是 JVM 堆内存设置过大。

第四步:查看容器内存 cgroup 限制与真实占用(关键一步)

有时候 docker stats 显示的内存占用并不完全准确,因为 Docker 默认的 stats 命令统计的是 cgroupmemory.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"

如果这里显示 Memory0,说明容器没有设置内存上限,这意味着它可以无限吞噬宿主机内存,直到触发 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 rundocker-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 细节 -> 内核日志。只要按照这个链路一步步来,再狡猾的内存杀手也能被揪出来。

最后再啰嗦一句:服务器硬件资源是有限的,但问题排查的思路是无限的。 如果你不想整天提心吊胆地守着服务器,建议还是选择靠谱的云服务商,并且养成给所有容器加内存限制的好习惯。毕竟,数据无价,稳定压倒一切。

希望这篇实战手册能帮到正在被容器内存问题折磨的你。如果你在实操中遇到了其他怪问题,欢迎在评论区留言,咱们一起探讨。

相关技术专题与延伸阅读

📦 【资源免费领】本文全套实操配置文件与避坑手册下载

本文涉及的全套 Docker Compose 配置文件、服务器运维避坑清单及 AI 提效指令库已完整打包,可免费极速转存:

👉 点击前往夸克网盘免费极速转存(手机端领 1TB 空间)

💡 提示:推荐使用手机【夸克网盘 App】打开保存,新用户首月免费赠送 1TB 超大空间与免流量极速下载特权。

滚动至顶部