写在前面

在前面我们详细学习了docker的三大核心概念:镜像、容器和仓库,接下来开始学习如何管理数据。在实际工作中使用docker,往往需要对数据进行持久化,或者需要在多个容器之间进行数据共享,此时必然会使用到容器数据管理的各种操作。

容器中的数据管理主要有两种方式:(1)数据卷(Data Volumes),表示容器内数据直接映射到本地主机环境;(2)数据卷容器(Data Volume Containers),表示使用特定容器维护数据卷。

本篇就来学习docker数据管理相关的知识,首先会介绍如何在容器内创建数据卷,并且把本地目录或者文件挂载到容器内的数据卷中,接着介绍如何使用数据卷容器在容器和宿主机、容器和容器之间共享数据,并实现数据的备份和恢复。

数据卷

数据卷(Data Volumes)是一个可供容器使用的特殊目录,它将宿主机操作系统目录直接映射进容器,有点类似于Linux操作系统中的mount行为。

数据卷特性

数据卷提供很多特性,包括且不限于以下几点:(1)数据卷可以在容器之间共享和重用,容器间传递数据将变得高效与方便;(2)无论是容器内操作还是本地操作,用户对数据卷内数据的修改会立马生效;(3)用户对数据卷的更新不会影响到镜像,可以解耦应用和数据;(4)数据卷会一直存在,直到没有容器使用,然后就可以安全的卸载它。

数据卷最佳使用场景

数据卷由于它提供的若干特性,使得它在某些场景下使用是最合适的,那些场景包括但不限于以下几处:(1)在多个容器之间共享数据,多个容器可以同时以只读或者读写的方式挂载同一个数据卷,从而共享数据卷中的数据;(2)当宿主机不能保证一定存在某个目录或一些固定路径的文件时,使用数据卷可以规避这种限制带来的问题;(3)开发者想把容器中的数据存储在宿主机之外的地方,如远程主机或云存储;(4)开发者需要把容器数据在不同的宿主机之间备份、恢复或迁移时,数据卷是不错的选择。

数据卷原理

在学习如何管理数据卷之前,先来看一张图,该图描述了docker容器挂载数据的三种方式,关于这三种方式的详细介绍会在后续进行:

在开篇介绍数据卷时笔者就讲过,数据卷本质上是一个可供容器使用的特殊目录。在容器创建过程中,docker会将宿主机上的指定目录(通常一个以数据ID为名称的目录)挂载到容器内指定的目录上,这其实就是上图中的挂载普通数据卷方式。

举个例子,现在我们需要创建名称为hello的数据卷,并将其挂载到别名为envy的容器的think目录下,此时使用的命令为:

1
2
3
4
5
[root@envythink ~]# docker volume create hello
hello
[root@envythink ~]# docker run -it --name envy --mount type=volume,source=hello,target=/think envy/ubuntu:latest /bin/bash
root@7918ea1ec248:/# exit
exit

之后使用docker inspect envy命令来查看新建的容器详情信息,可以看到下面的信息:

接着使用docker volume inspect hello命令来查看之前创建的名为hello的数据卷的详情信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
[root@envythink ~]# docker volume inspect hello
[
{
"CreatedAt": "2020-02-19T22:34:54+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/hello/_data",
"Name": "hello",
"Options": {},
"Scope": "local"
}
]

也就是说,在容器实际的创建过程中,非常类似于在容器中执行下面所列出的代码:

1
mount("/var/lib/docker/volumes/hello/_data", "rootfs/think", "none", MS_BIND, NULL)

也就是将名为hello的数据卷在宿主机上的目录(/var/lib/docker/volumes/hello/_data)绑定挂载到rootfs中指定的挂载点/think上。

数据卷管理

既然数据卷有以上列举的若干特性和最佳使用场景,那么接下来开始学习如何在容器内创建数据卷,并把本地目录或者文件挂载到容器内的数据卷中。请注意,开发者可以使用docker volume COMMAND命令来管理数据卷。

create命令创建数据卷

