一、docker数据卷

Docker镜像是由一系列的只读层组合而来的,当启动一个容器时, Docker加载镜像的所有只读层,并在最上层加入一个读写层。这个设计使得Docker可以提高镜像构建、存储和分发的效率,节省了时间和存储空间,然而也存在如下问题。

  • 容器中的文件在宿主机上存在形式复杂,不能在宿主机上很方便地对容器中的文件进行访问。
  • 多个容器间的数据无法共享。
  • 删除容器时,容器的数据丢失。

docker引入了数据集(volume)来解决这些问题。volume是存在于一个或多个容器中特点文件或文件夹,这个目录独立于联合文件系统的形式在宿主机存在,并为数据的共享与持久化提供便利:

  • volume在容器创建时就会初始化,在容器运行时就可以使用其中的文件。
  • volume能在不同的容器之间共享和重用
  • 对volume中数据的操作会马上生效
  • 对volume中数据的操作不会影响到镜像本身
  • volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何容器使用的volume也不会被Docker删除。

Docker提供了volumedriver接口,通过实现该接口,可以为Docker容器提供不同的volume存储支持。当前官方默认实现了local这种volumedriver,它使用宿主机的文件系统为Docker容器提供volume。

二、数据卷使用方式

添加volume,类似于Linux的mount操作。用户将一个文件夹作为volume挂载到容器上,可以方便地添加数据到容器,供其中的进程使用。

docker1.9引入了docker volume。通过这个命令创建、查看、删除。

1.创建volume

用户可以使用docker volume create创建一个volume

1
docker volume create --name vol

Docker当前并未对volume的大小提供配额管理,用户在创建volume时也无法指定volume的大小。在用户使用Docker创建volume时,由于采用的是默认的local volumedriver,所以volume的文件系统默认使用宿主机的文件系统,如果用户需要创建其他文件系统的volume,则需要使用其他的volumedriver。

创建容器时。可以使用-v为容器添加volume

1
2
$ sudo docker run -d -v /data ubuntu /bin/bash
$ sudo docker run -d -v vol_simple: /data ubuntu /bin/bash

Docker在创建volume的时候会在宿主机/var/lib/docker/volume/中创建一个以volume ID为名的目录,并将volume中的内容存储在名为_data的目录下。

1
2
3
4
5
6
7
8
$sudo docker volume inspect vol_simple
[
{
"Name" : "vol_simple",
"Driver": "local",
"Mountpoint" : "/var/lib/docker/volumes/vol_simplel_data"
}
]

2.挂载volume

用户可以将自行创建或者由Docker创建的volume挂载到容器中,也可以将宿主机上的目录或者文件作为volume挂载到容器中。下面分别介绍这两种挂载方式。

用户可以使用如下命令创建volume,并将其创建的volume挂载到容器中的/data目录下。

1
2
docker volume create --name vol_simple
docker run -d -v vol_simple: /data ubuntu /bin/bash

如果用户不执行第一条命令而直接执行第二条命令的话,Docker会代替用户来创建一个名为vol_simple的volume,并将其挂载到容器中的/data目录下。

用户也可以使用如下命令创建一个随机ID的volume,并将其挂载到/data目录下。

1
docker run -d -v /data ubuntu /bin/bash

以上命令都是将自行创建或者由Docker创建的volume挂载到容器中。Docker同时也允许我们将宿主机上的目录挂载到容器中。

1
2
docker run -v /host/dir:/container/dir ubuntu /bin/bash
docker run -it --name vol_file -v /host/file:/container/file ubuntu /bin/bash

/host/dir文件夹中所有文件都可以在容器的/container/dir文件夹下被访问,并且容器中文件夹下内容会被隐藏。也可以用挂载单个文件,文件夹使用绝对路径

可以使用:ro指定该volume为只读。可以在volume挂载时使用Z和z来指定该volume是否可以共享。Docker中默认的是z,即共享该volume。用户也可以在挂载时使用Z来标注该volume为私有数据卷。

1
2
3
4
#私有
docker run -it --name vol_unshared -v /host/dir:/container/dir:Z ubuntu /bin/bash
#只读
docker run -it --name vol_read_only -v /host/dir:/container/dir:ro ubuntu /bin/bash

使用多个-v标签为容器添加多个volume

1
docker run -it --name vol_mult -v /data1 -v /data2 -v /host/dir:/container/dir ubuntu /bin/bash

3.Dockerfile添加volume

使用VOLUME指令向容器添加volume。

1
2
VOLUME /data
vOLUME [ "/data1", "/data2"]

在使用docker build命令生成镜像并且以该镜像启动容器时会挂载一个volume到/data。与上文中vol_simple例子类似,如果镜像中存在/data文件夹,这个文件夹中的内容将全部被复制到宿主机中对应的文件夹中,并且根据容器中的文件设置合适的权限和所有者。

需要注意的是,在Dockerfile中使用VOLUME指令之后的代码,如果尝试对这个volume进行修改,这些修改都不会生效。

