Docker 镜像分层:为什么空间占用不能直接相加

Docker 镜像分层:为什么空间占用不能直接相加原创封面

一张镜像列表为什么无法说明物理占用

镜像列表中的大小回答的是使用这份镜像需要哪些文件内容,而磁盘实际占用回答的是这些内容在存储中保存了几份。两者的统计目标不同。多个服务使用同一个基础镜像时,它们可以引用相同的层;多个标签也可能指向同一份镜像。把列表里每行的大小相加,会把共享内容重复计算。遇到镜像占用看起来远大于磁盘容量的情况,应先检查统计口径,而不是立即怀疑 Docker 丢失了清理结果。本文讨论本机镜像存储中的共享关系,不用镜像展示值替代宿主机的文件系统统计。

排查的起点是 docker system df -v。它提供镜像的共享空间、独有空间以及关联容器信息,适合用于解释为何删除一个镜像未释放预期容量。检查时记录上下文和引擎版本,避免拿另一台远程引擎的数据与本机磁盘比较。下载时看到的压缩层大小、镜像列表里的虚拟大小和文件系统分配的空间,也不是一组可以直接互换的数字。需要明确每份报告来自哪里、统计什么,以及它是否包含容器写入层、日志和持久化卷。

从层的引用理解共享

可以把镜像理解成按顺序应用的一组文件系统变化,再附上启动命令等配置。两个镜像若引用了相同内容的层,存储可以复用这些内容。共享依据并不是镜像名称相似,也不是文件看起来一样,而是镜像存储所使用的内容标识与引用。仅给镜像添加一个新标签,通常不会制造一份完整的文件副本;但重新构建时输入变化,可能生成不同的层。排查工具中的镜像标识、标签和根文件系统层信息,应分别记录,不能用其中一个替代另外两个。

假设两个应用都使用同一份基础层,再各自添加业务代码。这时基础层在概念上属于两份镜像的可用内容,但物理存储可以只保留一份。删除其中一个应用镜像,仍被另一个镜像使用的基础内容需要继续存在。这个示例只说明引用机制,不提供实测的容量比例;实际回收还受到构建缓存、容器和镜像存储实现的影响。若维护目标是腾出空间,就需要找到真正不再被引用的独有内容,而不能只挑列表里看起来最大的标签。

容器的可写层要另列一项

容器启动后,会在镜像的只读内容之上建立自己的写入状态。应用生成文件、修改配置和删除文件,都会影响这一层。多个容器可以共享镜像,但它们的写入层并不是同一份数据。因此,统计多个容器时,可以比较各自的可写层大小,同时单独考虑共享镜像。docker ps --size 中的 size 和 virtual size 含义不同,后者将镜像内容计算在内,直接汇总会再次产生重复。对于数据密集的服务,应进一步核对应用究竟写入了哪里。

容器大小也不一定包括所有关联占用。持久化卷、宿主机绑定目录以及日志可能由其他位置保存。一个容器的可写层很小,并不证明这个服务整体很省磁盘;数据库使用卷时,大部分业务数据本来就不在写入层里。为服务估算占用,可以建立镜像、可写层、挂载数据、日志四项清单,然后说明共享部分。这样更有助于判断应该优化镜像构建、治理应用生成文件,还是调整日志保留,而不是把所有问题都归到镜像数量上。

在后续层删除文件不会抹掉早期层

构建文件里先复制大量素材,后面的步骤再删除其中一部分,最终容器目录可能看起来很干净,但前面层中已经保存的内容仍可能存在。后续删除表达的是最终文件系统视图中的变化,并不改写先前层。类似问题也会出现在先安装大量工具、再在另一个步骤中卸载的构建方式里。观察 docker history 可以帮助定位哪些构建步骤增加了内容,但历史记录需要结合实际构建文件解释,不能把每一条配置指令都当作包含大文件的层。

优化方法取决于文件是否需要进入最终制品。下载临时文件、使用后清理,可以在合适的同一个构建步骤内完成;只用于编译的工具可以放在独立阶段,最终阶段仅复制必要输出。构建上下文里的本地依赖和旧产物也应该排除。这里不建议为了压缩几行构建文件,把所有步骤塞进无法维护的一条长命令。需要验证的是最终镜像包含哪些运行必需内容、能否从锁定输入重建,以及是否有敏感文件曾经进入层中;最终目录看不到密钥,不代表历史层从未包含它。

用检查命令核对身份与历史

