容器镜像看起来像一个完整的文件系统,但它并不是把每个容器的所有文件都存一份。镜像由多个只读层堆叠而成,运行容器时,Docker 在这些层之上创建一个可写层。进程读取文件时,系统会从最上层向下逐层查找;进程修改文件时,系统不会改动底层的只读数据,而是把目标文件复制到可写层,再执行写入。这就是容器文件系统的 Copy-on-Write 机制,简称 COW。它让几十个容器可以共享同一份底包,只有改动部分才占用新空间,从而显著降低磁盘占用、提升启动速度,但也引出了查找、删除、写放大等一系列实现细节。

一、镜像分层与 COW 要解决的问题
传统虚拟机通常为每个实例分配完整虚拟磁盘,即使只改动一个配置,也要预留几 GB 空间。容器镜像采用分层设计,例如一个基于 Ubuntu 的镜像可能由基础层、软件安装层、配置层叠加而成。每一层只记录相对前一层的差异。运行容器时新增的可写层最初是空的,所有读操作都会穿透到只读层。
这种分层设计最大的好处是复用。同一台宿主机上运行几十个基于相同镜像的容器时,底层只读层只保存一份,每个容器只需要额外存储自己的私有修改。镜像仓库传输也节省带宽,因为已经拉取过的公共层可以直接复用。但如果每次修改都直接写入共享层,会让所有使用该层的容器互相影响。COW 的规则是任何修改都进入私有可写层,从根源上隔离。即使容器删除,只读镜像层仍然保持不变,可以被其他容器继续引用。
需要注意的是,COW 并不是在容器启动时就把整个镜像复制一遍,而是延迟到实际写入那一刻才复制。读操作几乎零开销,只有首次写文件才发生复制。这个机制带来两个直接问题:一是首次写入某个文件时会出现额外延迟,二是随着时间推移,可写层可能因为复制大文件而迅速膨胀。
二、OverlayFS 的目录结构与 copy-up 流程
OverlayFS 是 Linux 内核提供的联合文件系统,overlay2 是 Docker 目前默认的存储驱动。它把多个目录合并成一个视图,涉及四个关键路径:lowerdir、upperdir、workdir 以及最终的挂载点 merged。lowerdir 是只读底层目录,可以由多个用冒号分隔的层组成;upperdir 是可写层,保存所有修改;merged 是合并后的视图,容器内看到的就是这个目录;workdir 是 OverlayFS 执行内部原子操作时使用的目录,必须和 upperdir 位于同一文件系统。
可以用下面命令手工挂载一个 overlay 文件系统,观察各目录之间的关系:
mkdir -p /tmp/lower /tmp/upper /tmp/work /tmp/merged mount -t overlay overlay -o lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work /tmp/merged
当进程读取一个文件时,OverlayFS 会先在 upperdir 中查找。如果存在,直接返回该文件;如果不存在,再按照 lowerdir 中从高到低的顺序逐层查找。因此上层文件会遮蔽下层同名文件,镜像中的配置层可以覆盖基础层的默认配置。删除操作并不是真正删掉底层文件,而是在 upperdir 中创建一个同名的 whiteout 文件,它是一个字符设备,主次设备号都为 0。对于目录删除,还会设置 trusted.overlay.opaque 扩展属性,表示屏蔽所有下层同名目录的内容。
写文件时,如果目标文件来自只读层,OverlayFS 会触发一次 copy-up。具体流程是:先把原文件完整复制到 upperdir 的对应路径,然后让写操作发生在副本上。如果只是修改大文件中的几个字节,也会复制整个文件,这就是写放大。copy-up 结束后,该文件的所有后续读写都在 upperdir 中进行。对于新建文件,则直接在 upperdir 创建,不涉及复制。重命名、修改权限等元数据操作也可能触发 copy-up,因为只读层的元数据不能被直接修改。
三、COW 带来的性能影响与排查方法
从性能角度看,COW 对频繁读取小文件通常比较友好,因为大部分数据可以直接命中只读层的页缓存。但首次写某个大文件时可能产生明显卡顿。例如把一个 20GB 的数据库数据文件放进容器镜像,第一次执行修改会先复制 20GB 到可写层,再写入 4KB 数据,IO 和磁盘占用量都会显著增加。对于 SQLite、MySQL、PostgreSQL、虚拟机磁盘镜像、消息队列日志等需要原地写入的场景,写放大问题尤其突出。
删除大文件也不会立即从底层回收空间。如果文件来自镜像只读层,删除操作只是在可写层生成一个 whiteout,底层数据仍然占用磁盘。因此仅看容器内文件大小往往会误判磁盘占用。可以通过 docker inspect 查看 GraphDriver 中记录的 LowerDir、UpperDir、MergedDir 实际路径,再结合 du 命令对比可写层增长情况。
docker inspect --format '{{json .GraphDriver.Data}}' my-container
为了减少 COW 的代价,有几种常用优化方式。第一,将高写入目录挂载到 Docker volume,让数据绕过容器可写层。第二,对日志等临时数据使用 tmpfs,避免写入磁盘。第三,避免把数据库、搜索引擎索引、消息队列数据目录打进镜像。第四,定期执行 docker system prune,清理已经停止的容器和未使用的镜像层。第五,如果使用 overlay2,尽量减少在可写层上进行大量原地更新,把可变数据外置。
四、常见存储驱动中的 COW 实现差异
Docker 支持多种存储驱动,不同驱动在实现 Copy-on-Write 时粒度不同。AUFS 是早期 Docker 默认驱动,它支持多个底层分支和可写分支,文件级 COW 行为与 OverlayFS 类似,但需要内核补丁,一直没有进入 Linux 主线。DeviceMapper 则采用块级 COW,基于 thin-provisioning 和 snapshot 机制,默认按 64KB 块分配空间,修改一个块只复制该块,对大文件的随机写可能更节省空间,但元数据开销和配置复杂度更高。
OverlayFS 的 overlay2 驱动是当前 Linux 内核 4.0 以上环境的主流选择。它比早期 overlay 驱动支持更多的 lower 层,并且原生使用内核联合文件系统,不依赖额外补丁。Btrfs 和 ZFS 则是另一种思路,它们直接利用文件系统原生的 copy-on-write、快照和子卷能力来实现镜像层,适合已经深度使用这些文件系统的场景。不同驱动在写放大、删除延迟、页缓存效率上存在差异,因此排查容器 IO 问题时,需要先确认 docker info 显示的 Storage Driver 类型。
五、镜像构建中的 COW 隐藏成本
COW 不仅影响运行期,也会影响镜像构建。Dockerfile 中每一条 RUN 指令都会生成一个新的只读层。如果先复制一个 1GB 文件到镜像,再在下一条指令中删除它,最终镜像体积并不会减少,因为前一层仍然保存着这个文件,后一层只是添加了 whiteout。构建时应尽量把下载、解压、清理等操作合并到同一条 RUN 命令中,避免产生无用的中间层。
另外,镜像层一旦生成就不能修改。即使后续层覆盖了之前的配置,旧数据依然存在。这种不可变性保证了镜像可复用和可追踪,但也要求开发者在构建阶段就注意层级规划。对于数据频繁变化的场景,最佳实践仍然是把数据放在 volume 中,让 COW 只负责应用代码和依赖文件,而不是承载业务数据。
容器文件系统Copy-on-WriteOverlayFS修改时间:2026-08-26 20:01:55