5. Docker数据卷¶
前面介绍了Docker的镜像是由一系列的只读层组合而来的,当启动一个容器时,Docker加载镜像的所有只读层,并在最上层加入一个读写层。 这个设计使得Docker可以提高镜像构建、存储和分发的效率,节省了时间和存储空间,然而也存在如下问题。
容器中的文件在宿主机上存在形式复杂,不能在宿主机上很方便地对容器中的文件进行访问。
多个容器之间的数据无法共享。
当删除容器时,容器产生的数据将丢失。
为了解决这些问题,Docker引入了数据卷(volume)机制。 volume 是存在于一个或多个容器中的特定文件或文件夹,这个目录以独立于联合文件系统的形式在宿主机中存在,并为数据的共享与持久化提供以下便利。
volume在容器创建时就会初始化,在容器运行时就可以使用其中的文件。
volume能在不同的容器之间共享和重用。
对volume中数据的操作会马上生效。
对volume中数据的操作不会影响到镜像本身。
volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何容器使用的volume也不会被Docker删除。
Docker提供了volumedriver接口,通过实现该接口,我们可以为Docker容器提供不同的volume存储支持。当前官方默认实现了local这种volumedriver,它使用宿主机的文件系统为Docker容器提供volume。
5.1. 数据卷的使用方式¶
为容器添加volume,类似于Linux的mount操作,用户将一个文件夹作为volume挂载到容器上,可以很方便地将数据添加到容器中供其中的进程使用。 多个容器可以共享同一个volume,为不同容器之间的数据共享提供了便利。
5.1.1. 创建volume¶
Docker1.9版本引入了新的子命令,即docker volume。用户可以使用这个命令对volume进行创建、查看和删除,与此同时,传统的-v参数创建volume的方式也得到了保留。
用户可以使用docker volume create创建一个volume,以下命令创建了一个指定名字的volume。
$ [root@demo ~]<20210521 09:41:03># docker volume create --name volume_t1
volume_t1
[root@demo ~]<20210521 09:45:07># docker volume ls
DRIVER VOLUME NAME
local volume_t1
用户在使用docker run或docker create创建新容器时,也可以使用-v标签为容器添加volume,以下命令创建了一个随机名字的volume,并挂载到容器中的指定目录下。
$ docker run -d -v /tmp nginx:1.14-alpine sh
以下命令创建了一个指定名字的volume,并挂载到容器中的/data目录下。
$ docker run -d -v volume_t2:/data nginx:1.14-alpine sh
Docker在创建volume的时候会在宿主机/var/lib/docker/volume/中创建一个以volume ID为名的目录,并将volume中的内容存储在名为_data的目录下。
使用docker volume inspect命令可以获得该volume包括其在宿主机中该文件夹的位置等信息。
1 $ docker volume inspect volume_t2
2 [
3 {
4 "CreatedAt": "2021-05-21T09:54:09+08:00",
5 "Driver": "local",
6 "Labels": null,
7 "Mountpoint": "/var/lib/docker/volumes/volume_t2/_data",
8 "Name": "volume_t2",
9 "Options": null,
10 "Scope": "local"
11 }
12 ]
5.1.2. 挂载volume¶
用户在使用docker run或docker create创建新容器时,可以使用-v标签为容器添加volume。用户可以将自行创建或者由Docker创建的volume挂载到容器中,也可以将宿主机上的目录或者文件作为volume挂载到容器中。下面分别介绍这两种挂载方式。
用户可以使用如下命令创建volume,并将其创建的volume挂载到容器中的/data目录下。
$ docker volume create --name volume_t3
volume_t3
$ docker run --rm -v volume_t3:/data nginx:1.14-alpine sh
如果用户不执行第一条命令而直接执行第二条命令的话,Docker会代替用户来创建一个名为vol_t3的volume,并将其挂载到容器中的/data目录下。
用户也可以使用如下命令创建一个随机ID的volume,并将其挂载到/data目录下。
$ docker run --rm -v /data nginx:1.14-alpine sh
以上命令都是将自行创建或者由Docker创建的volume挂载到容器中。Docker同时也允许我们将宿主机上的目录挂载到容器中。
$ docker run --rm -v /host/dir:/container/dir ubuntu /bin/bash
使用以上命令将宿主机中的/host/dir文件夹作为一个volume挂载到容器中的/container/dir。注意以下几点:
文件夹必须使用绝对路径。
如果宿主机中不存在/host/dir,将创建一个空文件夹。
在/host/dir文件夹中的所有文件或文件夹可以在容器的/container/dir文件夹下被访问。
如果镜像中原本存在/container/dir文件夹,该文件夹下原有的内容将被隐藏,以保持与宿主机中的文件夹一致。
用户还可以将单个的文件作为volume挂载到容器中:
$ docker run --rm -v host_file:container_file ubuntu /bin/bash
使用上条命令将主机中的/host/file文件作为一个volume挂载到容器中的/container/file。文件必须使用绝对路径,如果文件中不存在/host/file,则Docker会创建一个同名空目录。挂载后文件内容与宿主机中的文件一致,也就是说如果容器中原本存在/container/file,该文件将被隐藏。
小技巧
将主机上的文件或文件夹作为volume挂载时,可以使用:ro指定该volume为只读。
$ echo 'My Logs' > /tmp/test.log
$ docker run -it --rm -v /tmp/test.log:/tmp/container.log:ro nginx:1.14-alpine sh
/ # cat /tmp/container.log
My Logs
/ # echo 'container' >> /tmp/container.log
sh: can't create /tmp/container.log: Read-only file system
/ # exit
类似于SELinux这类的标签系统,可以在volume挂载时使用z和Z来指定该volume是否可以共享。Docker中默认的是z,即共享该volume。 用户也可以在挂载时使用Z来标注该volume为私有数据卷。
1$ echo 'start test' > /tmp/test.log # 创建测试数据
2$ docker run -it --rm --name container-1 -v /tmp/test.log:/tmp/container.log:z,rw nginx:1.14-alpine sh # 启动第一个容器
3/ # cat /tmp/container.log
4start test
5/ # echo ' container-1' >> /tmp/container.log # 写入内容到共享卷
6
7
8// 新开启一个tty终端
9$ docker run -it --rm --name container-2 -v /tmp/test.log:/tmp/container.log:z,rw nginx:1.14-alpine sh
10/ # cat /tmp/container.log
11start test
12container-1
在使用docker run或docker create创建新容器时,可以使用多个-v标签为容器添加多个volume。
$ docker run -it -v /host/dir1:/contaier/dir1 -v /host/dir2:/container/dir2 ubuntu /bin/bash
5.1.3. Dockerfile引用挂载卷¶
使用VOLUME指令向容器添加volume。
$ VOLUME /data
在使用docker build命令生成镜像并且以该镜像启动容器时会挂载一个volume到/data。与前面的volume_t2例子类似,如果镜像中存在/data文件夹,这个文件夹中的内容将全部被复制到宿主机中对应的文件夹中,并且根据容器中的文件设置合适的权限和所有者。
注解
类似的,可以使用VOLUME指令添加多个volume
VOLUME ["/data1", "data2"]
与使用docker run -v不同的是,VOLUME指令不能挂载主机中指定的文件夹。这是为了保证Dockerfile的可移植性,因为不能保证所有的宿主机都有对应的文件夹。
需要注意的是,在Dockerfile中使用VOLUME指令之后的代码,如果尝试对这个volume进行修改,这些修改都不会生效。在下面的例子中,在创建volume后,尝试在其中添加一些初始化的文件并改变文件所有权。
1FROM ubuntu
2RUN useradd foo
3VOLUME /foo
4RUN touch /foo/bar.sh
5RUN chown -R foo:foo /foo
$ docker build -t my_ubuntu .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu
---> 7e0aa2d69a15
Step 2/5 : RUN useradd foo
---> Using cache
---> b1415a88d214
Step 3/5 : VOLUME /foo
---> Running in 4fc0d264a00a
Removing intermediate container 4fc0d264a00a
---> c6af3e144134
Step 4/5 : RUN touch /foo/bar.sh
---> Running in d964a67d8f23
Removing intermediate container d964a67d8f23
---> 16b2876cf52a
Step 5/5 : RUN chown -R foo:foo /foo
---> Running in 61727a4c5057
Removing intermediate container 61727a4c5057
---> 68f6dc9e0354
Successfully built 68f6dc9e0354
Successfully tagged my_ubuntu:latest
查看挂载内容
1$ docker run --rm -it my_ubuntu:latest ls /foo
2$
通过这个Dockerfile创建镜像并启动容器后,该容器中存在用户foo,并且能看到在/data挂载的volume,但是/data文件夹内并没有文件file, 更别说file的所有者并没有被改变为foo。这是由于Dockerfile中除了FROM指令的每一行都是基于上一行生成的临时镜像运行一个容器, 执行一条指令并执行类似docker commit的命令得到一个新的镜像,这条类似docker commit的命令不会对挂载的volume进行保存。 所以上面的Dockerfile最后两行执行时,都会在一个临时的容器上挂载/data,并对这个临时的volume进行操作,但是这一行指令执行并提交后, 这个临时的volume没有被保存,我们通过最后生成的镜像创建的容器所挂载的volume是没有操作过的。
如果想要对volume进行初始化或者改变所有者,可以使用以下方式。
1FROM ubuntu
2RUN useradd foo
3RUN mkdir /foo && touch /foo/bar.sh
4RUN chown -R foo:foo /foo
5VOLUME /foo
docker build 构建该镜像
$ docker build -t ubuntu:v1 .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu
---> 7e0aa2d69a15
Step 2/5 : RUN useradd foo
---> Using cache
---> b1415a88d214
Step 3/5 : RUN mkdir /foo && touch /foo/bar.sh
---> Using cache
---> 3e35b90c9e3c
Step 4/5 : RUN chown -R foo:foo /foo
---> Using cache
---> 51db0e326a11
Step 5/5 : VOLUME /foo
---> Using cache
---> 7b99e96e8203
Successfully built 7b99e96e8203
Successfully tagged ubuntu:v1
查看挂载内容
1$ docker run --rm -it ubuntu:v1 ls /foo
2bar.sh
5.1.3.1. Dockerfile中Volume的应用场景¶
通过上面的案例我们了解了怎么在Dockerfile中声明
首先创建两个独立的Docker容器
$ docker run --name u1 -d ubuntu:v1 sleep 500
0e089f9fdc1a0063e872b2a24c8525a180686e23e9a9d6341942e61c02075fd5
$ docker run --name u2 -d ubuntu:v1 sleep 500
68a9313106132fb504eb434f4d4a03b48883fa231b6e9b7e243b2f9ad7ded249
查看生成的随机id的volume
$ docker volume ls
DRIVER VOLUME NAME
local 1fc430323d92e5ef446464d00b96e1f5326773359592f91294d12a466c4cd108
local 50a8727c18b968158827cbd286afff703cc81e5c5dac4dc9e7f2f18cf02350b9
接下来创建一个新的容器,引用u1的卷
$ docker run --name u3 -d --volumes-from u1 ubuntu:v1 sleep 500
98d1ac153a67abf89ac5de770d43f364a68469053a5181f91dac31dbf007aec9
$ docker volume ls # 发现没有新的volume创建出来
DRIVER VOLUME NAME
local 1fc430323d92e5ef446464d00b96e1f5326773359592f91294d12a466c4cd108
local 50a8727c18b968158827cbd286afff703cc81e5c5dac4dc9e7f2f18cf02350b9
接下来在u1容器内的卷空间下写入数据,再从u3内查看内容是否自动更新
1$ docker exec -it u1 sh
2# echo 'hello u1'>>/foo/bar.sh
3# exit
4
5$ docker exec -it u3 cat /foo/bar.sh
6hello u1
小技巧
如果被共享的容器有多个volume,新容器也将有多个volume,并且其挂载的目的目录也与被共享的容器中的相同。
一个容器挂载了一个volume,即使这个容器停止运行,该volume仍然存在,其他容器也可以使用
--volumes-from与这个容器共享volume。如果有一些数据,比如配置文件、数据文件等,要在多个容器之间共享,一种常见的做法是创建一个数据容器,其他的容器与之共享volume。可以使用多个
--volumes-from标签,使得容器与多个已有容器共享volume。
5.1.4. 删除volume¶
如果创建容器时从容器中挂载了volume,在 /var/lib/docker/volumes 下会生成与volume对应的目录,使用 docker rm 删除容器并不会删除与volume对应的目录,这些目录会占据不必要的存储空间,即使可以手动删除,因为有些随机生成的目录名称是无意义的随机字符串,要知道它们是否与未被删除的容器对应也十分麻烦。所以在删除容器时需要对容器的volume妥善处理。在删除容器时一并删除volume有以下3种方法。
使用
docker volume rm <volume_name>删除volume使用
docker rm -v <container_name>删除容器在运行容器时使用 ``docker run –rm, –rm `` 标签会在容器停止运行时删除容器以及容器所挂载的volume
警告
在使用docker volume rm删除volume时,只有当没有任何容器使用该volume的时候,该volume才能成功删除.另外两种方法只会对挂载在该容器上的未命名的volume进行删除,而会对用户指定名字的volume进行保留
如果volume是在创建容器时从宿主机中挂载的,无论对容器进行任何操作都不会导致其在宿主机中被删除,如果不需要这些文件,只能手动删除它们。
5.1.5. 备份、恢复或迁移volume¶
volume作为数据的载体,在很多情况下需要对其中的数据进行备份、迁移,或是从已有数据恢复。
如果需要将Volume里面的数据备份,一个很容易想到的方法是使用docker inspect命令查找到volume在宿主机上对应的文件夹位置,然后复制其中的内容或是使用tar进行打包;同样地,如果需要恢复某个volume中的数据,可以查找到volume对应的文件夹,将数据复制进这个文件夹或是使用tar从存档文件中恢复。
这些做法可行但并不值得推荐,下面推荐一个用 --volumes-from 实现的volume的备份与恢复方法。
备份volume可以使用以下方法:
1$ docker run --rm --volumes-from <被备份的容器> -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /<被备份的数据目录>
2
3# 例如备份刚才的u1容器中/foo目录的数据
4$ docker run --rm --volumes-from u1 -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /foo
u1容器包含了我们希望备份的一个volume,上面这行命令启动了另外一个临时的容器,这个容器挂载了两个volume,第一个volume来自于u1容器的共享, 也就是需要备份的volume,第二个volume将宿主机的当前目录挂载到容器的/backup下。容器运行后将要备份的内容(/foo文件夹)备份到/backup/data.tar, 然后删除容器,备份后的data.tar就留在了当前目录。
恢复volume可以使用以下方法:
1$ docker run -it --name u_back ubuntu:v1 /bin/bash
2$ docker run --rm --volumes-from u_back -v $(pwd):/backup ubuntu tar xvf data.tar -C /foo
首先运行了一个新容器作为数据恢复的目标。第二行指令启动了一个临时容器,这个容器挂载了两个volume,第一个volume与要恢复的volume共享, 第二个volume将宿主机的当前目录挂载到容器的/backup下。由于之前备份的data.tar在当前目录下,那么它在容器中的/backup也能访问到, 容器启动后将这个存档文件中的/data恢复到/foo目录下,然后删除容器,恢复后的数据就在vol_bck的volume中了。