存储驱动程序
要有效地使用存储驱动程序,了解 Docker 如何构建和 存储镜像,以及容器如何使用这些镜像。您可以使用这个 信息,以便就保存数据的最佳方式做出明智的选择 您的应用程序,并在此过程中避免性能问题。
存储驱动程序与 Docker 卷
Docker 使用存储驱动程序来存储镜像层,并将数据存储在 容器的 writable 层。容器的可写层不会持久化 容器被删除后,但适合存储 在运行时生成。存储驱动程序针对空间效率进行了优化,但 (取决于存储驱动程序)写入速度低于本机文件系统 性能,尤其是对于使用写入时复制文件系统的存储驱动程序。 写入密集型应用程序(如数据库存储)会受到 性能开销,尤其是在只读 层。
将 Docker 卷用于写入密集型数据,这些数据必须在 容器的生命周期,以及必须在容器之间共享的数据。指 volumes 部分,了解如何使用 volumes 持久保存数据 并提高性能。
镜像和图层
Docker 镜像由一系列层构建而成。每个图层代表一个 指令。除最后一层外,每一层都是 只读。请考虑以下 Dockerfile:
# syntax=docker/dockerfile:1
FROM ubuntu:22.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 包含四个命令。修改文件系统创建的命令
一个层。这FROM
语句首先从ubuntu:22.04
镜像。这LABEL
命令仅修改镜像的元数据,而不会生成
新图层。这COPY
command 从 Docker 客户端的当前
目录。第一个RUN
命令使用make
命令
并将结果写入新层。第二个RUN
命令删除缓存
目录中,并将结果写入新层。最后,CMD
指令
指定要在容器内运行的命令,该命令仅修改
image 的元数据,它不会生成 image 层。
每个层只是与前一层的一组差异。请注意,添加和删除文件都会产生一个新图层。在上面的示例中,
这$HOME/.cache
目录已删除,但仍可在
previous layer 并加起来等于镜像的总大小。请参阅编写 Dockerfile 和使用多阶段构建的最佳实践部分,了解如何优化 Dockerfile 以获得高效的镜像。
这些层彼此堆叠在一起。创建新容器时,
您可以在底层图层上添加新的可写图层。这一层通常是
称为 “容器层”。对正在运行的容器所做的所有更改,例如
写入新文件、修改现有文件和删除文件将写入
这个薄的可写容器层。下图显示了基于
在ubuntu:15.04
镜像。

存储驱动程序处理有关这些层与 彼此。可以使用不同的存储驱动程序,这些驱动程序具有优势 以及不同情况下的缺点。
容器和图层
容器和镜像之间的主要区别在于顶部可写层。 对容器的所有添加新数据或修改现有数据的写入都存储在 this writable 层。删除容器时,可写层也会 删除。底层镜像保持不变。
因为每个容器都有自己的可写容器层,并且所有更改都是 存储在此容器层中,多个容器可以共享对同一 底层镜像,但具有自己的数据状态。下图显示 多个容器共享同一个 Ubuntu 15.04 镜像。