1
2
3
4
5
6
#在创建volume后,尝试在其中添加一些初始化的文件并改变文件所有权W。
FROM ubuntu
RUN useradd foo
VOLUME /data
RUN touch /data/file
RUN chown -R foo:foo /data

创建镜像并启动容器后,该容器中存在用户foo,并且能看到在/data挂载的volume,但是/data文件夹内并没有文件file,更别说file的所有者并没有被改变为foo。这是由于Dockerfile中除了FROM指令的每一行都是基于上一行生成的临时镜像运行一个容器,执行一条指令并执行类似docker commit的命令得到一个新的镜像,这条类似docker commit的命令不会对挂载的volume进行保存。所以上面的Dockerfile最后两行执行时,都会在一个临时的容器上挂载/data,并对这个临时的volume进行操作,但是这一行指令执行并提交后,这个临时的volume没有被保存,我们通过最后生成的镜像创建的容器所挂载的volume是没有操作过的。

1
2
3
4
5
6
7
8
9
10
11
12
#对volume初始化改变所有这
FROM ubuntu
RUN useradd foo
RUN mkdir /data && touch /data/file
RUN chown -R foo:foo ldata
VOLUME /data

#与RUN指令在镜像构建过程中执行不同,CND指令和ENTRVPOINT指令是在容器启动时执行,使用如下Dockerfile也可以达到对volume初始化的目的。
FROM ubuntu
RUN useradd foo
VOLUME /data
CMD touch /data/file && chown -R foo:foo /data

4.共享volume