创建数据卷分为两种情况,一是创建随机名字串的数据卷,并挂载到容器的指定目录,如/data目录;二是创建命名的数据卷,并挂载到容器的指定目录,如/data目录。请注意在使用docker run命令的时候,可以使用-v选项参数在容器内创建一个数据卷,多次使用-v选项参数可以创建多个数据卷。

创建随机名字串的数据卷

举个例子,开发者可以使用下面的命令快速在本地创建一个随机名字串的数据卷,并挂载到别名为envy的容器的/data目录:

1
2
[root@envythink ~]# docker run -it -d --name envy -v /data envy/ubuntu:latest /bin/bash
ee0c71b7c835b8b39e18b570f33fdde82b02af05603e7f33d7c612c30190e4b9

之后使用docker inspect envy命令来查看新创建的envy容器的详情信息,可以看到下面的信息:

接着使用docker volume ls命令来查看本地目录下是否存在那个随机名字串的数据卷:

可以发现的确存在,这也就验证了我们上述操作是成功的。

此时就可以发现在/var/lib/docker/volumes路径下创建了上述名为test的数据卷:

1
2
[root@envythink ~]# ls -l /var/lib/docker/volumes/
drwxr-xr-x. 3 root root 19 02月 19 21:06 test
创建命名的数据卷

开发者可以先创建数据卷,然后将数据卷挂载到容器的某个目录下:

1
2
3
4
[root@envythink ~]# docker volume create envy_vol
envy_vol
[root@envythink ~]# docker run -it -d --name helloenvy -v envy_vol:/data ubuntu:latest
e0abb694ed60da6e10ffdde68878be7029768dcfbfd5188692fcbe31c1efe1ad

之后使用docker inspect envyhello命令来查看新创建的envyhello容器的详情信息,可以看到下面的信息:

当然开发者也可以在创建容器的时候,直接创建命名数据卷并挂载:

1
[root@envythink ~]# docker run -it -d --name envyhello -v envy_vol:/data ubuntu:latest

当然除了可以将数据卷挂载到容器内,还可以将宿主机目录挂载到容器内:(将宿主机的tmp目录挂载到容器的opt目录下)

1
[root@envythink ~]# docker run -it -d --name envyhello -v /tmp:/opt ubuntu:latest /bin/bash

inspect命令查看详细信息

开发者可以使用docker volume inspect命令来查看数据卷的详细信息。举个例子,使用如下命令来查看之前所创建的名为test的数据卷:

1
2
3
4
5
6
7
8
9
10
11
12
[root@envythink ~]# docker volume inspect test
[
{
"CreatedAt": "2020-02-19T21:06:14+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/test/_data",
"Name": "test",
"Options": {},
"Scope": "local"
}
]

ls命令列出已有数据卷

开发者可以使用docker volume ls命令来列出已有数据卷。举个例子,使用如下命令来列出当前环境内已有的数据卷:

1
2
3
[root@envythink ~]# docker volume ls
DRIVER VOLUME NAME
local test

prune命令清除无用数据卷

开发者可以使用docker volume prune命令来清除无用数据卷。举个例子,使用如下命令来清除当前环境内无用数据卷:

1
2
3
[root@envythink ~]# docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] n

可以看到这个命令会删除本地环境所有未被使用过的数据卷。

rm命令删除指定数据卷

开发者可以使用docker volume rm命令来删除指定数据卷。举个例子,使用如下命令来删除之前创建的名为test的数据卷:

1
2
[root@envythink ~]# docker volume rm test
test

绑定数据卷

开发者除了使用docker volume子命令来管理数据卷外,还可以在创建容器时将宿主机本地的任意路径挂载到容器内,作为数据卷,这种形式创建的数据卷称之为绑定数据卷。

在前面使用docker run [container]命令创建一个正在运行容器的时候,可以使用-mount选项参数来使用数据卷。请注意-mount选项参数支持三种类型的数据卷,即其type属性存在三种值,分别包括:
(1)volume,这是普通数据卷,会映射到宿主机的/var/lib/docker/volumes路径下,如果在创建时没有指定名称,则docker会自动生成一串字符串名称;

