关于存储驱动持久化数据并避免性能问题的最佳方法。

为了有效地使用存储驱动程序,了解Docker如何构建和存储镜像,以及容器如何使用这些镜像非常重要。您可以使用这些信息做出的明智选择,关于应用程序持久保存数据并避免性能问题的最佳方法。

存储驱动程序使您可以在容器的可写层中创建数据。删除容器后,这些文件将不会保留,并且读写速度都低于本机文件系统性能。

注意:已知有问题的操作包括写密集型数据库存储,尤其是在只写层中有预先存在的数据时。本文档中提供了更多详细信息。

了解如何使用卷存储数据并提高性能。

镜像和层

Docker镜像由一系列的层组成。每层代表镜像的Dockerfile中的一条指令。除最后一层外的每一层都是只读的。考虑以下Dockerfile:

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

该Dockerfile包含四个命令,每个命令创建一个层。 FROM语句从ubuntu:18.04镜像创建一个层开始。 COPY命令从Docker客户端的当前目录添加一些文件。 RUN命令使用make命令构建您的应用程序。最后,最后一层指定在容器中运行什么命令。

每一层只是与上一层不同的一个集合。这些层彼此堆叠。创建新容器时,可以在基础层之上添加一个新的可写层。该层通常称为“容器层”。对运行中的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。下图显示了基于Ubuntu 18.04镜像的容器。

基于Ubuntu镜像的容器层

存储驱动程序处理有关这些层相互交互的方式的详细信息。提供了不同的存储驱动程序,它们在不同情况下各有利弊。

容器和镜像

容器和镜像之间的主要区别是可写顶层。在容器中添加新数据或修改现有数据的所有写操作都存储在此可写层中。删除容器后,可写层也会被删除。基础镜像保持不变。

因为每个容器都有其自己的可写容器层,并且所有更改都存储在该容器层中,所以多个容器可以共享对同一基础镜像的访问,但具有自己的数据状态。下图显示了多个共享同一Ubuntu 18.04镜像的容器。

容器共享同一个镜像

注意:如果您需要多个镜像共享对相同数据的访问,请将该数据存储在Docker卷中,并将其挂载到您的容器中。

Docker使用存储驱动程序来管理镜像层和可写容器层的内容。每个存储驱动程序实现的处理方式不同,但是所有驱动程序都使用可堆叠的镜像层和copy-on-write(CoW)策略。

磁盘上的容器大小

要查看正在运行的容器的大概大小,可以使用docker ps -s命令。有两个不同的列与大小有关。

  • size:用于每个容器的可写层的数据量(在磁盘上的)。

  • virtual size:容器使用的只读镜像数据的数据量加上容器的可写层size。多个容器可以共享一些或所有只读镜像数据。从同一镜像开始的两个容器共享100%的只读数据,而具有不同镜像的两个容器(具有公共层)共享这些公共层。因此,您不能只计算虚拟大小。这高估了总磁盘使用量,可能是一个不小的数目。

磁盘上所有正在运行的容器使用的磁盘总空间是每个容器的sizevirtual size值的某种组合。如果多个容器从完全相同的镜像开始,则这些容器在磁盘上的总大小将为:容器的size的和加上一个镜像大小(virtual size-size)。

这也不包括容器可以占用磁盘空间的以下其他方式:

  • 用于日志文件的磁盘空间,如果使用json-file日志记录驱动程序。如果您的容器生成大量的日志数据并且未配置日志覆盖,那么这可能占用非常多磁盘空间。
  • 容器使用的卷和绑定挂载。
  • 用于容器的配置文件的磁盘空间,通常较小。
  • 内存写入磁盘(如果启用了交换)。
  • 检查点,如果您使用的是实验性检查点/恢复功能。

写入时复制(copy-on-write简称CoW)策略

写入时复制是一种共享和复制文件的策略,可最大程度地提高效率。如果文件或目录位于镜像的较低层中,而另一层(包括可写层)需要对其进行读取访问,则它仅使用现有文件。另一层第一次需要修改文件时(在构建镜像或运行容器时),将文件复制到该层并进行修改。这样可以将I/O和每个后续层的大小最小化。这些优点将在下面更深入地说明。

共享可减小镜像

当您使用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/<storage-driver>的内容。此示例使用overlay2存储驱动程序:

$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l

目录名称与层ID不对应(从Docker 1.10开始就是如此)。