Docker 使用存储驱动程序来管理镜像层的内容,而 可写容器层。每个存储驱动程序都处理实现 不同,但所有驱动程序都使用可堆叠镜像层和写入时复制 (CoW) 策略。
注意
如果您需要多个容器来共享访问权限,请使用 Docker 卷 完全相同的数据。请参阅 卷 部分以了解 关于卷。
磁盘上的容器大小
要查看正在运行的容器的大致大小,您可以使用docker ps -s
命令。两个不同的列与大小相关。
size
:用于 每个容器。virtual size
:只读镜像数据使用的数据量 由容器加上容器的可写层使用size
. 多个容器可以共享部分或全部只读 镜像数据。从同一镜像启动的两个容器共享 100% 的 只读数据,而两个具有不同镜像的容器具有层 共享这些公共层。因此,您不能只将 虚拟大小。这可能会高估磁盘总使用量 非平凡的数量。
磁盘上所有正在运行的容器使用的总磁盘空间是一些
每个容器的size
和virtual size
值。如果
多个容器从同一精确镜像开始,磁盘上的总大小为
这些容器将是 SUM (size
的容器)加上一个镜像大小
(virtual size
- size
).
这也不计算容器可以采用的以下额外方式 磁盘空间:
- 用于 logging-driver 存储的日志文件的磁盘空间。 如果您的容器生成了大量日志记录,这可能并非易事 未配置数据和日志轮换。
- 容器使用的卷和绑定挂载。
- 用于容器配置文件的磁盘空间,通常为 小。
- 写入磁盘的内存(如果启用了交换)。
- Checkpoints(检查点),如果您使用的是实验性检查点/恢复功能。
写入时复制 (CoW) 策略
写入时复制是一种共享和复制文件以实现最高效率的策略。 如果文件或目录存在于镜像的较低层中,并且另一个 层(包括可写层)需要读取访问权限,它只使用 existing 文件。另一个图层第一次需要修改文件时(当 构建镜像或运行容器),文件将复制到该层中 和修改。这样可以最大限度地减少 I/O 和每个后续层的大小。 下面将更深入地解释这些优势。
共享可提升较小的镜像
当您使用docker pull
从存储库中拉取镜像,或者当您
从本地尚不存在的镜像创建容器,每个层都是
单独下拉,并存储在 Docker 的本地存储区域,即
通常/var/lib/docker/
在 Linux 主机上。您可以看到这些层被拉取
在此示例中:
$ docker pull ubuntu:22.04
22.04: Pulling from library/ubuntu
f476d66f5408: Pull complete
8882c27f669e: Pull complete
d9af21273955: Pull complete
f5029279ec12: Pull complete
Digest: sha256:6120be6a2b7ce665d0cbddc3ce6eae60fe94637c6a66985312d1f02f63cc0bcd
Status: Downloaded newer image for ubuntu:22.04
docker.io/library/ubuntu:22.04
这些层中的每一个都存储在 Docker 主机的
本地存储区域。要检查文件系统上的层,请列出内容
之/var/lib/docker/<storage-driver>
.此示例使用overlay2
存储驱动程序:
$ 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
,但有一些额外的
层:
# 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
命令验证共享的
图层是相同的。
创建新目录
cow-test/
并换上它。在
cow-test/
,创建一个名为hello.sh
包含以下内容。#!/usr/bin/env bash echo "Hello world"
将上面第一个 Dockerfile 的内容复制到一个名为
Dockerfile.base
.将上面第二个 Dockerfile 的内容复制到一个名为
Dockerfile
.在
cow-test/
目录中,构建第一个镜像。别忘了 包括最终的.
在命令中。这将PATH
,它告诉 用于查找需要添加到镜像的任何文件的 Docker。$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base . [+] Building 6.0s (11/11) FINISHED => [internal] load build definition from Dockerfile.base 0.4s => => transferring dockerfile: 116B 0.0s => [internal] load .dockerignore 0.3s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:1 1.5s => [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s => [internal] load .dockerignore 0.0s => [internal] load build definition from Dockerfile.base 0.0s => [internal] load metadata for docker.io/library/alpine:latest 0.0s => CACHED [1/2] FROM docker.io/library/alpine 0.0s => [2/2] RUN apk add --no-cache bash 3.1s => exporting to image 0.2s => => exporting layers 0.2s => => writing image sha256:da3cf8df55ee9777ddcd5afc40fffc3ead816bda99430bad2257de4459625eaa 0.0s => => naming to docker.io/acme/my-base-image:1.0 0.0s
构建第二个镜像。
$ docker build -t acme/my-final-image:1.0 -f Dockerfile . [+] Building 3.6s (12/12) FINISHED => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 156B 0.0s => [internal] load .dockerignore 0.1s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:1 0.5s => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s => [internal] load .dockerignore 0.0s => [internal] load build definition from Dockerfile 0.0s => [internal] load metadata for docker.io/acme/my-base-image:1.0 0.0s => [internal] load build context 0.2s => => transferring context: 340B 0.0s => [1/3] FROM docker.io/acme/my-base-image:1.0 0.2s => [2/3] COPY . /app 0.1s => [3/3] RUN chmod +x /app/hello.sh 0.4s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:8bd85c42fa7ff6b33902ada7dcefaaae112bf5673873a089d73583b0074313dd 0.0s => => naming to docker.io/acme/my-final-image:1.0 0.0s
查看镜像的大小。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE acme/my-final-image 1.0 8bd85c42fa7f About a minute ago 7.75MB acme/my-base-image 1.0 da3cf8df55ee 2 minutes ago 7.75MB
查看每张图片的历史记录。
$ docker image history acme/my-base-image:1.0 IMAGE CREATED CREATED BY SIZE COMMENT da3cf8df55ee 5 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0 <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB
某些步骤没有大小 (
0B
),并且是仅元数据更改,这些更改 不生成镜像图层,并且不占用除元数据以外的任何大小 本身。上面的输出显示此镜像由 2 个镜像层组成。$ docker image history acme/my-final-image:1.0 IMAGE CREATED CREATED BY SIZE COMMENT 8bd85c42fa7f 3 minutes ago CMD ["/bin/sh" "-c" "/app/hello.sh"] 0B buildkit.dockerfile.v0 <missing> 3 minutes ago RUN /bin/sh -c chmod +x /app/hello.sh # buil… 39B buildkit.dockerfile.v0 <missing> 3 minutes ago COPY . /app # buildkit 222B buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0 <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB
请注意,第一张镜像的所有步骤也包含在最终的 镜像。最终镜像包括第一个镜像中的两个图层,以及 在第二个镜像中添加的两个图层。
这
<missing>
行中的docker history
输出表示这些 steps 要么构建在另一个系统上,要么是alpine
镜像 它是从 Docker Hub 拉取的,或者是使用 BuildKit 作为构建器构建的。 在 BuildKit 之前,“经典”构建器会生成一个新的“中间” image 用于缓存目的,以及IMAGE
列将显示 该镜像的 ID。BuildKit 使用自己的缓存机制,不再需要中间 镜像进行缓存。请参阅 BuildKit 以了解有关 BuildKit 中的其他增强功能的更多信息。
查看每张镜像的图层
使用
docker image inspect
命令查看 图层:$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-base-image:1.0 [ "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf", "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a" ]
$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-final-image:1.0 [ "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf", "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a", "sha256:cc644054967e516db4689b5282ee98e4bc4b11ea2255c9630309f559ab96562e", "sha256:e84fb818852626e89a09f5143dbc31fe7f0e0a6a24cd8d2eb68062b904337af4" ]
请注意,两个镜像中的前两个图层相同。第二个 image 添加两个附加图层。共享镜像图层仅存储一次 在
/var/lib/docker/
,并在推送和拉取镜像时共享 添加到镜像注册表中。因此,共享镜像层可以减少网络 bandwidth 和 storage 的提示
使用
--format
选择。上面的示例使用
docker image inspect
命令与--format
选项查看图层 ID,格式为 JSON 数组。这--format
Docker 命令上的选项可能是一项强大的功能,允许您 从输出中提取特定信息并设置其格式,而无需 其他工具,例如awk
或sed
.了解有关格式设置的更多信息 使用--format
标志,请参阅 format command and log output 部分。 我们还使用jq
效用以提高可读性。
复制使容器高效
启动容器时,会在 其他层。容器对文件系统所做的任何更改都会被存储 这里。容器未更改的任何文件都不会复制到此可写对象 层。这意味着可写层尽可能小。
修改容器中的现有文件时,存储驱动程序会执行
copy-on-write作。涉及的具体步骤取决于具体的
storage 驱动程序。对于overlay2
驱动程序中,写入时复制作将遵循
这个粗略的顺序:
- 在镜像图层中搜索要更新的文件。流程开始 在最新的图层上,一次一层地向下工作到基础图层。 找到结果后,它们将被添加到缓存中以加快未来的作速度。
- 执行
copy_up
对找到的文件的第一个副本执行作,将 将文件复制到容器的 writable 层。 - 将对文件的此副本进行任何修改,并且容器不能 查看存在于 Lower Layer 中的文件的只读副本。
Btrfs、ZFS 和其他驱动程序以不同的方式处理写入时复制。您可以 稍后在详细的 描述。
写入大量数据的容器比容器占用更多的空间
没有。这是因为大多数写入作都会占用
容器的薄可写顶层。请注意,更改文件的元数据
例如,更改文件权限或文件所有权也可能导致
在copy_up
作,因此将文件复制到可写层。
提示
将卷用于写入密集型应用程序。
不要将数据存储在写入密集型应用程序的容器中。这样 应用程序(例如写入密集型数据库)已知为 特别是当预先存在的数据以只读方式存在时,尤其存在问题 层。
相反,请使用 Docker 卷,这些卷独立于正在运行的容器 并且设计为高效的 I/O。此外,还可以共享卷 在容器之间,并且不要增加容器的可写对象的大小 层。请参阅 使用卷 部分以了解 卷。
一个copy_up
作可能会产生明显的性能开销。此开销
根据正在使用的存储驱动程序而有所不同。大文件、
大量层和深目录树可以使影响更加明显。
这可以通过以下事实得到缓解:copy_up
作仅发生在第一个
修改给定文件的时间。
为了验证写入时复制的工作方式,以下过程将启动 5
基于acme/my-final-image:1.0
镜像,以及
检查它们占用了多少空间。
从 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 40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107 3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc 939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39 cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
运行
docker ps
命令与--size
验证 5 个容器的选项 正在运行,并查看每个容器的大小。$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}" CONTAINER ID IMAGE NAMES SIZE cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB) 939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB) 3ed3c1a10430 acme/my-final-image:1.0 my_container_3 0B (virtual 7.75MB) a5ff32e2b551 acme/my-final-image:1.0 my_container_2 0B (virtual 7.75MB) 40ebdd763416 acme/my-final-image:1.0 my_container_1 0B (virtual 7.75MB)
上面的输出显示所有容器共享镜像的只读层 (7.75MB),但没有数据写入容器的文件系统,因此没有额外的 storage 用于容器。
注意
此步骤需要 Linux 计算机,在 Docker Desktop 上不起作用,因为 它需要访问 Docker 守护程序的文件存储。
虽然
docker ps
提供有关磁盘空间的信息 由容器的可写层使用,它不包含信息 关于为每个容器存储的元数据和日志文件。通过浏览 Docker Daemon 的存储,可以获得更多详细信息 位置 (
/var/lib/docker
默认情况下)。$ sudo du -sh /var/lib/docker/containers/* 36K /var/lib/docker/containers/3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc 36K /var/lib/docker/containers/40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c 36K /var/lib/docker/containers/939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39 36K /var/lib/docker/containers/a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107 36K /var/lib/docker/containers/cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
这些容器中的每一个在文件系统上只占用 36k 的空间。
按容器存储
要演示这一点,请运行以下命令,将单词 'hello' 写入 容器中容器的可写层上的文件
my_container_1
,my_container_2
和my_container_3
:$ for i in {1..3}; do docker exec my_container_$i sh -c 'printf hello > /out.txt'; done
运行
docker ps
命令显示这些容器 现在每个使用 5 个字节。此数据对于每个容器都是唯一的,而不是 共享。容器的只读层不受影响,并且仍然 由所有容器共享。$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}" CONTAINER ID IMAGE NAMES SIZE cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB) 939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB) 3ed3c1a10430 acme/my-final-image:1.0 my_container_3 5B (virtual 7.75MB) a5ff32e2b551 acme/my-final-image:1.0 my_container_2 5B (virtual 7.75MB) 40ebdd763416 acme/my-final-image:1.0 my_container_1 5B (virtual 7.75MB)
前面的示例说明了 copy-on-write 文件系统如何帮助 容器高效。写入时复制不仅可以节省空间,还可以 减少容器启动时间。当您创建一个容器(或多个 containers 的 Git Git 中),Docker 只需要创建瘦可写 容器层。
如果 Docker 每次都必须创建底层镜像堆栈的完整副本
创建新容器,容器创建时间和使用的磁盘空间将为
显著增加。这类似于虚拟机
工作,每个虚拟机使用一个或多个虚拟磁盘。这vfs
存储不提供 CoW 文件系统或其他优化。使用此存储时
驱动程序中,将为每个容器创建镜像数据的完整副本。