先运行 docker image inspect,查看镜像标识、仓库摘要和根文件系统信息,再读取 docker history 的构建历史。标签适合表达用途,摘要更适合确认具体制品。为回滚保留镜像时,应记录当前部署实际使用的身份,而不是只保存 latest 这样的可变标签。如果标签已经指向新版本,清理旧镜像时可能误判它的作用。检查命令输出中的环境配置也可能包含不适合公开的内容,分享排查记录前应只保留与存储有关的字段。

对计划清理的镜像,还需要查看容器是否引用它。停止容器也可能保留镜像关联,因此删除镜像与删除容器存在顺序关系。但为了释放镜像而先移除全部停止容器,会失去它们的可写层和挂载线索。应逐个确认用途后操作,保持镜像检查与业务恢复清单一致。执行删除后,重新读取相同上下文的用量,而不是只根据命令返回成功推断物理空间已经释放。成功说明对象操作完成,实际回收仍需要另一个观察点。

构建缓存会继续引用相关内容

构建器为了复用中间结果,可能保留与某些镜像相关的层。镜像标签删除以后,构建记录依然有价值,存储因此未必立即释放。此时应使用 Buildx 的用量检查,确认哪些缓存记录属于当前构建器。镜像列表和构建缓存列表有交集,不能把它们的所有展示值再次相加。若要处理缓存,说明它的重建成本并使用明确的构建器目标;不要把数据卷纳入镜像层的问题分析。

不同存储实现的表现也可能不同。经典存储驱动与使用 containerd 镜像存储的安装,在目录组织和统计细节上并不完全一致。文中的层引用概念可以帮助建立判断模型,但不能据此推断每个系统都存在同样的目录。尤其不要手工删除 overlay、snapshot 或内容存储下面的文件来追求立即下降的数字。引擎需要维护对象引用关系,绕过它删除内容可能破坏仍在使用的镜像,也会使后续检查结果失去可信度。

设计一个可解释的对照实验

如果希望验证共享关系,可以在独立开发环境中构建两个使用相同基础镜像的小应用,分别记录镜像检查与系统用量。给其中一个镜像增加标签,再比较对象身份;随后仅删除新增标签,观察镜像内容是否仍存在。实验前应确认环境没有需要保留的业务卷,并固定构建输入,避免网络更新带来额外变化。这种实验适合验证统计含义,不适合作为生产机器上随意创建和删除资源的理由。

记录实验时,应把命令、输入和观察结果分开。没有实际执行,就只写预期,例如相同层可能共享、删去单个标签通常仍有其他引用。执行以后也不要只保留一个总容量结论:列出共享空间、独有空间、镜像身份和剩余引用,才能解释结果。如果外层虚拟磁盘没有同步缩小,应注明这是另一层存储观察,而不能据此否定引擎内部已经释放。可解释的实验比一个缺少口径的容量百分比更适合用来指导后续维护。

为镜像保留策略选择合适指标

镜像管理可以优先关注不再需要的独有内容、是否还承担回滚用途,以及重新下载的可用性。数量只是辅助指标:很多小标签可能共享同一份镜像,少数大镜像也可能具有大量独有文件。发布记录应把制品摘要和用途联系起来,清理记录应说明删除了哪些身份及其恢复来源。对于需要审计的系统,还应保留与部署对应的制品信息,避免清理以后无法确定运行版本来自哪个构建输入。

当统计出现矛盾时,沿着标签、镜像、层、容器写入、缓存引用和宿主机分配逐步解释。每一步都使用对应工具,尽量保持时间和上下文一致。本文没有给出一个可套用的镜像压缩比例,因为共享程度由项目输入决定。理解层的引用之后,空间管理的目标就会更清晰:控制不必要的内容与保留窗口,确认恢复条件,并在同一统计口径下验证变化。这样可以避免把展示值简单相加,也能避免将未回收的共享内容误认为清理失败。

实现片段

docker context show
docker system df -v
docker image ls --no-trunc
docker image inspect example/app:release --format '{{json .RootFS.Layers}}'
docker image inspect example/app:release --format '{{json .RepoDigests}}'
docker history --no-trunc example/app:release
docker ps -a --size
docker buildx du --builder default

一手参考资料

评论 · 0

还没有评论留下第一句经过思考的话。

游客评论需审核。注册后可直接公开,无需审核。

SHARE / 分享

分享这篇文章

WECHAT / 微信

用微信扫一扫

在手机微信中打开文章后,再从微信右上角分享给朋友或朋友圈。