现在,假设您有两个不同的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已经具有第一个镜像中的所有层,因此不需要再次将其拉出。这两个镜像共享它们共有的任何层。

如果您从两个Dockerfiles构建镜像,则可以使用docker image lsdocker history命令来验证共享层加密的ID是否相同。

  1. 新建一个目录cow-test/并切换到该目录。
  2. cow-test/中,创建一个名为hello.sh的新文件,其内容如下:
#!/bin/sh
echo "Hello world"

保存文件,并使其可执行:

chmod + x hello.sh
  1. 将上面第一个Dockerfile的内容复制到名为Dockerfile.base的新文件中。

  2. 将上面第二个Dockerfile的内容复制到名为Dockerfile的新文件中。

  3. cow-test/目录中,构建第一个镜像。不要忘记在命令中包含最后的.。它设置了PATH,它告诉Docker在哪里寻找需要添加到镜像的任何文件。

$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base .
Sending build context to Docker daemon  812.4MB
Step 1/2 : FROM ubuntu:18.04
 ---> d131e0fa2585
Step 2/2 : COPY . /app
 ---> Using cache
 ---> bd09118bcef6
Successfully built bd09118bcef6
Successfully tagged acme/my-base-image:1.0
  1. 构建第二个镜像。
$ docker build -t acme/my-final-image:1.0 -f Dockerfile .

Sending build context to Docker daemon  4.096kB
Step 1/2 : FROM acme/my-base-image:1.0
 ---> bd09118bcef6
Step 2/2 : CMD /app/hello.sh
 ---> Running in a07b694759ba
 ---> dbf995fc07ff
Removing intermediate container a07b694759ba
Successfully built dbf995fc07ff
Successfully tagged acme/my-final-image:1.0
  1. 检查镜像大小:
$ docker image ls

REPOSITORY                         TAG                     IMAGE ID            CREATED             SIZE
acme/my-final-image                1.0                     dbf995fc07ff        58 seconds ago      103MB
acme/my-base-image                 1.0                     bd09118bcef6        3 minutes ago       103MB
  1. 检出组成每个镜像的层:
