Docker迁移后系统盘仍告警?根因是/var/lib/containerd占用及迁移方案

📅 发布时间:2026/10/19 5:11:04
Docker迁移后系统盘仍告警?根因是/var/lib/containerd占用及迁移方案
又一次磁盘使用率告警还是那台机器还是那个系统盘。上周刚把/var/lib/docker从一块几十GB的系统盘迁移到数据盘按当时的计算至少能腾出一半空间结果没过几天告警平台又弹了“磁盘使用率超过90%”的通知。第一反应是“迁移没生效”但df -h一看数据盘确实挂载正常、空间也确有释放那剩下的占用在哪顺着du一层一层翻下去最后目标锁定在/var/lib/containerd。这个目录平时几乎没人会注意到但它悄无声息地吃掉了系统盘几十GB。更关键的是它根本不归docker命令管普通迁移/var/lib/docker的操作对它一点影响都没有。这篇就完整记录那次排查思路、根因分析和最终的根治方案给同样被磁盘空间问题困扰的运维朋友一个可复用的参考。1. 告警复现Docker 数据目录迁走之后空间为什么还在往下掉1.1 之前的第一次迁移做了什么先说这台机器的情况。它跑的是一套内部测试环境的容器集群常年有上百个容器在轮换部署镜像拉取频率很高。系统盘原本只有 40GB/var/lib/docker老早就占了 30GB 上下告警阈值一到就得手工清理。为了根治上个月我们把 Docker 的数据根目录整体迁到了一块 500GB 的数据盘上操作流程也很常规systemctl stop docker rsync -aH --delete /var/lib/docker/ /data/docker/ mv /var/lib/docker /var/lib/docker.bak mkdir -p /var/lib/docker mount --bind /data/docker /var/lib/docker systemctl start docker重启后docker info显示Docker Root Dir: /var/lib/docker实际数据落到数据盘df -h确认系统盘使用率从 85% 降到了 50% 左右。当时以为这事就算翻篇了。1.2 迁移后一周再次告警的排查起点结果一周后的早晨告警群又炸了。系统盘使用率又回到了 88%并且还在缓慢往上走。第一轮排查先确认了几件最基础的事系统盘上是否残留了旧目录/var/lib/docker.bak查了下这个目录在确认容器运行正常后已经删除不存在占用问题。日志文件是否暴涨du -sh /var/log只有几百MB排除了日志打满的可能。是否有进程写了大文件lsof L1查已删除但仍被进程占用的文件也没有异常。既然这些常规嫌疑都排除现象就很奇怪了系统盘的占用在迁移后依然持续增长增长量每天几个GB。这不符合“日志写满”的曲线更像是有某个服务在持续落盘数据。于是开始进入目录级的逐层扫描。2. 顺藤摸瓜占用大头全部指向 /var/lib/containerd2.1 用 du 一层层切割目录排查大目录最可靠的方式不是df而是du。很多人习惯直接看根分区总占用但想精确到“哪个目录涨得最快”必须逐层切。我常用的命令是du -h --max-depth1 -x /var/lib 2/dev/null | sort -hr | head -20注意-x参数很关键它表示不跨文件系统统计。因为我们用mount --bind把数据盘挂到了/var/lib/docker如果不加-xdu会顺着挂载点把数据盘内容也统计进/var/lib/docker的占用里结果会误导判断。扫描结果很意外/var/lib/docker只占了几GB这是缓存和临时数据真正的镜像层已经落到数据盘而/var/lib/containerd占了 26GB并且还在缓慢增长。再往下一层du -h --max-depth2 /var/lib/containerd出来的结构非常清晰路径占用/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs约 22GB/var/lib/containerd/io.containerd.content.v1.content约 3GB/var/lib/containerd/io.containerd.metadata.v1.bolt约 0.5GB其他目录不足 1GB对照之前的迁移记录这个目录完全不在/var/lib/docker的范围内也就是说前一次迁移根本与它无关。2.2 疑问为什么 Docker 迁移没带上 containerd当时第一反应是“Docker 和 containerd 难道不是一家的吗”这确实是最容易踩的认知盲区。对于 Docker Engine 20.10 之后的版本镜像的拉取、存储、挂载、容器运行时的生命周期管理实际都交给了 containerd 来做。你可以理解成docker命令是面向用户的控制面containerd是真正干活的执行面。我们在用docker pull、docker run时底层是 containerd 在拉取镜像、创建快照、挂载文件系统。但这两个组件的数据目录是分开的/var/lib/dockerDocker CLI 和 daemon 的元数据、配置、网络、容器状态等。/var/lib/containerdcontainerd 自己管理的镜像内容、快照、元数据。具体到这篇文章里的场景绝大部分空间消耗来自 snapshotter 目录。containerd 的 overlayfs snapshotter 会为每个镜像层和每个容器可写层创建快照目录这些数据默认落在/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots。镜像虽然显示成docker images里的一个名字但实际文件内容几乎都躺在 snapshotter 里。所以“只迁移 Docker 数据目录而不迁移 containerd 数据目录”的方案从一开始就不完整。镜像拉取得越多、版本迭代越快/var/lib/containerd就会越肥系统盘继续爆满只是时间问题。3. 深入机制containerd 的存储构成和 “垃圾回收” 的真实情况3.1 containerd 数据目录的三块核心内容搞清楚 containerd 为什么能吃掉这么多空间得先看它在本地扮演的几个角色。以/var/lib/containerd为例实际起作用的子目录主要有三个职责完全不同第一块snapshotter 快照数据路径/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs这是占用的大头。containerd 以 snapshot 为单位管理镜像层和容器可写层。拉一个几GB的镜像解压出来的每一层都在这里落一份每创建一个容器还会基于镜像层再生成一个可写快照。一个服务连续发布十几次版本旧版本镜像如果没被清理对应的快照就一直躺在磁盘上。第二块content 内容寻址存储路径/var/lib/containerd/io.containerd.content.v1.content/blobs这一块保存的是镜像的 blobs按 SHA256 寻址存储。它的特点是某个 blob 可能不会立即被引用删除即使对应的镜像已经不再被任何容器使用。清理策略依赖 GC 机制而 GC 的触发时机和删除策略可能比很多人想象的要保守。第三块metadata 元数据路径/var/lib/containerd/io.containerd.metadata.v1.bolt这是一个 Bolt 数据库文件记录镜像、容器、快照的索引关系。它本身不大但会随着镜像和容器的增删而产生碎片极端情况下也会有可观的膨胀。3.2 为什么 docker system prune 管不到 all of it大多数运维遇到磁盘不够时的第一反应是执行docker system prune -a -f这个命令确实能清理未被容器使用的镜像但用它收场这次问题时效果非常有限。原因在机制层面docker system prune清理的是 Docker 视角里的“悬空镜像”和“未使用镜像”但 containerd 的 content store 和部分 snapshot 数据不一定会同步被回收。containerd 的 GC 是异步运行的删除容器或镜像后数据可能只是进入待清理状态不会立刻释放磁盘。在新版本 Docker 中docker rmi删除镜像时会调用 containerd 的 API 删除对应对象但 overlayfs snapshotter 的父层快照如果还被其他快照引用就必须等待最后一个引用释放后才删除。换句话说镜像版本的堆积是“温水煮青蛙”。每次看起来只是多了一个几十MB的新层但架不住迭代上百次快照层的共享链越来越深/var/lib/containerd就在你眼皮底下一天天胖起来。3.3 迁移前必须理解的一个细节Docker 的 containerd 目录 vs 系统 containerd 目录排查时我还发现一个很多人容易搞混的点。/var/lib/docker/containerd和/var/lib/containerd不是同一个目录也不一定指向同一个进程。/var/lib/docker/containerdDocker daemon 内置的 containerd 实例数据通常 Docker 自己管理迁移 Docker 数据目录时会一并迁走。/var/lib/containerd独立 containerd 服务的数据目录由containerd.service启动负责当前 Docker 版本的镜像和容器运行时操作。在 Docker 20.10 之后系统普遍运行独立的containerd.service镜像拉取等工作都走这个外部 containerd所以它的数据增长完全独立于/var/lib/docker。如果只看/var/lib/docker会漏掉一整块真实占用。4. 根治方案把 containerd 的数据目录整体搬到数据盘4.1 两种方案选型改启动参数还是软链接确认/var/lib/containerd是元凶之后接下来就是迁移。这里有两种主流做法改containerd的启动参数或者建软链接指向数据盘。两者各有适用场景。方案一改 systemd unit 的启动参数containerd 的 root 目录是通过启动参数--root指定的默认是/var/lib/containerd。修改启动参数意味着从服务层面直接告诉 containerd “你的数据根目录在别处”。这样最干净不存在目录结构和挂载点混淆。方案二保留路径用软链接拐到数据盘把/var/lib/containerd整体同步到数据盘后删除原目录在原位置建一个指向数据盘的软链接。服务看到的路径没变实际数据落在数据盘。优点是操作风险低特别适合那些不方便改 systemd 配置的托管环境。我最终选择了方案一因为在我们的环境里 systemd 完全可控且后续排障时直接看 unit 就能确认数据根位置不用每次解释“为什么/var/lib/containerd是软链接”。4.2 完整迁移步骤含每个步骤的意图说明整个迁移过程需要停机操作顺序不能乱。下面按实际操作顺序写出来第一步确认当前 containerd root 配置先看当前的启动命令systemctl cat containerd.service | grep ExecStart通常输出是/usr/bin/containerd或者带若干参数。确认没有任何自定义--root后记录默认路径即可。第二步停止 Docker 和 containerddocker ps --quiet | xargs -r docker stop systemctl stop docker systemctl stop containerd必须先停止 Docker 再停 containerd因为 Docker daemon 还在连接 containerd 操作容器状态。如果只停 Docker 不停 containerdDocker 重启后可能因为 containerd 数据路径变化而产生状态不一致的告警。第三步同步数据到数据盘mkdir -p /data/containerd rsync -aH --delete /var/lib/containerd/ /data/containerd/这里用了-H保留硬链接很重要。containerd 的 snapshotter 和 content store 里存在大量硬链接用来共享 blob如果丢失硬链接关系不仅复制出的数据体积会翻倍还可能导致挂载时文件冲突。第四步确认同步完整性rsync 完成后建议再跑一遍rsync 的天然幂等性看是否还有增量输出。如果第二次执行没有实际传输说明源目录已完整同步。第五步改 containerd 启动参数用 systemd override 的方式不要直接改/lib/systemd/system/containerd.service这样方便统一管理和回滚systemctl edit containerd.service写入[Service] ExecStart ExecStart/usr/bin/containerd --root /data/containerd第一行ExecStart是必须的作用是清空 systemd 继承的默认启动指令否则新配置会追加成两个ExecStart服务直接起不来。第六步重新加载并启动systemctl daemon-reload systemctl start containerd systemctl start docker启动后马上验证systemctl status containerd docker ps docker images容器列表和镜像列表能正常显示基本就成功了大半。再执行一个临时容器运行测试docker run --rm hello-world确保运行时能正常创建快照而不仅仅是“能读”。4.3 常见坑备份目录不要急着删迁移完成后原/var/lib/containerd目录先不要删除。我的做法是把它改名为/var/lib/containerd.bak保留一个完整副本再观察几天。原因很简单如果 containerd 在迁移后因为某些隐藏问题不断报错比如快照损坏、元数据不匹配你需要一个能快速回滚的状态。等运行两三天、确认所有服务正常、告警阈值稳定后再删除备份释放系统盘空间也不迟。删除备份时注意/var/lib/containerd.bak里可能也有大量硬链接文件直接rm -rf有时候会比较慢可以用rsync先清空目录结构再删但一般在测试环境倒也不用那么讲究。5. 迁移后的验证、监控与日常维护建议5.1 验证是否真的释放了系统盘启动服务后用df -h对比系统盘和数据盘的使用率。正常情况是系统盘使用率应下降约等于之前/var/lib/containerd的占用数据盘增加同样大小。再跑一次目录扫描确认 containerd 的真实落盘位置df -h /var/lib/containerd如果输出显示挂载点对应数据盘说明--root参数已经生效。如果想更直观地从进程层面确认可以看 containerd 的打开文件路径ls -l /proc/$(pgrep containerd | head -1)/cwd cat /proc/$(pgrep containerd | head -1)/cmdline | tr \0 进程命令行里能看到/data/containerd就说明运行路径没错。5.2 把“双目录”检查加入监控这次问题给的最大教训是监控不能只看 Docker 一个目录。镜像和容器运行时的数据分散在两个位置任何一个都可能把系统盘撑爆。在监控系统里除了已有的/var/lib/docker使用率建议把以下路径单独加进去监控对象检查方式说明/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshotsdu定时统计快照层目录增长最快的区域/var/lib/containerd/io.containerd.content.v1.content/blobsdu定时统计镜像内容存储拉取镜像时增长/var/lib/containerd/io.containerd.metadata.v1.bolt文件大小元数据膨胀异常时重点检查系统盘整体使用率df标准告警一票制超过阈值直接告警最好是按目录粒度采集而不是只看整个/var/lib/containerd这样才能在磁盘爆满之前预判是哪个环节出了问题。5.3 建立周期性清理机制迁移只是解决了“位置不对”的问题如果平时不控制镜像版本堆积数据盘迟早也会满。建议在运维侧加一个定时任务定期清掉不需要的镜像和悬空快照docker image prune -af docker builder prune -af docker system prune -af --volumes但注意这三个命令必须配合容器的实际使用情况执行尤其是--volumes会连匿名卷一并删除生产环境慎用。更细粒度的做法是按镜像标签和时间维度筛选把超过 N 天未使用的镜像标记后删除。另外containerd 自身也提供 GC 能力但默认的调度比较保守。如果频繁出现“删除镜像后空间没有立刻释放”的情况可以通过查看 containerd 日志确认 GC 执行时间必要时调整 GC 调度配置。不过这属于第二优先级的事——先把数据盘位置改对再谈回收效率。写在最后的一点经验这次问题排查下来最值钱的收获不是“迁移了一个目录”而是意识到容器环境下磁盘占用从来不是单一数据目录的问题。Docker 组件拆得越细数据落盘的位置就越分散/var/lib/docker只是入口真正堆镜像层的是 containerd 的 snapshotter。如果以后再有人问“Docker 迁移后系统盘怎么还是满”我会先让他执行两条命令df -h / du -h --max-depth1 -x /var/lib | sort -hr | head -10把这两个结果发过来基本一眼就能定位问题。至于 containerd 目录迁移本身把--root改到数据盘、验证服务正常、保留备份观察两天这套流程走一遍系统的磁盘告警基本就能按下去了。