写入时复制(Copy-on-Write,简称 CoW)是一种用于共享与复制文件的数据管理机制,目标是在保证可修改性的同时,尽可能提升存储与 I/O 效率。在 Docker 中,如果某个文件或目录已经存在于镜像的底层,新的上层在只读访问时会直接复用该文件,而不会额外复制。只有当上层第一次需要修改该文件时,系统才会先将文件复制到当前层,再执行改动。借助这种方式,Docker 能有效减少重复数据、降低后续层的体积,并控制磁盘读写开销。
这种共享机制尤其适合构建体积更小、复用率更高的镜像。
当你执行 docker pull 从仓库拉取镜像,或基于本地尚不存在的镜像创建容器时,镜像的每一层都会被单独下载,并保存在 Docker 的本地存储区域中。在 Linux 主机上,这个位置通常是 /var/lib/docker/。下面是一个拉取镜像时的示例输出:
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
f476d66f5408: Pull complete
8882c27f669e: Pull complete
d9af21273955: Pull complete
f5029279ec12: Pull complete
Digest: sha256:ab6cb8de3ad7bb33e2534677f865008535427390b117d7939193f8d1a6613e34
Status: Downloaded newer image for ubuntu:18.04
这些层会分别存放在 Docker 主机本地存储中的独立目录里。你可以查看 /var/lib/docker/ 来了解实际的层存储情况。以下示例使用的是 overlay2 存储驱动:
$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l
需要注意的是,这些目录名并不直接等同于镜像层 ID。
现在假设你有两个不同的 Dockerfile。先用第一个 Dockerfile 构建一个名为 acme/my-base-image:1.0 的镜像:
FROM ubuntu:18.04
COPY . /app
第二个镜像则基于 acme/my-base-image:1.0 继续构建,并额外增加一层:
FROM acme/my-base-image:1.0
CMD /app/hello.sh
第二个镜像会包含第一个镜像的全部层,同时新增由 CMD 指令产生的一层,以及容器运行时使用的可读写层。由于 Docker 已经拥有基础镜像中的层,因此再次构建或拉取时,不需要重复下载或保存相同内容。也就是说,两个镜像会共享它们共同依赖的底层数据。
如果你分别根据这两个 Dockerfile 构建镜像,可以通过 docker image ls 和 docker history 来验证它们是否共享了相同的层。通常你会看到,公共层的哈希值是一致的。
可以先创建一个新的目录 cow-test/,然后进入该目录。
在 cow-test/ 中,新建一个名为 hello.sh 的文件,内容如下:
#!/bin/sh
echo "Hello world"
保存后,为这个脚本添加可执行权限:
chmod +x hello.sh
接着,把前面第一个 Dockerfile 的内容保存到一个名为 Dockerfile.base 的文件中。
再把第二个 Dockerfile 的内容保存到名为 Dockerfile 的文件里。
在 cow-test/ 目录下,先构建第一个镜像。注意命令末尾要带上 .,它表示当前目录是构建上下文,Docker 会从这里查找需要复制到镜像中的文件。
$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base .
然后构建第二个镜像:
$ docker build -t acme/my-final-image:1.0 -f Dockerfile .
构建完成后,可以查看镜像大小:
$ docker image ls
如果想进一步查看镜像由哪些层组成,可以执行:
$ docker history bd09118bcef6
你会发现,除了第二个镜像最顶层新增的一层外,其余层都与第一个镜像一致。这说明两者共享了相同的基础层,而这些共享层在 /var/lib/docker/ 中只会保存一次。实际上,这个新增顶层往往几乎不占空间,因为它没有修改文件内容,只是定义了容器启动时要执行的命令。
