“一次做一件事,把它做好”,一直以来都是信息技术 ( IT )领域颇为成功的口头禅之一。这个广泛使用的原则也非常适合构建和公开 Docker 容器,并且它被指定为最佳实践之一,以利用最初设想的 Docker 启发的容器化范例的好处。这意味着,我们必须将单个应用及其直接依赖项和库记录在 Docker 容器中,以确保容器的独立性、自给自足性、水平可扩展性和可操作性。让我们看看为什么容器如此重要:
- 容器的时间特性:容器的寿命通常与应用的寿命一样长,反之亦然。但是,这对应用数据有一些负面影响。应用自然会经历各种变化,以适应业务和技术变化,即使是在生产环境中。还有其他原因,例如应用故障、版本更改和应用维护,导致软件应用持续不断地更新和升级。在通用计算模型的情况下,即使应用由于任何原因而死亡,与该应用相关联的持久数据也可以保存在文件系统中。然而,在容器范例的情况下,应用升级通常是通过简单地丢弃旧的容器,用新版本的应用系统地创建一个新的容器来执行的。类似地,当应用出现故障时,需要启动一个新的容器,旧的容器必须被丢弃。总而言之,容器本质上通常是暂时的。
- 对业务连续性的需求:在容器环境中,完整的执行环境,包括其数据文件,通常被捆绑并封装在容器内。无论出于什么原因,当容器被丢弃时,应用数据文件也会随着容器一起消失。但是,为了提供没有任何中断和服务中断的软件应用,这些应用数据文件必须保存在容器之外,并根据需要传递给容器,以确保业务连续性。这意味着需要保证容器的弹性和可靠性。此外,一些应用数据文件,如日志文件,需要在容器外收集和访问,以进行各种事后分析。Docker 技术通过称为数据卷的新构建块非常创新地解决了这个文件持久性问题。
Docker 技术有三种不同的方式来提供持久存储:
- 第一种也是推荐的方法是使用使用 Docker 的卷管理创建的卷。
- 第二种方法是将目录从 Docker 主机挂载到容器内的指定位置。
- 另一种选择是使用纯数据容器。纯数据容器是一种特制的容器,用于与一个或多个容器共享数据。
在本章中,我们将涵盖以下主题:
- 数据卷宗
- 共享主机数据
- 在容器之间共享数据
- 可避免的常见陷阱
数据量是 Docker 环境中数据共享的基本构件。在进入数据共享的细节之前,有必要对数据量概念有一个很好的理解。到目前为止,我们在映像或容器中创建的所有文件都是 union 文件系统的一部分。容器的联合文件系统随着容器一起消失。换句话说,当容器被移除时,它的文件系统也被自动移除。然而,企业级的应用必须保存数据,并且容器的文件系统不能满足这样的要求。
然而,Docker 生态系统用数据量概念优雅地解决了这个问题。数据卷本质上是 Docker 主机文件系统的一部分,它只是装载在容器内。或者,您可以通过可插拔卷驱动程序将其他高级文件系统(如 Flocker 和 GlusterFS)用作数据卷。因为数据卷不是容器文件系统的一部分,所以它的生命周期独立于容器。
可以使用Dockerfile
的VOLUME
指令将数据体记录在 Docker 映像中。此外,还可以在启动容器时使用docker run
子命令的-v
选项进行规定。这里,在以下示例中,Dockerfile
中的VOLUME
指令的含义在以下步骤中详细说明:
- 用基础映像(
ubuntu:16.04
)和数据体(/MountPointDemo
)的指令创建一个非常简单的Dockerfile
:
FROM ubuntu:16.04
VOLUME /MountPointDemo
- 使用
docker build
子命令创建名称为mount-point-demo
的映像:
$ sudo docker build -t mount-point-demo .
- 构建映像后,让我们使用
docker inspect
子命令快速检查数据量的映像:
$ sudo docker inspect mount-point-demo
[
{
"Id": "sha256:<64 bit hex id>",
"RepoTags": [
"mount-point-demo:latest"
],
... TRUNCATED OUTPUT ...
"Volumes": {
"/MountPointDemo": {}
},
... TRUNCATED OUTPUT ...
显然,在前面的输出中,数据量被记录在映像本身中。
- 现在,让我们使用早期制作的映像中的
docker run
子命令启动一个交互式容器,如以下命令所示:
$ sudo docker run --rm -it mount-point-demo
从容器的提示中,让我们使用ls -ld
命令检查数据量的存在:
root@8d22f73b5b46:/# ls -ld /MountPointDemo
drwxr-xr-x 2 root root 4096 Nov 18 19:22
/MountPointDemo
如前所述,数据卷是 Docker 主机文件系统的一部分,它被装载,如以下命令所示:
root@8d22f73b5b46:/# mount | grep MountPointDemo
/dev/xvda2 on /MountPointDemo type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
- 在本节中,我们检查了映像,以了解映像中的数据量声明。现在我们已经启动了容器,让我们在不同的终端中使用容器标识作为参数的
docker inspect
子命令来检查容器的数据量。我们之前创建了几个容器,为此,让我们直接从容器的提示中获取8d22f73b5b46
容器标识:
$ sudo docker inspect -f
'{{json .Mounts}}' 8d22f73b5b46
[
{
"Propagation": "",
"RW": true,
"Mode": "",
"Driver": "local",
"Destination": "/MountPointDemo",
"Source":
"/var/lib/docker/volumes/720e2a2478e70a7cb49ab7385b8be627d4b6ec52e6bb33063e4144355d59592a/_data",
"Name": "720e2a2478e70a7cb49ab7385b8be627d4b6ec52e6bb33063e4144355d59592a"
}
]
显然,在这里,数据卷被映射到 Docker 主机中的一个目录,并且该目录以读写模式装载。该目录也称为卷,由 Docker 引擎在容器启动期间自动创建。自从 Docker 的 1.9 版本以来,卷是通过顶级卷管理命令来管理的,我们将在下一节深入研究这个命令。
到目前为止,我们已经看到了Dockerfile
中VOLUME
指令的含义,以及 Docker 如何管理数据量。像Dockerfile
的VOLUME
指令一样,我们可以使用docker run
子命令的-v <container mount point path>
选项,如下命令所示:
$ sudo docker run -v /MountPointDemo -it ubuntu:16.04
启动容器后,我们鼓励您在新启动的容器中尝试ls -ld /MountPointDemo
和mount
命令,然后检查容器,如前面的步骤 5 所示。
在这里描述的两个场景中,Docker 引擎自动在/var/lib/docker/volumes/
目录下创建卷,并将其装载到容器中。当使用docker rm
子命令移除容器时,Docker 引擎不会移除在容器启动期间自动创建的卷。这种行为本质上是为了保留存储在卷文件系统中的容器应用的状态。如果您想要移除由 Docker 引擎自动创建的卷,您可以在移除容器时这样做,方法是在已经停止的容器上为docker rm
子命令提供一个-v
选项:
$ sudo docker rm -v 8d22f73b5b46
如果容器仍在运行,那么您可以通过在前一个命令中添加一个-f
选项来移除容器以及自动生成的目录:
$ sudo docker rm -fv 8d22f73b5b46
我们已经向您介绍了在 Docker 主机中自动生成目录并将其装载到容器中的数据卷的技术和技巧。但是,使用docker run
子命令的-v
选项,可以将用户定义的目录装载到数据卷中。在这种情况下,Docker 引擎不会自动生成任何目录。
The system generation of a directory has a caveat of directory leak. In other words, if you forget to delete the system-generated directories, you may face some unwanted issues. For further information, read the Avoiding common pitfalls section in this chapter.
Docker 从 1.9 版引入了顶级卷管理命令,以便有效地管理持久文件系统。卷管理命令能够管理作为 Docker 主机一部分的数据卷。除此之外,它还帮助我们使用可插拔卷驱动程序(Flocker、GlusterFS 等)来扩展 Docker 持久性功能。你可以在https://docs.docker.com/engine/extend/legacy_plugins/找到支持的插件列表。
docker volume
命令支持下面列出的四个子命令:
create
:这将创建一个新的卷inspect
:显示一个或多个卷的详细信息ls
:这列出了 Docker 主机中的卷rm
:这将删除一个卷
让我们通过几个例子快速探索卷管理命令。您可以使用docker volume create
子命令创建一个卷,如下所示:
$ sudo docker volume create
50957995c7304e7d398429585d36213bb87781c53550b72a6a27c755c7a99639
前面的命令将通过自动生成 64 位十六进制字符串作为卷名来创建卷。但是,为便于识别,用有意义的名称命名卷更有效。您可以使用docker volume create
子命令的--name
选项来命名卷:
$ sudo docker volume create --name example
example
现在,我们已经创建了两个有卷名和没有卷名的卷,让我们使用docker volume ls
子命令来显示它们:
$ sudo docker volume ls
DRIVER VOLUME NAME
local 50957995c7304e7d398429585d36213bb87781c53550b72a6a27c755c7a99639
local example
列出卷后,让我们运行docker volume inspect
子命令,查看我们之前创建的卷的详细信息:
$ sudo docker volume inspect example
[
{
"Name": "example",
"Driver": "local",
"Mountpoint":
"/var/lib/docker/volumes/example/_data",
"Labels": {},
"Scope": "local"
}
]
docker volume rm
子命令使您能够删除不再需要的卷:
$ sudo docker volume rm example
example
现在我们已经熟悉了 Docker 卷管理,让我们在接下来的章节中深入探讨数据共享。
前面,我们描述了使用Dockerfile
中的VOLUME
指令在 Docker 映像中创建数据卷的步骤。然而,Docker 没有提供任何机制来在构建期间装载主机目录或文件,以确保 Docker 映像是可移植的。Docker 提供的唯一配置是在容器启动期间将主机目录或文件装载到容器的数据卷中。Docker 通过docker run
子命令的-v
选项公开主机目录或文件挂载工具。-v
选项有五种不同的格式,列举如下:
-v <container mount path>
-v <host path>:<container mount path>
-v <host path>:<container mount path>:<read write mode>
-v <volume name>:<container mount path>
-v <volume name>:<container mount path>:<read write mode>
<host path>
格式是 Docker 主机中的绝对路径,<container mount path>
是容器文件系统中的绝对路径,<volume name>
是使用docker volume create
子命令创建的卷的名称,<read write mode>
可以是只读(ro
)或读写(rw
)模式。第一种-v <container mount path>
格式已经在本章的数据卷部分进行了解释,作为在容器启动过程中创建挂载点的一种方法。第二种和第三种格式使我们能够将文件或目录从 Docker 主机装载到容器装载点。第四种和第五种格式允许我们挂载使用docker volume create
子命令创建的卷。
我们想通过几个例子来更深入地了解主机的数据共享。在第一个示例中,我们将演示如何在 Docker 主机和容器之间共享目录,在第二个示例中,我们将演示文件共享。
这里,在第一个示例中,我们将目录从 Docker 主机挂载到容器,在容器上执行一些基本的文件操作,并从 Docker 主机验证这些操作,具体如下步骤所示:
- 首先,让我们用
docker run
子命令的-v
选项启动一个交互式容器,将 Docker 主机目录的/tmp/hostdir
装载到容器的/MountPoint
中:
$ sudo docker run -v /tmp/hostdir:/MountPoint \
-it ubuntu:16.04
If /tmp/hostdir
is not found on the Docker host, the Docker Engine will create the directory per se. However, the problem is that the system-generated directory cannot be deleted using the -v
option of the docker rm
subcommand.
- 成功启动容器后,我们可以使用
ls
命令检查/MountPoint
的存在:
root@4a018d99c133:/# ls -ld /MountPoint
drwxr-xr-x 2 root root 4096 Nov 23 18:28
/MountPoint
- 现在,我们可以使用
mount
命令继续检查挂载细节:
root@4a018d99c133:/# mount | grep MountPoint
/dev/xvda2 on /MountPoint type ext3
(rw,noatime,nobarrier,errors=
remount-ro,data=ordered)
- 在这里,我们将验证
/MountPoint
,使用cd
命令更改到/MountPoint
目录,使用touch
命令创建几个文件,并使用ls
命令列出文件,如下脚本所示:
root@4a018d99c133:/# cd /MountPoint/
root@4a018d99c133:/MountPoint# touch {a,b,c}
root@4a018d99c133:/MountPoint# ls -l
total 0
-rw-r--r-- 1 root root 0 Nov 23 18:39 a
-rw-r--r-- 1 root root 0 Nov 23 18:39 b
-rw-r--r-- 1 root root 0 Nov 23 18:39 c
- 在新终端上使用
ls
命令验证/tmp/hostdir
Docker 主机目录中的文件可能是值得的,因为我们的容器在现有终端上以交互模式运行:
$ sudo ls -l /tmp/hostdir/
total 0
-rw-r--r-- 1 root root 0 Nov 23 12:39 a
-rw-r--r-- 1 root root 0 Nov 23 12:39 b
-rw-r--r-- 1 root root 0 Nov 23 12:39 c
在这里,我们可以看到与第 4 步中看到的相同的文件集。但是,您可能已经注意到文件时间戳的不同。这个时间差是由于 Docker 主机和容器之间的时区差异造成的。
- 最后,让我们以
4a018d99c133
容器标识为参数运行docker inspect
子命令,看看 Docker 主机和容器装载点之间是否设置了目录映射,如下命令所示:
$ sudo docker inspect \
--format='{{json .Mounts}}' 4a018d99c133
[{"Source":"/tmp/hostdir",
"Destination":"/MountPoint","Mode":"",
"RW":true,"Propagation":"rprivate"}]
显然,在docker inspect
子命令的前面输出中,Docker 主机的/tmp/hostdir
目录安装在容器的/MountPoint
安装点上。
对于第二个示例,我们将从 Docker 主机将文件装载到容器中,从容器更新文件,并从 Docker 主机验证这些操作,如以下步骤所示:
- 为了将文件从 Docker 主机装载到容器中,文件必须预先存在于 Docker 主机中。否则,Docker 引擎将使用指定的名称创建一个新目录,并将其装载为目录。我们可以从使用
touch
命令在 Docker 主机上创建一个文件开始:
$ touch /tmp/hostfile.txt
- 使用
docker run
子命令的-v
选项启动一个交互式容器,将/tmp/hostfile.txt
Docker 主机文件作为/tmp/mntfile.txt
装载到容器中:
$ sudo docker run -v /tmp/hostfile.txt:/mntfile.txt \
-it ubuntu:16.04
- 成功启动容器后,现在让我们使用
ls
命令检查/mntfile.txt
是否存在:
root@d23a15527eeb:/# ls -l /mntfile.txt
-rw-rw-r-- 1 1000 1000 0 Nov 23 19:33 /mntfile.txt
- 然后,使用
mount
命令继续检查安装细节:
root@d23a15527eeb:/# mount | grep mntfile
/dev/xvda2 on /mntfile.txt type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
- 然后,使用
echo
命令将一些文本更新到/mntfile.txt
:
root@d23a15527eeb:/# echo "Writing from Container"
> mntfile.txt
- 同时,切换到 Docker 主机中的不同终端,并使用
cat
命令打印/tmp/hostfile.txt
Docker 主机文件:
$ cat /tmp/hostfile.txt
Writing from Container
- 最后,以
d23a15527eeb
容器标识作为参数运行docker inspect
子命令,查看 Docker 主机和容器装载点之间的文件映射:
$ sudo docker inspect \
--format='{{json .Mounts}}' d23a15527eeb
[{"Source":"/tmp/hostfile.txt",
"Destination":"/mntfile.txt",
"Mode":"","RW":true,"Propagation":"rprivate"}]
从前面的输出中,很明显来自 Docker 主机的/tmp/hostfile.txt
文件作为/mntfile.txt
安装在容器内。
对于最后一个示例,我们将创建一个 Docker 卷,并将一个命名的数据卷装载到一个容器中。在这个例子中,我们不会像前面两个例子那样运行验证步骤。但是,我们鼓励您运行我们在第一个示例中列出的验证步骤。
- 使用
docker volume create
子命令创建命名数据卷,如下所示:
$ docker volume create --name namedvol
- 现在,使用
docker run
子命令的-v
选项启动一个交互式容器,将namedvol
命名数据值装载到容器的/MountPoint
中:
$ sudo docker run -v namedvol:/MountPoint \
-it ubuntu:16.04
During the launch of the container, Docker Engine creates namedvol
if it is not created already.
- 成功启动容器后,您可以重复第一个示例的验证步骤 2 到 6,并且在这个示例中您也会发现相同的输出模式。
在前一章中,我们在 Docker 容器中启动了一个 HTTP 服务。但是,如果您没记错的话,HTTP 服务的日志文件仍然在容器中,不能从 Docker 主机直接访问。在本节中,我们将逐步阐述从 Docker 主机访问日志文件的过程:
- 让我们从启动 Apache2 HTTP 服务容器开始,通过使用
docker run
子命令的-v
选项,将 Docker 主机的/var/log/myhttpd
目录装载到容器的/var/log/apache2
目录中。在本例中,我们通过调用以下命令来利用我们在上一章中构建的apache2
映像:
$ sudo docker run -d -p 80:80 \
-v /var/log/myhttpd:/var/log/apache2 apache2
9c2f0c0b126f21887efaa35a1432ba7092b69e0c6d523ffd50684e27eeab37ac
如果您回忆起第 6 章、中的Dockerfile
在容器中运行服务,APACHE_LOG_DIR
环境变量被设置到/var/log/apache2
目录,使用ENV
指令。这将使 Apache2 HTTP 服务将所有日志消息路由到/var/log/apache2
数据卷。
- 一旦容器启动,我们可以将目录更改为 Docker 主机上的
/var/log/myhttpd
:
$ cd /var/log/myhttpd
- 或许,快速检查一下
/var/log/myhttpd
目录中的文件是合适的:
$ ls -1
access.log
error.log
other_vhosts_access.log
这里access.log
文件包含了 Apache2 HTTP 服务器处理的所有访问请求。error.log
文件是一个非常重要的日志文件,我们的 HTTP 服务器会记录它在处理任何 HTTP 请求时遇到的错误。other_vhosts_access.log
文件是虚拟主机日志,在我们的例子中,它总是空的。
- 我们可以使用带有
-f
选项的tail
命令显示/var/log/myhttpd
目录中所有日志文件的内容:
$ tail -f *.log
==> access.log <==
==> error.log <==
AH00558: apache2: Could not reliably determine the
server's fully qualified domain name, using 172.17.0.17\.
Set the 'ServerName' directive globally to suppress this
message
[Thu Nov 20 17:45:35.619648 2014] [mpm_event:notice]
[pid 16:tid 140572055459712] AH00489: Apache/2.4.7
(Ubuntu) configured -- resuming normal operations
[Thu Nov 20 17:45:35.619877 2014] [core:notice]
[pid 16:tid 140572055459712] AH00094: Command line:
'/usr/sbin/apache2 -D FOREGROUND'
==> other_vhosts_access.log <==
一旦文件更新,tail -f
命令将持续运行并显示文件内容。这里access.log
和other_vhosts_access.log
都是空的,error.log
文件上有一些错误信息。显然,这些错误日志是由容器内部运行的 HTTP 服务生成的。然后,日志被存储在 Docker 主机目录中,该目录在容器启动期间被装载。
- 当我们继续运行
tail -f *
时,让我们从容器内部运行的网络浏览器连接到 HTTP 服务,并观察日志文件:
==> access.log <==
111.111.172.18 - - [20/Nov/2014:17:53:38 +0000] "GET /
HTTP/1.1" 200 3594 "-" "Mozilla/5.0 (Windows NT 6.1;
WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65
Safari/537.36"
111.111.172.18 - - [20/Nov/2014:17:53:39 +0000] "GET
/icons/ubuntu-logo.png HTTP/1.1" 200 3688
"http://111.71.123.110/" "Mozilla/5.0 (Windows NT 6.1;
WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65
Safari/537.36"
111.111.172.18 - - [20/Nov/2014:17:54:21 +0000] "GET
/favicon.ico HTTP/1.1" 404 504 "-" "Mozilla/5.0 (Windows
NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/39.0.2171.65 Safari/537.36"
HTTP 服务更新access.log
文件,我们可以通过docker run
子命令的-v
选项从挂载的主机目录中操作该文件。
在前一节中,您学习了 Docker 引擎如何无缝地实现 Docker 主机和容器之间的数据共享。尽管对于大多数用例来说,这是一个非常有效的解决方案,但是在一些用例中,您必须在一个或多个容器之间共享数据。Docker 解决这个用例的方法是使用docker run
子命令的--volume-from
选项将一个容器的数据量装载到其他容器中。
在 Docker 引入顶级卷管理功能之前,纯数据容器是实现数据持久性的推荐方法。理解纯数据容器是值得的,因为您会发现许多基于纯数据容器的实现。纯数据容器的主要职责是保存数据。创建纯数据容器非常类似于数据卷部分中说明的方法。此外,容器是为其他容器显式命名的,以便使用容器的名称装载数据卷。此外,容器的数据卷可以从其他容器访问,即使只有数据的容器处于停止状态。纯数据容器可以通过两种方式创建,如下所示:
- 在容器启动期间,通过配置数据卷和容器的名称
- 在映像构建过程中,数据量也可以刻上
Dockerfile
,之后,容器可以在容器启动过程中命名
在下面的示例中,我们通过使用docker run
子命令的-v
和--name
选项配置容器启动来启动纯数据容器,如下所示:
$ sudo docker run --name datavol \
-v /DataMount \
busybox:latest /bin/true
在这里,容器是从busybox
映像中推出的,该映像因其较小的占地面积而被广泛使用。这里,我们选择执行/bin/true
命令,因为我们不打算对容器进行任何操作。因此,我们使用--name
选项命名容器datavol
,并使用docker run
子命令的-v
选项创建新的/DataMount
数据卷。/bin/true
命令以0
退出状态立即退出,这又将停止容器并继续处于停止状态。
Docker 引擎提供了一个漂亮的接口,可以将数据卷从一个容器装载(共享)到另一个容器。Docker 通过docker run
子命令的--volumes-from
选项使该界面可用。--volumes-from
选项将容器名称或容器标识作为其输入,并自动装载指定容器上的所有可用数据卷。Docker 允许您多次使用--volumes-from
选项装载多个具有数据量的容器。
下面是一个实际示例,演示如何从另一个容器装载数据卷,并逐步展示数据卷装载:
- 我们从启动一个交互式 Ubuntu 容器开始,从纯数据容器(
datavol
)挂载数据卷,这是我们在上一节中启动的:
$ sudo docker run -it \
--volumes-from datavol \
ubuntu:latest /bin/bash
- 现在,从容器的提示中,让我们使用
mount
命令来验证数据卷装载:
root@e09979cacec8:/# mount | grep DataMount
/dev/xvda2 on /DataMount type ext3
(rw,noatime,nobarrier,errors=remount-ro,data=ordered)
在这里,我们成功地从datavol
纯数据容器装载了数据卷。
- 接下来,我们需要使用
docker inspect
子命令从另一个终端检查该容器的数据量:
$ sudo docker inspect --format='{{json .Mounts}}'
e09979cacec8
[{"Name":
"7907245e5962ac07b31c6661a4dd9b283722d3e7d0b0fb40a90
43b2f28365021","Source":
"/var/lib/docker/volumes
/7907245e5962ac07b31c6661a4dd9b283722d3e7d0b0fb40a9043b
2f28365021/_data","Destination":"
/DataMount","Driver":"local","Mode":"",
"RW":true,"Propagation":""}]
显然,来自datavol
纯数据容器的数据量被安装,就好像它们被直接安装到该容器上一样。
我们可以从另一个容器装载数据卷,也可以展示装载点。我们可以通过使用数据卷在容器之间共享数据来使装载的数据卷工作,如下所示:
- 让我们重用我们在前面示例中启动的容器,并通过向文件中写入一些文本在
/DataMount
数据卷中创建一个/DataMount/testfile
文件,如下所示:
root@e09979cacec8:/# echo \
"Data Sharing between Container" > \
/DataMount/testfile
- 使用
cat
命令,旋转一个容器来显示我们在上一步中编写的文本:
$ sudo docker run --rm \
--volumes-from datavol \
busybox:latest cat /DataMount/testfile
以下是上述命令的典型输出:
Data Sharing between Container
显然,我们新容器化的cat
命令的前面的Data Sharing between Container
输出是我们在步骤 1 中在datavol
容器的/DataMount/testfile
中写的文本。
很酷,不是吗?通过共享数据卷,您可以在容器之间无缝地共享数据。在这个例子中,我们使用了纯数据容器作为数据共享的基本容器。但是,Docker 允许我们共享任何类型的数据卷,并一个接一个地装载数据卷,如下所示:
$ sudo docker run --name vol1 --volumes-from datavol \
busybox:latest /bin/true
$ sudo docker run --name vol2 --volumes-from vol1 \
busybox:latest /bin/true
这里,在vol1
容器中,我们从datavol
容器装载数据卷。然后,在vol2
容器中,我们安装了来自vol1
容器的数据卷,该数据卷最终来自datavol
容器。
在本章的前面,您学习了从 Docker 主机访问 Apache2 HTTP 服务的日志文件的机制。虽然通过将 Docker 主机目录装载到容器中来共享数据相当方便,但后来我们知道,只需使用数据卷就可以在容器之间共享数据。因此,在这里,我们将通过在容器之间共享数据来引入 Apache2 HTTP 服务日志处理的方法。为了在容器之间共享日志文件,我们将在以下步骤中分离下列容器:
- 首先,一个纯数据容器,它将向其他容器公开数据卷。
- 然后,Apache2 HTTP 服务容器利用纯数据容器的数据量。
- 一个容器,用于查看我们的 Apache2 HTTP 服务生成的日志文件。
If you are running any HTTP service on the 80
port number of your Docker host machine, pick any other unused port number for the following example. If not, first stop the HTTP service, then proceed with the example in order to avoid any port conflict.
现在,我们将一丝不苟地指导您完成制作相应映像的步骤,并启动容器来查看日志文件:
- 在这里,我们首先使用
VOLUME
指令创建一个带有/var/log/apache2
数据量的Dockerfile
。/var/log/apache2
数据量是到APACHE_LOG_DIR
的直接映射,环境变量设置在第 6 章、在容器中运行服务,使用ENV
指令:
#######################################################
# Dockerfile to build a LOG Volume for Apache2 Service
#######################################################
# Base image is BusyBox
FROM busybox:latest
# Author: Dr. Peter
MAINTAINER Dr. Peter <peterindia@gmail.com>
# Create a data volume at /var/log/apache2, which is
# same as the log directory PATH set for the apache image
VOLUME /var/log/apache2
# Execute command true
CMD ["/bin/true"]
由于这个Dockerfile
是为启动纯数据容器而精心制作的,因此默认执行命令被设置为/bin/true
。
- 我们将继续使用
docker build
创建一个名为apache2log
的 Docker 映像,如下所示:
$ sudo docker build -t apache2log .
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM busybox:latest
... TRUNCATED OUTPUT ...
- 使用
docker run
子命令从apache2log
映像启动一个纯数据容器,并使用--name
选项命名生成的容器log_vol
:
$ sudo docker run --name log_vol apache2log
根据前面的命令,容器将在/var/log/apache2
中创建一个数据卷,并将其移动到停止状态。
- 同时,您可以使用
-a
选项运行docker ps
子命令来验证容器的状态:
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND
CREATED STATUS PORTS
NAMES
40332e5fa0ae apache2log:latest "/bin/true"
2 minutes ago Exited (0) 2 minutes ago
log_vol
根据输出,容器以0
退出值退出。
- 使用
docker run
子命令启动 Apache2 HTTP 服务。在这里,我们正在重用我们在第 6 章、中创建的apache2
映像,在容器中运行服务。此外,在这个容器中,我们将使用--volumes-from
选项从log_vol
装载/var/log/apache2
数据卷,这是我们在步骤 3 中启动的纯数据容器:
$ sudo docker run -d -p 80:80 \
--volumes-from log_vol \
apache2
7dfbf87e341c320a12c1baae14bff2840e64afcd082dda3094e7cb0a0023cf42
随着从log_vol
装载/var/log/apache2
数据卷的 Apache2 HTTP 服务的成功启动,我们可以使用临时容器访问日志文件。
- 这里,我们使用一个临时容器列出了 Apache2 HTTP 服务存储的文件。这个临时容器通过从
log_vol
装入/var/log/apache2
数据卷而分离出来,并且使用ls
命令列出/var/log/apache2
中的文件。此外,docker run
子命令的--rm
选项用于在完成执行ls
命令后移除容器:
$ sudo docker run --rm \
--volumes-from log_vol \
busybox:latest ls -l /var/log/apache2
total 4
-rw-r--r-- 1 root root 0 Dec 5 15:27
access.log
-rw-r--r-- 1 root root 461 Dec 5 15:27
error.log
-rw-r--r-- 1 root root 0 Dec 5 15:27
other_vhosts_access.log
- 最后,使用
tail
命令访问 Apache2 HTTP 服务生成的错误日志,如下命令所示:
$ sudo docker run --rm \
--volumes-from log_vol \
ubuntu:16.04 \
tail /var/log/apache2/error.log
AH00558: apache2: Could not reliably determine the
server's fully qualified domain name, using 172.17.0.24\.
Set the 'ServerName' directive globally to suppress this
message
[Fri Dec 05 17:28:12.358034 2014] [mpm_event:notice]
[pid 18:tid 140689145714560] AH00489: Apache/2.4.7
(Ubuntu) configured -- resuming normal operations
[Fri Dec 05 17:28:12.358306 2014] [core:notice]
[pid 18:tid 140689145714560] AH00094: Command line:
'/usr/sbin/apache2 -D FOREGROUND'
到目前为止,我们已经讨论了如何有效地使用数据卷在 Docker 主机和容器之间以及容器之间共享数据。使用数据量的数据共享被证明是 Docker 范例中非常强大和重要的工具。然而,它确实有一些陷阱,需要仔细识别和消除。在本节中,我们尝试列出一些与数据共享相关的常见问题,以及克服这些问题的方法和途径。
在数据卷一节中,您已经了解到 Docker 引擎会根据Dockerfile
中的VOLUME
指令以及docker run
子命令的-v
选项自动创建目录。我们还了解到,Docker 引擎不会自动删除这些自动生成的目录,以保留容器内运行的应用的状态。我们可以使用docker rm
子命令的-v
选项强制 Docker 删除这些目录。手动删除的过程带来了两大挑战,列举如下:
- **未删除的目录:**可能会有这样的场景,您可能会有意或无意地选择在删除容器时不删除生成的目录。
- **第三方映像:**通常,我们会利用第三方 Docker 映像,这些映像本可以通过
VOLUME
指令构建。同样,我们也可能有自己的刻有VOLUME
的 Docker 映像。当我们使用这样的 Docker 映像启动容器时,Docker 引擎将自动生成指定的目录。由于我们不知道数据卷的创建,我们可能不会使用-v
选项调用docker rm
子命令来删除自动生成的目录。
在前面提到的场景中,一旦关联的容器被移除,就没有直接的方法来识别容器被移除的目录。以下是一些关于如何避免这个陷阱的建议:
- 始终使用
docker inspect
子命令检查 Docker 映像,并检查映像中是否记录了任何数据量。 - 始终使用
-v
选项运行docker rm
子命令,删除为容器创建的任何数据卷(目录)。即使数据卷由多个容器共享,使用-v
选项运行docker rm
子命令仍然是安全的,因为只有当共享该数据卷的最后一个容器被删除时,与该数据卷相关联的目录才会被删除。 - 出于任何原因,如果您选择保留自动生成的目录,您必须保留一个清晰的记录,以便以后可以删除它们。
- 实现一个审计框架,它将审计并找出没有任何容器关联的目录。
如前所述,Docker 允许我们在构建期间使用VOLUME
指令访问 Docker 映像中的每个数据卷。尽管如此,在构建期间,数据卷不应用于存储任何数据,否则会导致不必要的影响。
在本节中,我们将通过创建一个Dockerfile
来演示在构建过程中使用数据量的不良影响,然后通过构建这个Dockerfile
来展示其含义。
以下是Dockerfile
的详情:
- 使用 Ubuntu 16.04 作为基础映像构建映像:
# Use Ubuntu as the base image
FROM ubuntu:16.04
- 使用
VOLUME
指令创建/MountPointDemo
数据卷:
VOLUME /MountPointDemo
- 使用
RUN
指令在/MountPointDemo
数据卷中创建文件:
RUN date > /MountPointDemo/date.txt
- 使用
RUN
指令显示/MountPointDemo
数据卷中的文件:
RUN cat /MountPointDemo/date.txt
- 使用
docker build
子命令从这个Dockerfile
开始构建映像,如下所示:
$ sudo docker build -t testvol .
Sending build context to Docker daemon 2.56 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:16.04
---> 9bd07e480c5b
Step 1 : VOLUME /MountPointDemo
---> Using cache
---> e8b1799d4969
Step 2 : RUN date > /MountPointDemo/date.txt
---> Using cache
---> 8267e251a984
Step 3 : RUN cat /MountPointDemo/date.txt
---> Running in a3e40444de2e
cat: /MountPointDemo/date.txt: No such file or directory
2014/12/07 11:32:36 The command [/bin/sh -c cat
/MountPointDemo/date.txt] returned a non-zero code: 1
在docker build
子命令的前面输出中,您会注意到构建在步骤 3 中失败,因为它找不到在步骤 2 中创建的文件。显然,在步骤 2 中创建的文件在到达步骤 3 时就消失了。这种不良影响是由于 Docker 用来构建其映像的方法造成的。对 Docker 形象塑造过程的理解将揭开这个谜。
在构建过程中,对于Dockerfile
中的每个指令,遵循以下步骤:
- 通过将
Dockerfile
指令转换为等效的docker run
子命令来创建新的容器。 - 将新创建的容器提交给映像。
- 重复步骤 1 和 2,将新创建的映像视为步骤 1 的基础映像。
当一个容器被提交时,它保存容器的文件系统,并且故意不保存数据卷的文件系统。因此,存储在数据卷中的任何数据都将在此过程中丢失。因此,在构建过程中不要使用数据卷作为存储。
为了使企业级分布式应用在操作和输出方面与众不同,数据是最重要的工具和要素。随着信息技术容器化,旅程以轻快明亮的方式开始。通过 Docker 引擎的智能利用,信息技术和业务软件解决方案被智能地容器化。然而,最初的动机是需要更快、更完美地实现应用感知的 Docker 容器,因此,数据与容器中的应用紧密耦合。然而,这种接近带来了一些真正的风险。如果应用崩溃,那么数据也会消失。此外,多个应用可能依赖于相同的数据,因此数据必须跨应用共享。
在本章中,我们讨论了 Docker 引擎在促进 Docker 主机和容器之间以及容器之间的无缝数据共享方面的功能。数据量被指定为在不断增长的 Docker 生态系统的组成部分之间实现数据共享的基础构建块。在下一章中,我们将解释容器编排背后的概念,并看看如何通过一些自动化工具简化这个复杂的方面。编排对于实现复合容器是必不可少的。