(2)bind,这是绑定数据卷,会映射到宿主机指定路径下,言外之意可以存储在宿主机的任意位置。但是请注意绑定数据卷,在不同的宿主机系统内是不可移植的。由于Linux和Windows系统的目录结构不同,因此所指向的目录也不同,这也是绑定数据卷不能出现在Dockerfile中的原因,因为Dockerfile可以移植,而此处不允许移植,因此就不能使用绑定数据卷。

(3)tmpfs,这是临时数据卷,只会存在于宿主机的内存中,不会写入宿主机的文件系统内。

以上三种方式的挂载示意图如下所示:

接下来尝试使用training/webapp镜像创建一个Web容器,并创建一个数据卷挂载到容器的/opt/webapp目录:

1
[root@envythink ~]# docker run -d -P --name web --mount type=bind,source=/webapp,destination=/opt/webapp training/webapp python app.py

其实上述这个命令等同于使用之前旧的-v标记,同样可以实现上面的目的:

1
[root@envythink ~]# docker run -d -P --name web -v /webapp:/opt/webapp training/webapp python app.py

这个功能在进行应用测试的时候非常方便,如用户可以放置一些程序或者数据到本地目录中实时进行更新,然后在容器内运行和使用。

请注意,本地目录的路径必须是绝对路径,容器内的路径可以为相对路径。如果目录不存在,那么Docker会自动创建。

当开发者需要将宿主机内的某个文件作为volume挂载到容器中时,那么该文件必须存在于宿主机内,否则无法挂载,因为Docker默认是支持目录挂载。

举个例子,开发者想将当前目录下的hello.txt文件挂载到容器别名为envy的/opt目录下,此时命令应该为:

1
[root@envythink ~]# docker run -it -d --name envy -v ${pwd}/hello.txt:/opt/hello.txt ubuntu:latest /bin/bash

另外Docker挂载数据卷的默认权限是读写(rw),用户也可以通过使用(ro)来指定为只读:

1
[root@envythink ~]# docker run -it -d --name envyhello -v envy_vol:/data:ro ubuntu:latest /bin/bash

这样在容器目录后面添加了:ro之后,容器内对所挂载数据卷内的数据就无法修改了。

请注意,如果直接挂载一个文件到容器,使用文件编辑工具,包括vi或者sed --in-place的时候,可能会造成文件inode的改变,而从Docker1.1.0开始,这样会导致报错误信息,因此笔者比较推荐的方式是直接挂载文件所在的目录到容器内。

数据卷容器

如果用户需要在多个容器之间共享一些持续更新的数据,那么最简单的方式就是使用数据卷容器。数据卷容器,顾名思义就是一个专门用于提供数据卷给其他容器挂载的容器。

首先,我们创建一个数据卷容器dbdata,并在其中创建一个数据卷挂载到/dbdata,使用的命令如下:

1
2
3
4
[root@envythink ~]# docker run -it -d -v /dbdata --name dbdata ubuntu:latest
root@e8b49864e280:/# ls
bin dbdata etc lib lib64 media opt root sbin sys usr
boot dev home lib32 libx32 mnt proc run srv tmp var

可以看到新创建的dbdata容器的/dbdata目录结构如上所示。

接着,用户可以在其他容器中使用--volumes-from参数来挂载dbdata容器中的数据卷。举个例子,这里创建两个容器db1和db2,它们分别从dbdata数据卷容器中挂载数据卷:

1
2
[root@envythink ~]# docker run -it -d --volumes-from dbdata --name db1 ubuntu:latest
[root@envythink ~]# docker run -it -d --volumes-from dbdata --name db2 ubuntu:latest

此时容器db1和db2都挂载同一个数据卷到相同的/dbdata目录,这样三个容器中任意一方在该目录下的写入,其他容器均能看得到。举个例子,我们可以在dbdata容器内创建一个test文件:

1
2
3
4
root@e8b49864e280:/# cd dbdata
root@e8b49864e280:/dbdata# touch test
root@e8b49864e280:/dbdata# ls
test

