本文探讨了Docker容器磁盘空间不足的问题,特别是overlay2目录和日志文件导致的磁盘空间占用。文章指出直接删除overlay2目录会损坏Docker元数据,导致容器无法启动;同时,删除日志文件并不会释放磁盘空间,因为文件句柄未被释放。建议使用Docker自带工具分析磁盘占用,并正确清理日志文件。
前阵子半夜收到云监控报警,一台跑了十来个微服务容器的应用服务器根分区使用率直接冲到了 98%。登录上去敲了个 df -h,100G 的系统盘只剩下不到 2G 可用空间。顺着 du -sh /* 一层层往下查,发现罪魁祸首死死卡在 /var/lib/docker/overlay2,单个目录就吃掉了将近 78G。
刚开始接触 Docker 时,很多人遇到这种情况第一反应就是想去 overlay2 里面挑大目录删。这往往是灾难的开始。后来经历了几次线上故障,我才摸清了 Docker 磁盘占用的真实结构和正确的排查清理顺序。
坑一:千万别直接 rm 掉 overlay2 里的任何目录
overlay2 目录下全是一长串哈希值命名的子目录,比如 d41d8cd98f00b204e980...。如果不清楚它的底层原理,很容易以为这些是构建镜像残留的临时缓存,顺手就敲个 rm -rf。
千万不能这么做。overlay2 是 Docker 的联合文件系统(OverlayFS)存储驱动目录。每个容器镜像的只读层、容器运行时的可写层(diff)、以及层与层合并的挂载点,全部通过这里的软链接和元数据拼装在一起。
一旦手动删除了其中某个目录,Docker 的元数据索引就会彻底损坏。接下来你会发现容器无法启动、镜像无法拉取,报错全都是诡异的 no such file or directory。遇到这种情况,除了把损坏的容器彻底销毁重建,几乎没有优雅的修复办法。
要找到到底是哪个容器吃掉了空间,不能肉眼看 overlay2 的哈希名,而应该先用 Docker 自带的分析工具:
docker system df -v
这个命令会列出四部分空间的分布:
- Images:镜像实际占用的物理大小和共享大小
- Containers:每个运行中容器的可写层大小(writable layer)
- Local Volumes:持久化数据卷的大小
- Build Cache:构建镜像产生的中间缓存
如果看到 Containers 这一项的 SIZE 异常巨大,接着执行:
docker ps -a -s
注意观察输出里的最后一列 SIZE,它会有两段数值,形如 2.1GB (virtual 5.4GB)。括号前面的 2.1GB 就是容器自身可写层的数据量。如果某个容器的这个数值达到了几十 G,说明应用代码在容器内部疯狂写本地文件,比如把业务日志、上传附件写到了没有挂载数据卷的容器内部路径。
坑二:用 rm 删除了日志文件,磁盘空间却一点没降
在实际排查中,最常见的元凶其实还不是容器内写文件,而是容器的标准输出日志。
Docker 默认的日志驱动是 json-file,容器内应用只要往 stdout 或 stderr 打印日志,Docker 守护进程就会把它按 JSON 格式追加写到宿主机的这个路径下:
/var/lib/docker/containers/<container-id>/<container-id>-json.log
可以用一条查找命令把整个目录下的大日志文件全揪出来:
find /var/lib/docker/containers/ -name "*-json.log" -exec ls -lh {} +
有一次我排查一个 Java 服务的容器,发现光是这一个 *-json.log 就占了整整 45G。
不少人查到这里,上去就执行:
rm -f /var/lib/docker/containers/xxxx/xxxx-json.log
删完之后满心欢喜地运行 df -h,结果发现已用空间纹丝不动。
这是因为 Linux 的文件删除机制。当执行 rm 时,只是把目录项下的文件名引用计数减一(unlink)。但由于 Docker 守护进程(dockerd)和容器日志采集进程依然打开着这个文件句柄,内核判定该文件的 inode 依然处于打开状态,根本不会真正释放物理磁盘块。
你可以通过这条命令验证是不是被未释放的句柄卡住了:
lsof | grep deleted | grep docker
终端会明确显示那个几十 G 的 log 文件后面标记着 (deleted),但进程一直在往里面写。
正确的在线清理姿势是截断文件,而不是删除文件:
truncate -s 0 /var/lib/docker/containers/*/*-json.log
或者使用重定向:
cat /dev/null > /var/lib/docker/containers/<container-id>/<container-id>-json.log
truncate 会直接把文件长度截断为 0 字节,在保留现有打开句柄的同时立即把占用的磁盘空间归还给操作系统,不需要重启容器,磁盘占用瞬间就会降下来。
坑三:被忽略的悬空镜像和 Buildx 构建缓存
排查完容器和日志后,如果 overlay2 依然占用很大,往往是持续集成(CI/CD)在宿主机上频繁构建镜像留下的副产物。
每次编译部署,新镜像打上了最新标签,旧镜像就变成了悬空镜像(Dangling Images),在 docker images 里显示为 <none>:<none>。此外,如果开启了 Docker Buildx 或者 BuildKit,多阶段构建的中间缓存也会悄悄堆积在 overlay2 里面。
查看构建缓存占用情况:
docker buildx du
清理这些无用对象时,要小心使用 docker system prune。很多教程直接给出一句 docker system prune -a --volumes -f,这非常危险。--volumes 参数会一并清理掉没有被任何容器关联的本地数据卷,万一你的 MySQL 或 Redis 容器暂时处于停止状态,持久化数据卷就可能被这句命令直接清理掉。
我平时更倾向于分步做安全清理:
清理悬空镜像与未运行容器:
docker image prune -f
docker container prune -f
清理持续累积的 BuildKit 构建缓存:
docker builder prune -a --force --filter "until=72h"
指定保留最近 72 小时内的构建缓存,既能回收几天前积累的垃圾层,又不会让下一次发布因为失去基础缓存导致重新拉取编译变慢。
坑四:治本必须配置全局日志轮转,但别忘了存量容器
手工截断日志只是应急灭火,过几天日志还是会把磁盘写满。彻底杜绝这个问题,必须给 Docker 守护进程配置日志文件的上限和轮转切分。
编辑宿主机的 Docker 配置文件 /etc/docker/daemon.json(如果没有该文件就新建一个):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
这里的含义是:每个容器的日志文件最大不超过 50MB,单个容器最多保留 3 个滚动历史文件。这样单个容器的日志上限就被死死锁定在 150MB 以内,无论应用怎么疯狂刷屏,都不可能再把磁盘打满。
配置保存后重载并重启 Docker 服务:
systemctl daemon-reload
systemctl restart docker
这里有一个极容易被忽视的坑:修改 daemon.json 里的日志规则,只对重启之后新创建的容器生效。
原本就已经存在的存量容器,它们的日志驱动参数在容器初始化时就已经固化在各自的 hostconfig.json 里了,单纯重启 Docker 服务或者执行 docker restart <name> 根本不会更新日志策略。
如果是通过 Docker Compose 管理的项目,必须让它重新创建容器实例:
docker compose up -d --force-recreate
如果是单容器运行,需要先 docker rm -f 删除旧容器,再用原本的 run 命令重新拉起。通过 docker inspect <container-id> | grep -A 5 "LogConfig" 看到配置里的 max-size 已经变成 50m,才算真正完成了日志上限的全局落地。
文章标题:Docker 容器把磁盘撑满了?排查 overlay2 目录暴涨和日志清理我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/198.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?