在云原生和微服务应用领域,保存数据的有状态应用变得越来越重要。Docker 是这一领域的重要基础设施技术,因此我们将在本章中把注意力转向研究 Docker 如何处理写持久数据的应用。
我们将把这一章分成通常的三个部分:
- TLDR
- 深潜
- 命令
有两大类数据——持久数据和非持久数据。
持久化就是你需要保留的数据。比如;客户记录、财务数据、研究结果、审计日志,甚至某些类型的应用日志数据。非持久性是不需要保留的数据。
这两者都很重要,Docker 对这两者都有解决方案。
为了处理非持久数据,每个 Docker 容器都有自己的非持久存储。这是为每个容器自动创建的,并与容器的生命周期紧密耦合。因此,删除容器将会删除存储和其中的任何数据。
为了处理持久数据,容器需要将其存储在卷中。卷是独立的对象,其生命周期与容器分离。这意味着您可以独立创建和管理卷,并且它们不受任何容器生命周期的限制。最终,您可以删除正在使用卷的容器,而卷不会被删除。
那是 TLDR。让我们仔细看看。
有一种流行的观点认为,容器对于保存数据的有状态应用来说并不好。几年前这是真的。然而,事情正在发生变化,现在已经有一些技术使得容器成为许多有状态应用的可行选择。
我是说容器是所有有状态应用的最佳解决方案吗?不。但是,我们将看到容器处理持久和非持久数据的一些方式,您可能会发现很难看到与虚拟机的许多区别。
我们将从非持久性数据开始。
容器被设计成不可变的。这只是一个时髦的词,意思是只读的——在容器部署后最好不要改变它的配置。如果有什么东西坏了或者你需要改变什么东西,你应该创建一个包含修复/更新的新容器,并部署它来代替旧容器。您不应该登录到一个正在运行的容器并进行配置更改!
然而,许多应用需要读写文件系统才能简单地运行——它们甚至不能在只读文件系统上运行。这意味着它不像使容器完全只读那么简单。每个 Docker 容器都是通过在其所基于的只读映像之上添加一个薄读写层来创建的。图 13.1 显示了共享一个只读映像的两个运行容器。
Figure 13.1 Ephemeral container storage
可写容器层存在于 Docker 主机的文件系统中,您会听到它被称为各种名称。这些包括本地存储、临时存储和图形驱动程序存储。它通常位于以下位置的 Docker 主机上:
- Linux Docker 主机:
/var/lib/docker/<storage-driver>/...
- Windows Docker 主机:
C:\ProgramData\Docker\windowsfilter\...
这个薄的可写层是容器不可分割的一部分,支持所有读/写操作。如果您或应用更新文件或添加新文件,它们将被写入该层。然而,它与容器的生命周期紧密耦合——它在创建容器时被创建,在删除容器时被删除。它与容器一起被删除的事实意味着它不是您需要保留(保存)的重要数据的选项。
如果您的容器没有创建持久数据,这个薄的可写层本地存储就可以了,您可以开始了。但是,如果您的容器需要保存数据,您需要阅读下一节。
在进入下一部分之前,最后说一句话。
本地存储的可写层由存储驱动程序在每个 Docker 主机上管理(不要与卷驱动程序混淆)。如果您在 Linux 上运行 Docker 生产,您需要确保将正确的存储驱动程序与您的 Docker 主机上的 Linux 发行版相匹配。使用以下列表作为指南:
- 红帽企业版 Linux: 在运行 Docker 17.06 或更高版本的现代版 RHEL 上使用
overlay2
驱动程序。使用旧版本的devicemapper
驱动程序。这适用于甲骨文 Linux 和其他红帽相关的上下游发行版。 - **乌班图:**使用
overlay2
或aufs
驱动程序。如果你使用的是 Linux 4.x 内核或更高版本,你应该选择overlay2
。 - **SUSE Linux 企业服务器:**使用
btrfs
存储驱动。 - Windows Windows 只有一个驱动,默认配置。
卷是将数据保存在容器中的推荐方式。这有三个主要原因:
- 卷是独立的对象,与容器的生命周期无关
- 卷可以映射到专用的外部存储系统
- 卷使不同 Docker 主机上的多个容器能够访问和共享相同的数据
在高级别上,您创建一个卷,然后创建一个容器并将该卷装入其中。该卷被装载到容器文件系统中的一个目录中,写入该目录的任何内容都存储在该卷中。如果删除容器,卷及其数据仍将存在。
图 13.2 显示了作为单独对象存在于容器外部的 Docker 卷。它在/data
装载到容器的文件系统中,写入/data
目录的任何数据都将存储在卷上,并在容器被删除后存在。
Figure 13.2 High-level view of volumes and containers
在图 13.2 中,/data
目录是 Docker 卷,可以映射到外部存储系统或 Docker 主机上的目录。无论哪种方式,它的生命周期都是与容器分离的。容器中的所有其他目录都使用 Docker 主机上本地存储区域中的精简可写容器层。
从卷到/data
目录的箭头显示为虚线,表示卷和容器之间的解耦关系。
在 Docker,卷是一等公民。其中,这意味着它们在 API 中是自己的对象,并且有自己的docker volume
子命令。
使用以下命令创建一个名为myvol
的新卷。
$ docker volume create myvol
myvol
默认情况下,Docker 使用内置的
local驱动程序创建新卷。顾名思义,使用
local驱动程序创建的卷只对与卷位于同一节点的容器可用。您可以使用
-d`标志指定不同的驱动程序。
第三方卷驱动程序作为插件提供。这些为 Docker 提供了对外部存储系统(如云存储服务)和内部存储系统(包括 SAN 或 NAS)的无缝访问。这如图 13.3 所示。
Figure 13.3 Plugging external storage into Docker
我们将在后面的章节中看到一个第三方驱动程序的例子。
现在创建了体积,您可以使用docker volume ls
命令查看它,并使用docker volume inspect
命令检查它。
$ docker volume ls
DRIVER VOLUME NAME
local myvol
$ docker volume inspect myvol
[
{
"CreatedAt": "2020-05-02T17:44:34Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/myvol/_data",
"Name": "myvol",
"Options": {},
"Scope": "local"
}
]
注意
Driver和
Scope都是
local。这意味着该卷是用
local驱动程序创建的,并且只对该 Docker 主机上的容器可用。
Mountpoint`属性告诉我们卷在 Docker 主机的文件系统中的位置。
所有使用local
驱动程序创建的卷在 Linux 上的/var/lib/docker/volumes
和 Windows 上的C:\ProgramData\Docker\volumes
下都有自己的目录。这意味着您可以在 Docker 主机的文件系统中看到它们。您甚至可以直接从 Docker 主机访问它们,尽管通常不建议这样做。我们在 Docker Compose 一章中展示了一个这样的例子——我们将一个文件直接复制到 Docker 主机上的卷目录中,该文件立即出现在容器内的卷中。
现在卷已经创建,它可以被一个或多个容器使用。我们一会儿将看到用法示例。
有两种方法可以删除 Docker 卷:
docker volume prune
docker volume rm
docker volume prune
将删除所有未装入容器或服务副本的卷,因此请谨慎使用! docker volume rm
可让您精确指定要删除的卷。这两个命令都不会删除容器或服务副本正在使用的卷。
由于myvol
卷没有使用,用prune
命令删除。
$ docker volume prune
WARNING! This will remove all volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
myvol
Total reclaimed space: 0B
`恭喜,您已经创建、检查并删除了 Docker 卷。你没有和容器互动。这证明了卷的独立性。
至此,您已经知道了创建、列出、检查和删除 Docker 卷的所有命令。但是,也可以使用VOLUME
指令通过 Dockerfiles 部署卷。格式为VOLUME <container-mount-point>
。有趣的是,在 Dockerfile 中定义卷时,不能指定主机上的目录。这是因为主机目录是不同的,取决于你的 Docker 主机运行的是什么操作系统——如果你在一个不存在的 Docker 主机上指定了一个目录,它可能会破坏你的构建。因此,在 Dockerfile 中定义卷需要您在部署时指定主机目录。
让我们看看如何将卷用于容器和服务。
这些示例将来自一个没有预先存在卷的系统,我们演示的所有内容都适用于 Linux 和 Windows。
使用以下命令创建一个新的独立容器,该容器装载一个名为bizvol
的卷。
Linux 示例:
$ docker container run -dit --name voltainer \
--mount source=bizvol,target=/vol \
alpine
`窗口示例:
对所有 Windows 示例使用 PowerShell,并注意使用倒勾(`)将命令拆分到多行。
> docker container run -dit --name voltainer `
--mount source=bizvol,target=c:\vol `
mcr.microsoft.com/powershell:nanoserver
该命令使用
--mount标志将名为“bizvol”的卷装入位于
/vol或
c:\vol的容器中。尽管系统上没有名为
bizvol`的卷,该命令还是成功完成。这引出了一个有趣的问题:
- 如果指定现有卷,Docker 将使用现有卷
- 如果您指定了一个不存在的卷,Docker 将为您创建它
在这种情况下,bizvol
不存在,所以 Docker 创建了它,并将其安装到新容器中。这意味着你可以用docker volume ls
看到它。
$ docker volume ls
DRIVER VOLUME NAME
local bizvol
`尽管容器和卷有不同的生命周期,但您不能删除容器正在使用的卷。试试看。
$ docker volume rm bizvol
Error response from daemon: remove bizvol: volume is in use - [b44d3f82...dd2029ca]
卷是全新的,所以没有任何数据。让我们将
exec放到容器上,并向其中写入一些数据。举了一个 Linux 的例子,如果你在 Windows 上执行,只需在命令的末尾用
pwsh.exe替换
sh`。所有其他命令将在 Linux 和 Windows 上工作。
$ docker container exec -it voltainer sh
/# echo "I promise to write a review of the book on Amazon" > /vol/file1
/# ls -l /vol
total 4
-rw-r--r-- 1 root root 50 Jan 12 13:49 file1
/# cat /vol/file1
I promise to write a review of the book on Amazon
键入
exit`返回到 Docker 主机的外壳,然后用以下命令删除容器。
$ docker container rm voltainer -f
voltainer
`即使容器被删除,卷仍然存在:
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
$ docker volume ls
DRIVER VOLUME NAME
local bizvol
`因为该卷仍然存在,所以您可以查看它在主机上的装载点,以检查数据是否仍然存在。
从 Docker 主机的终端运行以下命令。第一个将显示文件仍然存在,第二个将显示文件的内容。
如果你在 Windows 上跟随,一定要使用C:\ProgramData\Docker\volumes\bizvol\_data
目录。此外,这一步在 Mac 和 Windows 10 的 Docker Desktop 上不起作用。这是因为 Docker Desktop 在虚拟机内部运行 Docker,并且卷数据目录存在于虚拟机内部。
$ ls -l /var/lib/docker/volumes/bizvol/_data/
total 4
-rw-r--r-- 1 root root 50 Jan 12 14:25 file1
$ cat /var/lib/docker/volumes/bizvol/_data/file1
I promise to write a review of the book on Amazon
`太好了,数量和数据依然存在。
甚至可以将bizvol
卷装入新的服务或容器中。以下命令创建一个新的 Docker 服务,称为 hellcat,并将 bizvol 装载到/vol
处的服务副本中。您需要在群集模式下运行此命令才能工作。如果你在单引擎模式下运行,你可以使用docker container run
命令代替。
$ docker service create \
--name hellcat \
--mount source=bizvol,target=/vol \
alpine sleep 1d
overall progress: 1 out of 1 tasks
1/1: running [====================================>]
verify: Service converged
我们没有指定
--replicas`标志,所以只部署了一个服务副本。找到它在群中的哪个节点上运行。
$ docker service ps hellcat
ID NAME NODE DESIRED STATE CURRENT STATE
l3nh... hellcat.1 node1 Running Running 19 seconds ago
在本例中,副本运行在
node1上。登录
node1`获取服务副本容器的标识。
node1$ docker container ls
CTR ID IMAGE COMMAND STATUS NAMES
df6..a7b alpine:latest "sleep 1d" Up 25 secs hellcat.1.l3nh...
请注意,容器名称是由句点分隔的
service-name、
replica-number和
replica-ID`的组合。
执行到容器上,检查数据是否出现在/vol
中。我们将在exec
示例中使用服务副本的容器标识。如果你在 Windows 上跟随,记得用pwsh.exe
代替sh
。
node1$ docker container exec -it df6 sh
/# cat /vol/file1
I promise to write a review of the book on Amazon
`很好,该卷保留了原始数据,并使其可用于新的容器。
我想是时候跳到亚马逊上写书评了:-D
通过将外部存储系统与 Docker 集成,可以在群集节点之间共享卷。这些外部系统可以是云存储服务,也可以是您内部数据中心的企业存储系统。例如,单个存储 LUN 或 NFS 共享可以呈现给多个 Docker 主机,允许容器和服务副本使用它,无论它们运行在哪个 Docker 主机上。图 13.4 显示了呈现给两个 Docker 节点的单个外部共享卷。然后,这些 Docker 节点可以使共享卷对一个容器或两个容器都可用。
Figure 13.4
构建这样的设置需要很多东西。您需要访问专门的存储系统,并了解其工作原理和存储方式。您还需要知道应用如何将数据读写到共享存储中。最后,您需要一个与外部存储系统配合使用的卷驱动程序插件。
Docker Hub 是查找卷插件的最佳位置。登录 Docker Hub,选择视图显示plugins
而不是containers
,过滤结果只显示Volume
插件。找到适合您的存储系统的插件后,您可以创建它可能需要的任何配置文件,并用docker plugin install
安装它。
注册插件后,您可以使用带有-d
标志的docker volume create
从存储系统创建新卷。
以下示例安装了纯存储 Docker 卷插件。该插件提供对纯存储闪存阵列或闪存刀片存储系统上的存储卷的访问。插件仅适用于正确的外部存储系统。
- 纯存储插件需要在 Docker 主机的
/etc/pure-docker-plugin/
目录中有一个名为pure.json
的配置文件。该文件包含插件定位外部存储系统、进行认证和访问资源所需的信息。 - 安装插件并授予所需的权限。
$ docker plugin install purestorage/docker-plugin:latest --alias pure --grant-all-permissions
Plugin "purestorage/docker-plugin:3.8" is requesting the following privileges:
- network: [host]
- host pid namespace: [true]
- mount: [/etc/pure-docker-plugin/pure.json]
- mount: [/dev]
- mount: [/sys]
- allow-all-devices: [true]
- capabilities: [CAP_SYS_ADMIN CAP_SYS_PTRACE]
Do you grant the above permissions? [y/N] y
`1. 列出可用的插件。
$ docker plugin ls
ID NAME DESCRIPTION ENABLED
6b5e61aefbb3 pure:latest Pure Storage plugin for Docker true
`1. 使用插件创建一个新卷(您也可以在容器创建过程中这样做)。本示例在注册的纯存储后端创建一个名为“fastvol”的 25GB 新卷。
$ docker volume create -d pure -o size=25GB fastvol
fastvol
`不同的存储驱动程序支持不同的选项,但这应该足以让您感受到它们是如何工作的。
在多个容器之间共享单个卷的任何配置的主要问题是数据损坏。
假设基于图 13.4 的例子如下。
运行在节点 1 上的 ctr-1 中的应用更新共享卷中的一些数据。但是,它不会将更新直接写入卷,而是将其保存在本地缓冲区中,以便更快地调用(这在许多操作系统中很常见)。此时,ctr-1 中的应用认为数据已经写入卷。但是,在节点 1 上的 ctrl-1 刷新其缓冲区并将数据提交到卷之前,节点 2 上的 ctrl-2 中的应用会用不同的值更新相同的数据,并将其直接提交到卷。此时,两个应用都认为已经更新了卷中的数据,但实际上只有 ctr-2 中的应用更新了。几秒钟后,节点-1 上的 ctrl-1 将数据刷新到卷,覆盖了应用在 ctrl-2 中所做的更改。然而,ctr-2 中的应用完全没有意识到这一点!这是数据损坏发生的方式之一。
为了防止这种情况,您需要以一种避免这种情况的方式编写应用。
docker volume create
是我们用来创建新卷的命令。默认情况下,使用local
驱动程序创建卷,但是您可以使用-d
标志来指定不同的驱动程序。docker volume ls
将列出本地 Docker 主机上的所有卷。docker volume inspect
显示详细的音量信息。使用此命令可以查看许多有趣的卷属性,包括卷在 Docker 主机文件系统中的位置。docker volume prune
将删除容器或服务副本未使用的所有卷。慎用!docker volume rm
删除未使用的特定卷。docker plugin install
将从 Docker Hub 安装新的卷插件。docker plugin ls
列出安装在 Docker 主机上的所有插件。
有两种主要的数据类型:持久数据和非持久数据。持久数据是需要保留的数据,非持久数据是不需要保留的数据。默认情况下,所有容器都获得一层可写的非持久存储,它与容器一起生存和死亡——我们称之为本地存储,它非常适合非持久数据。但是,如果您的容器创建了需要保留的数据,您应该将数据存储在 Docker 卷中。
在 Docker API 中,Docker 卷是一等公民,并使用自己的docker volume
子命令独立于容器进行管理。这意味着删除容器不会删除它正在使用的卷。
第三方卷插件可以为 Docker 提供对专用外部存储系统的访问。它们通过docker plugin install
命令从 Docker Hub 安装,并在卷创建时通过-d
命令标志引用。
在 Docker 环境中,卷是处理持久数据的推荐方式。``````````````````