存储是 Docker 中非常重要的部分,了解 Docker Storage drivers 中 image、layer 的概念和设计细节有利于更好的优化程序在 Docker 中的读写性能。本文记录了 Docker docs 中 About storage drivers 部分的内容。
Storage drivers versus Docker volumes
Storage drivers(存储驱动) | Docker volumes(卷) | |
解释 | Docker 使用 Storage drivers 程序来存储镜像层,并将数据存储在容器的可写层中。 容器的可写层在容器被删除后不会持久化,适合存储运行时产生的临时数据。 | Volumes 是一个或多个容器中的一个特别指定的目录,它绕过联合文件系统。 Volumes 可以持久保存数据,独立于容器的生命周期。 因此,当删除容器时,Docker 不会自动删除 Volumes,它也不会“垃圾收集”不再被容器引用的 Volumes。 |
特点 | 依赖于容器存在 | 独立于容器的生命周期 |
Images and layers
容器 image 由一系列 layer 构成, 在 Dockerfile 中 layer 由一个指令表示,除了最后一层 layer 之外的每一层 layer 都是只读的。
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py
Dockerfile 由四个指令构成,对文件系统修改的指令将创建一层 layer:
FROM 表示从 image ubuntu:18.04 创建一层 layer(又称为父镜像层,所有操作都基于此);
LABEL 仅修改 image 元数据,因此不创建新的 layer;
COPY 从 Docker 用户当前的目录下添加了一些文件;
第一个 RUN 使用 make 构建该应用,并将结果写入了一个新 layer;
第二个 RUN 使用 rm 移除了一个缓存目录,并将结果写入了一个新 layer;
CMD 指示在容器中运行,仅修改 image 元数据,因此不产生新 layer。
每一层 layer 都是前一层 layer 的一些变化,比如对文件的添加、移除会引入一个新的 layer,然而尽管目录 $HOME/.cache 被移除,但是仍然可以在上一层 layer 被访问,因此其大小也是被记入 image 总大小。(所以 Dockerfiles 的编写是有技巧的
这些层相互堆叠。 当创建一个新容器时,image 会在基础层之上添加一个新的可写层。 该层通常称为“容器层”。 对正在运行的容器所做的所有更改,例如写入新文件、修改现有文件和删除文件,都会写入这个薄的可写容器层。 下图显示了一个基于 ubuntu:15.04 镜像的容器。
Container and layers
容器和图像之间的主要区别在于顶部可写层。 添加新数据或修改现有数据的所有对容器的写入都存储在此可写层中。 当容器被删除时,可写层也被删除。 底层图像保持不变。
因为每个容器都有自己的可写容器层,并且所有的变化都存储在这个容器层中,所以多个容器可以共享对同一个底层镜像的访问,并且拥有自己的数据状态。 下图显示了多个容器共享同一个 Ubuntu 15.04 映像。
Docker 使用 Storage drivers 来管理镜像层和可写容器层的内容。 每个存储驱动程序以不同的方式处理实现,但所有驱动程序都使用可堆叠的图像层和写时复制 (CoW) 策略。
鉴于这种特点,所以多个容器的大小,不一定是单纯各个容器大小的累加,有可能是共享的 read-only image ➕ writable layer of each container。
The copy-on-write (CoW) strategy
Copy-on-write 是一种共享和复制文件以实现最大效率的策略。 如果一个文件或目录存在于镜像中的较低层,并且另一个层(包括可写层)需要对其进行读取访问,则它只使用现有文件。 其他层第一次需要修改文件时(构建镜像或运行容器时),文件被复制到该层并修改。 这最大限度地减少了 I/O 和每个后续层的大小。 这些优点将在下面更深入地解释。
Sharing promotes smaller images
当你使用 docker pull 从仓库拉下一个镜像,或者当你从一个本地尚不存在的镜像创建一个容器时,每一层都被单独拉下,并存储在 Docker 的本地存储区,通常是 /var /lib/docker/ 在 Linux 主机上。
$ 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/<storage-driver> 的内容。 此示例使用 overlay2 storage driver:
$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l
目录名称与层 ID 不对应。(这段没怎么看明白)
现在假设有两个不同的 Dockerfile,第一个叫 acme/my-base-image:1.0
# syntax=docker/dockerfile:1
FROM alpine
RUN apk add --no-cache bash
第二个基于 acme/my-base-image:1.0,但是有一些其他的 layer
# syntax=docker/dockerfile:1
FROM acme/my-base-image:1.0
COPY . /app
RUN chmod +x /app/hello.sh
CMD /app/hello.sh
第二个镜像包含第一个镜像的所有层,加上由 COPY 和 RUN 指令创建的新层,以及一个读写容器层。 Docker 已经拥有了第一个镜像的所有层,因此不需要再次拉取它们。 这两个图像共享它们共有的层。
如果从两个 Dockerfile 构建镜像,我们可以使用 docker image ls 和 docker image history 命令来验证共享层的加密 ID 是否相同。
Copying makes containers efficient
启动容器时,会在其他层之上添加一个薄的可写容器层。容器对文件系统所做的任何更改都存储在这里。为了保证可写层的轻量级,可写层仅复制更改的文件。
当容器中的现有文件被修改时,存储驱动程序会执行写时复制操作。涉及的具体步骤取决于特定的存储驱动程序。对于 overlay2、overlay 和 aufs 驱动程序,写时复制操作遵循以下粗略顺序:
- 在图像层中搜索要更新的文件。该过程从最新的层开始,一次一层地向下工作到基础层。找到结果后,会将它们添加到缓存中以加速未来的操作。
- 对找到的文件的第一个副本执行 copy_up 操作,将文件复制到容器的可写层。
- 对该文件的这个副本进行需要的修改,容器都看不到下层中存在的该文件的只读副本。
- Btrfs、ZFS 和其他驱动程序以不同方式处理写时复制。
写入大量数据的容器比单纯的容器将消耗更多空间。这是因为大量的写操作会消耗容器薄可写顶层中的新空间。请注意,更改文件的元数据,例如更改文件权限或文件所有权,也可能导致 copy_up 操作,从而将文件复制到可写层。
Tips:对于写入量大的应用程序,不应将数据存储在容器中。 而使用 Docker volumes (独立于容器)可以提高 I/O 效率。 此外,volumes 可以在容器之间共享,并且不会增加容器可写层的大小。
copy_up 操作会产生一个明显的性能开销。这种开销的大小取决于使用的是哪种存储驱动。大文件、大量的层和深的目录树会使影响更加明显。由于每个 copy_up 操作只在第一次修改给定文件时发生,这一点得到了缓解。
写时复制不仅节省空间,而且还减少了容器启动时间。 当你创建一个容器(或同一个镜像的多个容器)时,Docker 只需要创建瘦可写容器层。
如果 Docker 每次创建新容器时都必须制作底层镜像堆栈的完整副本,那么容器创建时间和使用的磁盘空间将显着增加。 这类似于虚拟机的工作方式,每个虚拟机有一个或多个虚拟磁盘。 vfs 存储不提供 CoW 文件系统或其他优化。 使用此存储驱动程序时,将为每个容器创建映像数据的完整副本。