ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker Overlay2 磁盘占用排查与清理实战指南

Docker Overlay2 磁盘占用排查与清理实战指南 接手一台莫名其妙磁盘告警的 Linux 服务器第一件事我先不看监控平台而是直接登上去跑df -h看到/分区使用率 90% 以上的时候再切到/var/lib/docker里用du -sh *扫一遍十次有八次是overlay2目录顶在最前面。这类问题在 Docker 环境里太常见了尤其是那些用 default 配置跑了几年的机器镜像、容器、日志、构建缓存全部堆在一个分区里最后磁盘被撑爆往往不是某个业务数据多大而是 Docker 自己留下的“垃圾”没人管。这篇文章我把自己排查和清理 Overlay2 磁盘占用的一套完整思路写出来从现象定位到原理分析再到具体命令按顺序操作基本能把空间问题稳妥解决。1. 先搞清楚磁盘究竟被谁吃掉了从 docker system df 到目录级排查1.1 最该先跑的一条命令docker system df遇到磁盘告警记得先在服务器上执行这条命令docker system df它会一次性告诉你 Docker 的几类核心资源分别占了多大空间类型说明常见占用原因Images所有本地镜像包含正在被容器使用的和不再被任何容器引用的悬空镜像反复构建镜像、拉取新版本历史镜像层残留Containers容器可写层即使容器已停止也不会自动释放停止/退出后的容器不会删除可写层占空间Local VolumesDocker 管理的命名卷用于数据持久化启动容器时用-v挂在的卷目录Build Cache构建过程中生成的缓存层使用 BuildKit 构建镜像时产生的中间缓存实际输出长这样TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 27 8 5.823GB 3.212GB (55%) Containers 19 2 486MB 486MB (100%) Local Volumes 4 3 4.916GB 0B (0%) Build Cache 82 0 2.116GB 2.116GB (100%)这里面的RECLAIMABLE列特别重要它表示可回收空间。上面这个例子里镜像可回收 3.2GB、容器全部可回收 486MB、构建缓存几乎全部可回收 2.1GB加起来约 5.8GB 的空间是可以安全清理出来的。这一列反应的其实就是“无效资产”比例如果长期不清理比例会不断上升。1.2 用 du 逐层切分 /var/lib/dockerdocker system df只是全局视角要精确定位异常还是要一头扎进 Docker 的数据目录。Docker 默认的数据目录是/var/lib/docker切换存储驱动为 Overlay2 后里面主要这几个子目录需要重点看sudo du -h --max-depth1 /var/lib/docker 2/dev/null | sort -hr常见输出4.1G /var/lib/docker/overlay2 2.3G /var/lib/docker/containers 1.9G /var/lib/docker/buildkit 1.2G /var/lib/docker/volumes 60M /var/lib/docker/image排查优先级我会这样排overlay2存放镜像层和容器可写层的地方如果这里有几十GB通常是构建缓存、悬空镜像、堆积容器在共同作祟containers不仅是容器配置还包含每个容器的标准输出日志文件*-json.log这是最容易爆的单项buildkit构建缓存跑过一次大型镜像构建后这里会迅速膨胀volumes业务数据卷清理要格外谨慎有个排查习惯值得分享先跑docker system df -v这个命令会列出每个镜像、容器、卷的具体大小和是否被引用的状态能一次性找出“占着位置但没被使用的对象”。之后再针对地翻目录效率会高很多。新手最容易犯的错是直接rm -rf /var/lib/docker/overlay2/xxxx用不了多久容器就会异常退出下面我会说明为什么这个动作非常危险。2. Overlay2 为什么会胀得这么厉害存储驱动的“记账”机制与常见误区2.1 Overlay2 的基本工作原理lowerdir 与 upperdirDocker 的 Overlay2 存储驱动基于 Linux 内核的 OverlayFS 实现。一个容器运行时它的文件系统由多层目录叠加组成核心概念有两个lowerdir只读的镜像层多个镜像层按顺序叠加底层是基础镜像如 ubuntu上层是依次执行的 RUN 指令产生的变化层upperdir容器可写层容器运行期间所有文件写入、修改、删除都记录在这一层用户在日常操作中看到的/var/lib/docker/overlay2/下那一堆随机十六进制字符串的目录实际上每个目录对应的是一个镜像层l/目录里是指向真实层的简短软链或一个容器的可写层。这个机制带来的直接结果是镜像层一旦生成就是只读、共享且不可变的。多个容器基于同一个镜像启动时它们共享下层所有只读镜像层只在各自的可写层上记录差异。这也是为什么容器方式比虚拟机节省空间的原因。2.2 空间消耗为什么只增不减悬空镜像、构建层与可写层残留Overlay2 目录膨胀有三个主要“漏洞”第一悬空镜像层。每次执行docker build如果生成了新的镜像标签旧镜像就会变成none的悬空镜像dangling image。这些镜像的层仍遗留在 Overlay2 目录中占用磁盘空间。尤其是 CI 环境反复构建同一个服务几百个悬空镜像轻松吃掉几 GB。第二构建缓存累积。使用 BuildKit 构建时每个指令的中间层都会保存为缓存便于下次构建复用。但长期不执行docker builder prune这些缓存层就会一直留在磁盘上。我的经验是一个大型 Java/前端镜像构建构建缓存轻松超过镜像本身的体积。第三容器可写层不会自动销毁。很多人用容器跑一次性任务跑完只执行docker stop就直接不管了。容器进入停止状态后可写层依然存在。上百个退出状态的容器堆积起来Overlay2 目录自然越来越大。你用docker ps -a就能看到一堆状态是Exited的容器每一个都挂着一层“写层残骸”。2.3 为什么绝对不能直接 rm -rf overlay2 下的目录这一条我放到最前面强调因为太多人在这里折过。永远不要直接删除/var/lib/docker/overlay2/下面的任何目录来“清理空间”。原因是 Docker Daemon 在本地维护了一套完整镜像元数据其中记录了“哪些层属于哪个镜像、哪个镜像被哪些容器引用”。元数据主要存储在/var/lib/docker/image/overlay2/下。如果你绕过 Docker API/CLI 直接删除 overlay2 下的数据目录会造成如下后果Docker Daemon 认为某个镜像或容器仍然存在但实际文件已经缺失尝试启动相关容器时直接报错如failed to mount overlay... no such file or directory即使docker rmi想删除这些镜像清理过程也可能失败因为元数据引用的层文件和实际文件不一致一个真实案例某同学为了省钱直接删除了一堆 overlay2 目录结果 Docker Daemon 里还“记得”这些层docker images能看到镜像有任务需要启动时却完全起不来最后只能把所有镜像重新 pull期间服务全部不可用。所以请把这一条刻进脑子里Overlay2 目录受 Docker 元数据管理唯一合法的清理方式是通过 Docker CLI/API 执行镜像、容器、卷、缓存清理操作。后面的所有清理思路都是在这个前提下展开的。3. 从上到下的清理指令prune 家族的正确打开方式3.1 官方 pr 指令族各管一摊选对命令避免误删Docker 提供了一套成体系的清理命令我把它们的作用范围和危险程度整理成表命令作用范围是否默认删除悬空项危险程度docker container prune删除已停止的容器是低注意卷不会自动删docker image prune删除悬空镜像无标签镜像是低到中默认不动被容器引用的镜像docker image prune -a删除所有未被容器使用的镜像是中高会把你不在运行中的历史镜像全部删掉docker volume prune删除未被任何容器挂载的卷是高删除后数据不可恢复docker builder prune删除构建缓存是低最多下次构建慢一点docker system prune容器、悬空镜像、构建缓存、网络汇总清理是中默认不动卷docker system prune -a --volumes以上所有 全部未使用镜像 卷是极高需要仔细核对中国确认3.2 最实用的一条命令组合如果确认磁盘紧张我通常会先执行下面这一组命令能安全释放大部分空间docker container prune -f docker image prune -f docker builder prune -f docker system df第一句删掉所有停止状态的容器第二句清理悬空镜像第三句清空构建缓存最后再看一眼系统状态确认空间变化。这套组合对业务影响很小因为停止状态的容器本来也不会被使用悬空镜像和构建缓存删了也可以再次生成。如果空间依然紧张下一步才会考虑扩大到docker image prune -a -f这句会把所有未被运行中容器引用的镜像全部删掉包括那些已经在仓库里但本地打了 tag 的镜像。使用前一定要确认本地没有需要离线使用的特殊镜像否则后续启动容器时会被迫重新拉取。3.3 按时间过滤保留最近 N 天需要的镜像有的团队希望尽量删除旧镜像但又要保留最近几天可回滚的版本。可以通过--filter until参数实现docker image prune -a -f --filter until168h这个命令会删除所有未被使用、且创建时间超过 7 天的镜像。同理可用于容器docker container prune -f --filter until24h这个组合在当前生产环境维护中很实用既能让磁盘空间保持健康又能让最新版本随时可以回滚。不过要注意until是基于镜像创建时间过滤的如果你的发布流程保留了多个 tag 且创建时间都在 7 天内它不会删除这些镜像。3.4 通过 docker system df -v 找到具体的“空间大户”前面提到过docker system df -v这里展开用法。它会输出一份结构化清单包含所有镜像、容器、卷的详细占用量。例如Images space usage: REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS mysql 8.0 1a2b3c4d 2 weeks ago 514MB 0B 14.25MB 0其中SHARED SIZE表示多个镜像共享的层大小UNIQUE SIZE表示该镜像独占的层大小。如果CONTAINERS为 0说明该镜像是可以安全删除的候选对象。凭经验删镜像前我会优先找CONTAINERS为 0、UNIQUE SIZE较大的镜像处理因为这些镜像占用的空间纯粹是“独享盘”删掉释放最彻底。4. 日志文件撑爆磁盘最容易被忽略的隐形杀手4.1 Docker 日志的默认行为无限增长很多人的磁盘爆掉问题不只在 overlay2而是容器日志文件。Docker 默认的日志驱动是json-file它会把容器写到标准输出stdout/stderr的内容保存成文件文件路径是/var/lib/docker/containers/容器完整ID/容器完整ID-json.log关键的问题是默认配置下这个文件没有任何大小上限、没有自动轮转。一个只打印一条 SQL 查询的 API 服务一天能写几个 GB 日志一个调试接口一晚上打印堆栈的应用第二天磁盘直接 100%。4.2 定位和清理现有日志先定位占用最高的日志文件sudo du -h /var/lib/docker/containers/*/*-json.log 2/dev/null | sort -hr | head -20如果发现单个日志文件已经好几个 GB可以立即清空它不需要停止容器sudo truncate -s 0 /var/lib/docker/containers/容器完整ID/容器完整ID-json.log用truncate而不是rm原因是想保留文件 inode 和文件句柄。如果直接rm已经打开该日志文件的容器进程仍然持有句柄空间不会立刻释放直到容器重启才消失。而truncate -s 0属于原文件清空空间立即释放且不会影响 Docker 继续写入日志。在某些旧的容器运行场景里日志文件被进程占用导致 rm 后空间不释放的问题很典型所以用 truncate 是保险做法。4.3 修改 Docker Daemon 配置让日志自动轮转长期方案是配置 Docker Daemon 对日志文件做轮转。在/etc/docker/daemon.json中加入{ log-driver: json-file, log-opts: { max-size: 20m, max-file: 5 } }含义单个日志文件最大 20MB最多保留 5 份超过后自动轮转。也就是单个容器的日志最多占用 100MB不会再无限膨胀。修改后需要重启 Dockersudo systemctl restart docker但是注意几个问题修改只对之后新建的容器生效已经存在的容器仍沿用旧日志配置如果容器数量多重启 Docker 会影响所有依赖容器的服务请务必在维护窗口进行也可以不重启 Daemon改用每个容器的启动参数docker run --log-opt max-size20m --log-opt max-file5 ...给存量容器统一加日志限制的另一种思路是用docker inspect找到现有容器然后重建它们。这个我会在后面的自动化运维章节给个参考脚本。5. 构建缓存与孤立卷两个同样值得处理的占用大头5.1 BuildKit 缓存构建次数越多膨胀越厉害如果你用 Docker 23.0 以上版本且使用 BuildKit默认开启/var/lib/docker/buildkit目录会保存构建过程中的缓存。这包括每个 RUN 指令产生的中间快照层下载的依赖包缓存编译过程产生的中间产物这些缓存能显著加速重复构建但代价是磁盘占用稳步上升。释放它们docker builder prune -f也可以只删除超过一定时间的缓存docker builder prune -f --filter until168h需要注意清理构建缓存不会影响已有镜像的正常使用只是下次构建需要重新执行部分步骤构建时间会变长。在磁盘不宽裕的主机上我宁愿用构建时常换磁盘空间。如果团队用 Jenkins/GitLab CI 频繁构建务必配置一条周期性清理任务否则 buildkit 目录吞掉的空间会超出多数人的心理预期。5.2 孤立卷删之前先认准是谁的数据docker volume ls能列出所有 Docker 命名的卷。孤立卷指的是创建了但当前没有任何容器挂载的卷。它们通常来自几个场景先docker volume create创建卷后来忘记使用容器删除时没用-v参数卷被保留了下来老旧的数据备份容器删掉了卷留下了处理前先看有哪些孤儿卷以及大小docker volume ls -qf danglingtrue docker system df -v | grep -A 100 Local Volumesdanglingtrue状态的卷就是未被任何容器引用的卷。要安全删除docker volume prune -f我觉得这条命令是全套餐里最需要谨慎的一条。卷里可能保存了数据库文件、上传文件、配置文件等真实业务数据。删之前务必用下面两个办法确认一遍docker volume inspect 卷名查看元数据尤其是挂载时用的容器名如果无法判断先把卷 mount 到一个临时容器里把内容备份出来一个很稳妥的习惯是对不确定的卷执行docker run --rm -v 卷名:/mnt/data -it ubuntu bash ls -la /mnt/data进去看一眼内容再决定要不要清。虽然有些麻烦但总比误删一个月的数据好。5.3 单独揪出 overlay2 目录里的“逃逸分子”即使执行了全套 prune/var/lib/docker/overlay2还是很大这种情况需要具体查看里面的层归属。可以用下面的命令找到所有层目录再关联镜像元数据判断ls -lt /var/lib/docker/overlay2/ | head -20如果你发现某几个目录的时间戳特别新且大小异常很可能是一个正在运行的高写入容器引起的。比如容器内持续往/var/log写文件或者跑着一个不断落盘的数据库但没挂载命名卷这些写入都会体现在 overlay2 的可写层中。处理方式不是去删目录而是从源头调整容器的数据持久化方式把数据目录用-v挂载出来或者挂载到tmpfs上避免落盘。举一个典型的开发场景docker run -d --name webapp \ -p 8080:80 \ --tmpfs /tmp:rw,size1g \ -v /data/webapp:/app/data \ nginx:latest--tmpfs /tmp表示容器的/tmp写入只占内存不上磁盘-v /data/webapp指向真实业务数据。这样设计后即使容器写层再占空间也有限业务数据落在卷里也方便备份。5.4 另一种常见的“目录体积错觉”du -sh /var/lib/docker/overlay2/*看到某些层特别大时别急着认为这些层都是“垃圾”。OverlayFS 的层与层之间存在硬链接共享多个镜像共用同一个底层文件du统计时如果对同一物理文件多次计数会让你误以为占用翻倍。实际上准确可靠的方式还是以docker system df的SHARED SIZE/UNIQUE SIZE为准不要单看目录统计就下结论。这就是为什么要养成先跑docker system df再深入操作的习惯。6. 自动化清理与长期维护让磁盘问题不再反复爆发6.1 配置一套定期清理任务一台机器如果不做定期清理空间问题大概率会在某个周末的深夜重新找上门。我会建议把清理逻辑做成一个定时任务每周运行一次。下面是我在服务器上反复使用过的一段清理脚本#!/bin/bash # docker-cleanup.sh echo Docker Disk Cleanup Start: $(date) # 删除超过 24 小时且已停止的容器 docker container prune -f --filter until24h # 删除悬空镜像 docker image prune -f # 删除超过 7 天未被使用且无容器引用的镜像打开这个开关前请确认镜像仓库可用 # docker image prune -a -f --filter until168h # 删除超过 7 天的构建缓存 docker builder prune -f --filter until168h # 删除孤立网络 docker network prune -f echo Disk Usage After Cleanup docker system df df -h / | tail -1放到 croncrontab -e # 每周日凌晨 3 点执行 0 3 * * 0 /usr/local/bin/docker-cleanup.sh /var/log/docker-cleanup.log 21需要留意几个点脚本里docker image prune -a那条我默认注释掉避免误删本地离线镜像。同时确保脚本有可执行权限sudo chmod x /usr/local/bin/docker-cleanup.sh6.2 从源头控制镜像体积多阶段构建与频繁清理很多 Overlay2 膨胀问题是可以从构建阶段就避免的。一个经典的错误是 Dockerfile 连续多个 RUN 命令每一条都产生一个镜像层# 反面示例 FROM ubuntu:20.04 RUN apt-get update RUN apt-get install -y wget curl git RUN wget https://example.com/app.tar.gz RUN tar -xzf app.tar.gz RUN rm -f app.tar.gz每一条 RUN 都会产生新层即使最后删掉了文件中间层里仍然残留着临时文件。优化方式是把多条命令合并成一条# 正确示例 FROM ubuntu:20.04 RUN apt-get update \ apt-get install -y wget curl git \ wget https://example.com/app.tar.gz \ tar -xzf app.tar.gz \ rm -f app.tar.gz或者更进一步使用多阶段构建把编译环境和运行环境分离最终只把精简产物拷贝到最终镜像中FROM golang:1.20 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED0 go build -o server . FROM alpine:3.18 WORKDIR /app COPY --frombuilder /app/server . EXPOSE 8080 CMD [./server]多阶段构建能让最终镜像小一个数量级从根源上减少 overlay2 磁盘占用。我在生产环境见过一个项目优化前镜像 1.8GB优化后 78MB释放的效果立竿见影。6.3 给存量容器补齐日志轮换配置前面说了 daemon.json 中的日志限制只影响新建容器。对于已经存在的容器如果不想逐个重建可以在维护窗口里批量做“重建替换”。一个示例思路sudo mkdir -p /etc/docker cat EOF /etc/docker/daemon.json { log-driver: json-file, log-opts: { max-size: 20m, max-file: 5 } } EOF然后选择一个低峰时段重启 Docker再在所有启动脚本/编排文件里补充日志限制参数。如果你使用 docker-compose那么在服务定义中直接加services: app: image: myapp:latest logging: driver: json-file options: max-size: 20m max-file: 5这样以后docker compose up重建出来的服务都会带上日志轮转参数磁盘占用就稳定住了。6.4 关于磁盘监控与告警的一些个人建议光清理还不够要避免再次爆盘得设个底线监控落盘量。最简单的做法是写个脚本检查当前磁盘使用率超过阈值就推送告警邮件、钉钉/企业微信机器人等#!/bin/bash THRESHOLD85 USAGE$(df / | awk NR2 {print $5} | sed s/%//) if [ $USAGE -ge $THRESHOLD ]; then echo Warning: disk usage ${USAGE}% on / at $(date) /var/log/disk-alert.log # 这里可以接入你的告警发送逻辑 fi用 cron 每 10 分钟跑一次即可。磁盘碎片化带来的问题通常是复用空间主要是docker system df中的RECLAIMABLE指标稳步增加这时候即使五分钟阈值已过也会留下足够余量针对性清理。不用担心定时任务有点老旧关键是每次预留足够告警缓冲。6.5 存储驱动的取舍Overlay2 是默认解但不绝对是唯一解最后沿着存储驱动层面再做一点延展。如果 Overlay2 在某种环境下问题频繁且磁盘压力很大可以评估切换到其他存储驱动常见选项有vfs、fuse-overlayfs等。但绝大多数生产环境优先选择 Overlay2原因如下存储驱动优点缺点适用场景overlay2性能好、镜像层共享高效、磁盘利用率高元数据不一致时清理复杂绝大多数 Linux 发行版默认选择vfs无特殊内核依赖兼容性极强每个层都是完整拷贝空间占用极其夸张内核不支持 OverlayFS 的少数环境基本不推荐fuse-overlayfs用户态文件系统某些环境可用性能不如内核态 overlay2无 root 权限容器场景切换存储驱动属于“伤筋动骨”的操作需要导出所有镜像和卷数据、重新初始化 Docker 数据目录再做导入恢复操作不当会丢数据。除非你的环境确实存在兼容性的硬伤一般不建议只为了省空间去切存储驱动。6.6 从运维习惯上减少磁盘压力我维护过几台长期不重启、不清理的 Docker 主机它们的共同特征就是磁盘紧张。反观那些管理得当的主机一般都有这些习惯“随用随删”的容器一次性执行任务的容器启动时加--rm参数退出后自动删除。“标签即清理依据”每次发版保留最近 3 个镜像 tag更早的版本进入清理脚本的删除范围。“数据卷显式挂载”所有需要持久化的数据都挂到命名卷或宿主机目录避免写入容器可写层可写层在容器删除后会丢失且难以备份。“例行巡检”每周看一眼docker system df观察 RECLAIMABLE 比例在膨胀趋势明显时手动介入。这些动作不像清缓存那样立竿见影但长期坚持下来Overlay2 目录的增长速度会慢得多很多所谓的“磁盘爆掉”事件根本不会发生。回到最开始那个场景如果你现在正面对一台磁盘告警的机器我建议的行动顺序是先docker system df看整体盘子再du定位/var/lib/docker下的大头然后按“容器 - 悬空镜像 - 构建缓存 - 日志”的顺序清理最后设置定时任务和日志轮转。这套流程我一个人跑了几年也帮别人处理过不少同样的问题基本可以在半小时内让磁盘回到安全水位。操作时唯一要记住的底线就是一切清理通过 Docker 自身的命令完成绝不手撕 overlay2 目录。
返回列表