之后进入db1容器中,可以发现其中确实已经存在了名为test的文件:

1
2
3
root@15ffe1a3a4c0:/# cd dbdata 
root@15ffe1a3a4c0:/dbdata# ls
test

开发者可以多次使用--volumes-from参数来从多个容器挂载多个数据卷,还可以从其他已经挂载了容器卷的容器来挂载数据卷:

1
[root@envythink ~]# docker run -it -d --name db3 --volumes-from db1 ubuntu:latest

其实这里多次使用的数据卷是dbdata容器的/dbdata目录,而不是宿主机中匿名的数据目录。

请注意,使用--volumes-from参数所挂载数据卷的容器,其自身并不需要保持在运行状态。

如果删除了挂载的容器(如这里的dbdata、db1、db2),其实数据卷并不会被自动删除。如果开发者想要删除一个数据卷,就必须在删除最后一个还挂载这它的容器时,显式使用docker rm -v命令来指定同时删除关联的容器。

使用数据卷容器还可以让用户在容器之间自由地升级和移动数据卷,关于这一点即将进行介绍。

利用数据卷容器来迁移数据

开发者可以使用数据卷容器来对其中的数据卷进行备份、恢复,以实现数据的迁移。

备份

举个例子,开发者需要对之前创建的dbdata数据卷容器中的数据卷进行备份,那么可以使用如下的命令:

1
[root@envythink ~]# docker run --volumes-from dbdata -v ${pwd}:/backup --name worker ubuntu:latest tar cvf /backup/backup.tar /dbdata

解释一下上述命令,首先使用ubuntu:latest镜像创建了一个名为worker的容器,并使用--volumes-from dbdata参数来让worker容器挂载dbdata容器的数据卷(即dbdata数据卷);接着使用-v ${pwd}:/backup参数来挂载本地的当前目录到worker容器的/backup目录;然后当worker容器启动后,使用tar cvf /backup/backup.tar /dbdata命令将/dbdata下内容备份为容器内的/backup/backup.tar,也就是宿主机当前目录下的backup.tar文件。

请注意,前面的tar cvf 打包而成的文件 待打包的文件命令格式不要搞错,同时这里粘贴几个常用的命令:

1
2
3
4
5
6
7
8
#  将etc下的所有目录打包为etcbak.tar文件
tar cvf etcbak.tar etc/
# 将etcbak.tar文件进行解压(这个文件只被打包)
tar xvf etcbak.tar
# 将etc下的所有目录打包压缩为etcbak.tar文件
tar cvzf etcbak.tar.gz etc/
# 将etcbak.tar文件进行解压(这个文件被打包和压缩过)
tar zxvf etcbak.tar.gz

恢复

开发者如果想恢复数据到一个容器中,那么可以按照如下操作:

第一步,创建一个带有数据卷的容器dbdata2,使用的命令如下:

1
[root@envythink ~]# docker run -v /dbdata --name dbdata2 ubuntu:latest /bin/bash

第二步,创建另一个新的容器,用来挂载dbdata2的容器,并使用untar命令来解压备份文件到所挂载的容器卷中:

1
[root@envythink ~]# docker run --volumes-from dbdata2 -v ${pwd}:/backup worldenvy tar xvf /backup/backup.tar

#小结
Docker采用数据卷机制为数据管理提供了方便的操作,本文介绍了通过数据卷和数据卷容器对容器内的数据进行共享、备份和恢复等操作,通过这些操作,即使容器在运行中出现故障,用户也不用担心数据会发生丢失,只需要快速的重新创建容器即可。

当然了在生产环境中,笔者推荐在使用数据卷或者数据卷容器之外,应当定期将主机的本地数据进行备份,或者使用支持容错的存储系统,包括RAID或者分布式文件系统,如Ceph、GPFS和HDFS等。

在实际工作中也可能出现某些数据只是作为中间产物而存在的,我们是不希望它能保存在宿主机或者容器中,此时就可以创建tmpfs类型的数据卷,该数据卷中的数据只存在于内存中,容器退出后会自动删除,这样就很好的解决了我们的问题。