$ docker history bd09118bcef6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
bd09118bcef6        4 minutes ago       /bin/sh -c #(nop) COPY dir:35a7eb158c1504e...   100B                
d131e0fa2585        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           3 months ago        /bin/sh -c mkdir -p /run/systemd && echo '...   7B                  
<missing>           3 months ago        /bin/sh -c sed -i 's/^#\s*\(deb.*universe\...   2.78kB              
<missing>           3 months ago        /bin/sh -c rm -rf /var/lib/apt/lists/*          0B                  
<missing>           3 months ago        /bin/sh -c set -xe   && echo '#!/bin/sh' >...   745B                
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:eef57983bd66e3a...   103MB
$ docker history dbf995fc07ff

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
dbf995fc07ff        3 minutes ago       /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "/a...   0B                  
bd09118bcef6        5 minutes ago       /bin/sh -c #(nop) COPY dir:35a7eb158c1504e...   100B                
d131e0fa2585        3 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           3 months ago        /bin/sh -c mkdir -p /run/systemd && echo '...   7B                  
<missing>           3 months ago        /bin/sh -c sed -i 's/^#\s*\(deb.*universe\...   2.78kB              
<missing>           3 months ago        /bin/sh -c rm -rf /var/lib/apt/lists/*          0B                  
<missing>           3 months ago        /bin/sh -c set -xe   && echo '#!/bin/sh' >...   745B                
<missing>           3 months ago        /bin/sh -c #(nop) ADD file:eef57983bd66e3a...   103MB

请注意,除了第二个镜像的顶层以外,所有层都是相同的。所有其他层在两个镜像之间共享,并且仅在/var/lib/docker/中存储一次。实际上,新层根本不占用任何空间,因为它不更改任何文件,而仅运行命令。

注意docker history输出中的<missing>行表明这些层是在另一个系统上构建的,并且在本地不可用。这可以忽略。

复制使容器高效

启动容器时,会在其他层之上添加一个薄的可写容器层。容器对文件系统所做的任何更改都存储在这里。容器未更改的任何文件都不会复制到此可写层。这意味着可写层尽可能小。

当容器中的现有文件被修改时,存储驱动程序将执行写时复制操作。涉及的具体步骤取决于特定的存储驱动程序。对于aufsoverlayoverlay2驱动程序,写时复制操作遵循以下大致步骤:

  • 在镜像层中搜索要更新的文件。该过程从最新层开始,一层一层的像下找。找到结果后,会将它们添加到缓存中,以加快将来的操作。

  • 在找到的文件的第一个副本上执行copy_up操作,将文件复制到容器的可写层。

  • 任何修改都对其文件的此副本进行,并且容器看不到低层中文件的只读副本。

Btrfs,ZFS和其他驱动程序以不同方式处理写时复制。您可以在稍后的详细描述中阅读有关这些驱动程序使用的方法的更多信息。

写入大量数据的容器比不写入数据的容器消耗更多的空间。这是因为大多数写操作会占用容器薄的可写顶层中的新空间。

注意:对于大量写应用程序,您不应将数据存储在容器中。而是使用Docker卷,它们独立于正在运行的容器,并且旨在提高I/O效率。此外,卷可以在容器之间共享,并且不会增加容器可写层的大小。

copy_up操作可能会导致明显的性能开销。此开销因所使用的存储驱动程序而异。大文件,许多层和深层目录树可以使影响更加明显。每个copy_up操作仅在第一次修改给定文件时才发生,这可以缓解这种情况。

为了验证写时复制的工作方式,以下过程根据我们之前构建的acme/my-final-image:1.0镜像生成了5个容器,并检查了它们占用了多少空间。

注意:此过程不适用于Docker Desktop for Mac或Docker Desktop for Windows。

  1. 从Docker主机上的终端,运行以下docker run命令。最后的字符串是每个容器的ID。
$ docker run -dit --name my_container_1 acme/my-final-image:1.0 bash && docker run -dit --name my_container_2 acme/my-final-image:1.0 bash && docker run -dit --name my_container_3 acme/my-final-image:1.0 bash && docker run -dit --name my_container_4 acme/my-final-image:1.0 bash && docker run -dit --name my_container_5 acme/my-final-image:1.0 bash

c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513
1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
  1. 运行docker ps命令以验证5个容器正在运行。
CONTAINER ID      IMAGE                     COMMAND     CREATED              STATUS              PORTS      NAMES
1a174fc216cc      acme/my-final-image:1.0   "bash"      About a minute ago   Up About a minute              my_container_5
38fa94212a41      acme/my-final-image:1.0   "bash"      About a minute ago   Up About a minute              my_container_4
1e7264576d78      acme/my-final-image:1.0   "bash"      About a minute ago   Up About a minute              my_container_3
dcad7101795e      acme/my-final-image:1.0   "bash"      About a minute ago   Up About a minute              my_container_2
c36785c423ec      acme/my-final-image:1.0   "bash"      About a minute ago   Up About a minute              my_container_1
  1. 列出本地存储区的内容。
$ sudo ls /var/lib/docker/containers

1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765
1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c
38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544
c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409
dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513    ```

4. 现在检查它们的大小:

$ sudo du -sh /var/lib/docker/containers/*

32K /var/lib/docker/containers/1a174fc216cccf18ec7d4fe14e008e30130b11ede0f0f94a87982e310cf2e765 32K /var/lib/docker/containers/1e7264576d78a3134fbaf7829bc24b1d96017cf2bc046b7cd8b08b5775c33d0c 32K /var/lib/docker/containers/38fa94212a419a082e6a6b87a8e2ec4a44dd327d7069b85892a707e3fc818544 32K /var/lib/docker/containers/c36785c423ec7e0422b2af7364a7ba4da6146cbba7981a0951fcc3fa0430c409 32K /var/lib/docker/containers/dcad7101795e4206e637d9358a818e5c32e13b349e62b00bf05cd5a4343ea513


这些容器中每个仅占用文件系统上32k的空间。

写时复制不仅可以节省空间,还可以缩短启动时间。当启动一个容器(或从同一镜像运行多个容器)时,Docker只需要创建可写的容器层。

如果Docker每次启动新容器都必须复制基础镜像堆栈的完整副本,则容器启动时间和使用的磁盘空间将大大增加。这将类似于虚拟机的工作方式,每个虚拟机具有一个或多个虚拟磁盘。

## 相关信息

* [卷](/docker/docker-guides/use-volumes)
* [选择存储驱动程序](/docker/docker-guides/docker-storage-drivers)