在使用docker run或docker create创建新容器时,可以使用–volumes-from`标签使得容器与已有的容器共享volume。

1
2
3
4
5
#新创建的容器vol_use与之前创建的容器vol_simple共享volume,这个volume目的目录也是/data。如果被共享的容器有多个volume(如上文中出现的vol_mult ),新容器也将有多个volume,并且其挂载的目的目录也与vol_mult中的相同。
docker run --rm -it --name vol_use --volumes-from vol_simple ubuntu /bin/bash

#使用用多个--volumes-from标签,使得容器与多个已有容器共享volume。
docker run --rm -it --name vol_use_mult --volumes-from vol_1 --volumes-from vol_2 ubuntu /bin/bash

一个容器挂载了一个volume,即使这个容器停止运行,该volume仍然存在,其他容器也可以使用–volumes-from与这个容器共享volume。如果有一些数据,比如配置文件、数据文件等,要在多个容器之间共享,一种常见的做法是创建一个数据容器,其他的容器与之共享volume。

1
2
3
docker run --name vol_data -v /data ubuntu echo "This is a data-only container"
docker run -it --name vol_share1 --volumes-from vol_data ubuntu /bin/bash
docker run -it --name vol_share2 --volumes-from vol_data ubuntu /bin/bash

首先创建了一个挂载了volume的数据容器vol_data,这个容器仅仅输出了一条提示后就停止运行以避免浪费资源。接下来的两个容器vol_share1vol_sharez与这个数据容器共享这个volume。这样就将两个需要共享数据的容器进行了较好的解耦,避免了容器之间因为共享数据而产生相互依赖。

5.删除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是在创建容器时从宿主机中挂载的,无论对容器进行任何操作都不会导致其在宿主机中被删除,如果不需要这些文件,只能手动删除它们。

6.备份、恢复或迁移volume

一个方法是使用docker inspect命令查找到/data在宿主机上对应的文件夹位置,然后复制其中的内容或是使用tar进行打包;同样地,如果需要恢复某个volume中的数据,可以查找到volume对应的文件夹,将数据复制进这个文件夹或是使用tar从存档文件中恢复。这些做法可行但并不值得推荐。

推荐一个用--volumes-from实现的volume的备份与恢复方法。

1
2
3
#vol_simple容器包含了我们希望备份的一个volume,这行命令启动了另外一个临时的容器,这个容器挂载了两个volume,第一个volume来自于vol_simple容器的共享,也就是需要备份的volume,第二个volume将宿主机的当前目录挂载到容器的/backup下。容器运行后将要备份的内容(l/data文件夹)备份到/backup/data.tar,然后删除容器,备份后的data.tar就留在了当前目录。

docker run --rm --volumes-from vol_simple -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /data

恢复volume可以使用以下方法。

1
2
3
4
#首先运行了一个新容器作为数据恢复的目标。第二行指令启动了一个临时容器,这个容器挂载了两个volume,第一个volume与要恢复的volume共享,第二个volume将宿主机的当前目录挂载到容器的/backup下。由于之前备份的data.tar在当前目录下,那么它在容器中的/backup也能访问到,容器启动后将这个存档文件中的/data恢复到根目录下,然后删除容器,恢复后的数据就在vol_bck的volume中了。

docker run -it --name vol_bck -v /data ubuntu /bin/bash
docker run --rm --volumes-from vol_bck -v $(pwd): /backup ubuntu tar xvf /backup/data.tar -C /

三、数据卷原理

Docker的volume的本质是容器中一个特殊的目录。在容器的创建过程中,Docker会将宿主机上的指定目录(一个以volume ID为名称的目录,或者指定的宿主机目录)挂载到容器中指定的目录上,这里使用的挂载方法是绑定挂载( bind mount ),故挂载完成后的宿主机目录和容器内的目标目录表现一致。

1
2
3
4
5
6
#用户执行docker run -v /data busybox /bin/sh指定容器里的/data目录为一个volume,实际上相当于在创建容器的过程中在容器里执行如下代码:
#将宿主机上的volume_id目录绑定挂载到rootfs中指定的挂载点/data上
mount(“/var/lib/docker/volumes/volume_id/_data”, “rootfs/data”, “none”, NS_BIND,NULL)
#而如果用户执行的是docker run -v /var/log:/data busybox /bin/sh的话,则实际对应了:
#将宿主机上的/var/log目录绑定挂载到rootfs中指定的挂载点/data上
mount( "/var/log”, “rootfs/data”, “none”, MS_BIND,NULL)

在处理完所有的mount操作之后(真正需要Docker容器挂载的除了volume目录还包括rootfs,init-layer里的内容,/proc设备等),Docker只需要通过chdirpivot_root切换进程的根目录到rootfs中,这样容器内部进程就只能看见以rootfs为根的文件内容以及被mount到rootfs之下的各项目录了。例如,下面的data目录就是生成出来的volume挂载点了:

1
2
root@in_the_container:/ ls
bin boot data dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usrvar

Docker daemon在为容器挂载目录的过程中着重处理的事情就是是如何组装出合适的mount指令,而在源码中,挂载点这个结构体中则包含了组装mount命令所有需要的信息。

1.创建volume

volume的来源只有两种,即用户通过命令行指定的绑定挂载和从其他容器共享。
Docker首先需要根据用户指定的volume类型,判断并新建对应的挂载点。Docker在创建volume的过程中主要进行了如下操作。

volume的创建依照容器启动的过程可以分为两个阶段。第一阶段为容器创建阶段,Docker根据两种不同的volume来源组装挂载点列表。第二阶段为容器启动阶段,libcontainer使用组装好的挂载点列表进行mount操作,完成volume的创建。

Docker会负责维护一个本地的volume列表,该列表中存储了所有本地有名字的volume,列表的键为volume的名字,值为volume的存储路径和驱动名称。如果用户指定了volume的名字,那么Docker会在volume列表中查找是否已经有对应的volume。若Docker没有在volume列表中找到对应的volume,Docker会创建一个以此名字命名的volume,并将该volume加入到Docker维护的volume列表中,然后创建一个新的挂载点。如果找到了对应的volume,则将其中的信息复制到新创建的挂载点中(主要信息为volume的源地址)。

Docker为每一个容器都维护着如下所示的挂载点组成的列表,在这个挂载点中填写上述宿主机上源目录路径、容器内挂载位置、读写权限等信息。

1
2
3
4
5
6
7
8
9
10
11
type MountPoint struct {
Source string //源目录
Destination string //目的目录
Rw bool //是否可写
Name string // volume的名字
Driver string // volume driver的名字
Volume Volume //该挂载点所对应的本地volume信息
Mode string //挂载的模式
Propagation string //挂载的拓展选项
Named bool //该挂载点是否被命名
}

对于共享的volume,Docker从输入参数中解析出volume容器ID是否可读”两个变量。接着根据容器ID查找到对应的容器对象Container,然后根据该对象中volumes数组复制并创建新的挂载点,并加入到上面提到的挂载点列表中。

不同参数所感的主要字段

成员 -v vol_simple:/containerdir -v /containerdir -v /hostdir:/containerdir:ro
Source /var/lib/docker/volumes/vol_simple/_data /var/lib/docker/volumes/随机ID/_data /hostdir
Destination /containerdir /containerdir /containerdir
Rw true true false
Name vol_simple 随机ID nill
Named true true true

2.删除volume

  • 使用docker volume rm删除volume时,Docker首先会检查是否还有容器在使用这个volume,如果这个volume还被其他容器所使用,则返回错误信息,并终止删除。如果没有容器在使用这个volume,那么Docker会将这个volume在宿主机上对应的目录删除,并删除其维护的本地volume列表中的相关信息。
  • 使用docker run --rmdocker rm -v进行volume删除时,其volume的删除过程与第一种类似,不过需要注意的是,这种删除的方式会过滤掉挂载点中Named字段为true的volume,也就是说这种方式并不会对命名的volume进行删除

3.volume相关配置文件

Docker的每个容器在/var/lib/docker/containers文件夹下有一个以容器ID命名的子文件夹,这个子文件夹中的config.json文件是这个容器的配置文件,可以从中看到这个容器所使用的volumeID以及它们的可写情况。如果你要查看volume的具体信息,你可以在/var/lib/docker/volumes文件夹下找与volume ID或者volume名字命名的子文件夹,这个子文件夹中的_data目录存储了该volume中的所有内容。