.. include:: ../../sumaccess.rst ================== 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挂载到容器上,可以很方便地将数据添加到容器中供其中的进程使用。 多个容器可以共享同一个volume,为不同容器之间的数据共享提供了便利。 创建volume ------------ Docker1.9版本引入了新的子命令,即docker volume。用户可以使用这个命令对volume进行创建、查看和删除,与此同时,传统的-v参数创建volume的方式也得到了保留。 用户可以使用docker volume create创建一个volume,以下命令创建了一个指定名字的volume。 .. code:: bash $ [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,并挂载到容器中的指定目录下。 .. code:: bash $ docker run -d -v /tmp nginx:1.14-alpine sh 以下命令创建了一个指定名字的volume,并挂载到容器中的/data目录下。 .. code:: bash $ 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包括其在宿主机中该文件夹的位置等信息。 .. code-block:: bash :linenos: $ docker volume inspect volume_t2 [ { "CreatedAt": "2021-05-21T09:54:09+08:00", "Driver": "local", "Labels": null, "Mountpoint": "/var/lib/docker/volumes/volume_t2/_data", "Name": "volume_t2", "Options": null, "Scope": "local" } ] 挂载volume --------------- 用户在使用docker run或docker create创建新容器时,可以使用-v标签为容器添加volume。用户可以将自行创建或者由Docker创建的volume挂载到容器中,也可以将宿主机上的目录或者文件作为volume挂载到容器中。下面分别介绍这两种挂载方式。 用户可以使用如下命令创建volume,并将其创建的volume挂载到容器中的/data目录下。 .. code-block:: bash $ 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目录下。 .. code-block:: bash $ docker run --rm -v /data nginx:1.14-alpine sh 以上命令都是将自行创建或者由Docker创建的volume挂载到容器中。Docker同时也允许我们将宿主机上的目录挂载到容器中。 .. code:: bash $ 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挂载到容器中: .. code:: bash $ docker run --rm -v host_file:container_file ubuntu /bin/bash 使用上条命令将主机中的/host/file文件作为一个volume挂载到容器中的/container/file。文件必须使用绝对路径,如果文件中不存在/host/file,则Docker会创建一个同名空目录。挂载后文件内容与宿主机中的文件一致,也就是说如果容器中原本存在/container/file,该文件将被隐藏。 .. TIP:: 将主机上的文件或文件夹作为volume挂载时,可以使用:ro指定该volume为只读。 .. code:: $ 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为私有数据卷。 .. code-block:: bash :linenos: :caption: 容器卷允许多挂载测试 :name: 容器卷允许多挂载测试 $ echo 'start test' > /tmp/test.log # 创建测试数据 $ docker run -it --rm --name container-1 -v /tmp/test.log:/tmp/container.log:z,rw nginx:1.14-alpine sh # 启动第一个容器 / # cat /tmp/container.log start test / # echo ' container-1' >> /tmp/container.log # 写入内容到共享卷 // 新开启一个tty终端 $ docker run -it --rm --name container-2 -v /tmp/test.log:/tmp/container.log:z,rw nginx:1.14-alpine sh / # cat /tmp/container.log start test container-1 在使用docker run或docker create创建新容器时,可以使用多个-v标签为容器添加多个volume。 .. code:: bash $ docker run -it -v /host/dir1:/contaier/dir1 -v /host/dir2:/container/dir2 ubuntu /bin/bash Dockerfile引用挂载卷 ----------------------- 使用VOLUME指令向容器添加volume。 .. code:: bash $ VOLUME /data 在使用docker build命令生成镜像并且以该镜像启动容器时会挂载一个volume到/data。与前面的volume_t2例子类似,如果镜像中存在/data文件夹,这个文件夹中的内容将全部被复制到宿主机中对应的文件夹中,并且根据容器中的文件设置合适的权限和所有者。 .. NOTE:: 类似的,可以使用VOLUME指令添加多个volume .. code:: VOLUME ["/data1", "data2"] 与使用docker run -v不同的是,VOLUME指令不能挂载主机中指定的文件夹。这是为了保证Dockerfile的可移植性,因为不能保证所有的宿主机都有对应的文件夹。 需要注意的是,在Dockerfile中使用VOLUME指令之后的代码,如果尝试对这个volume进行修改,这些修改都不会生效。在下面的例子中,在创建volume后,尝试在其中添加一些初始化的文件并改变文件所有权。 .. code-block:: Dockerfile :linenos: :caption: Dockerfile :name: Dockerfile FROM ubuntu RUN useradd foo VOLUME /foo RUN touch /foo/bar.sh RUN chown -R foo:foo /foo .. code:: bash $ 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 查看挂载内容 .. code-block:: bash :linenos: :emphasize-lines: 2 $ docker run --rm -it my_ubuntu:latest ls /foo $ 通过这个Dockerfile创建镜像并启动容器后,该容器中存在用户foo,并且能看到在/data挂载的volume,但是/data文件夹内并没有文件file, 更别说file的所有者并没有被改变为foo。这是由于Dockerfile中除了FROM指令的每一行都是基于上一行生成的临时镜像运行一个容器, 执行一条指令并执行类似docker commit的命令得到一个新的镜像,这条类似docker commit的命令不会对挂载的volume进行保存。 所以上面的Dockerfile最后两行执行时,都会在一个临时的容器上挂载/data,并对这个临时的volume进行操作,但是这一行指令执行并提交后, 这个临时的volume没有被保存,我们通过最后生成的镜像创建的容器所挂载的volume是没有操作过的。 如果想要对volume进行初始化或者改变所有者,可以使用以下方式。 .. code-block:: Dockerfile :linenos: :caption: Dockerfile :name: Dockerfile FROM ubuntu RUN useradd foo RUN mkdir /foo && touch /foo/bar.sh RUN chown -R foo:foo /foo VOLUME /foo docker build 构建该镜像 .. code:: bash $ 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 查看挂载内容 .. code-block:: :linenos: :emphasize-lines: 2 $ docker run --rm -it ubuntu:v1 ls /foo bar.sh Dockerfile中Volume的应用场景 ++++++++++++++++++++++++++++ 通过上面的案例我们了解了怎么在Dockerfile中声明 首先创建两个独立的Docker容器 .. code:: $ docker run --name u1 -d ubuntu:v1 sleep 500 0e089f9fdc1a0063e872b2a24c8525a180686e23e9a9d6341942e61c02075fd5 $ docker run --name u2 -d ubuntu:v1 sleep 500 68a9313106132fb504eb434f4d4a03b48883fa231b6e9b7e243b2f9ad7ded249 查看生成的随机id的volume .. code:: $ docker volume ls DRIVER VOLUME NAME local 1fc430323d92e5ef446464d00b96e1f5326773359592f91294d12a466c4cd108 local 50a8727c18b968158827cbd286afff703cc81e5c5dac4dc9e7f2f18cf02350b9 接下来创建一个新的容器,引用u1的卷 .. code:: $ 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内查看内容是否自动更新 .. code-block:: :linenos: :emphasize-lines: 5-6 $ docker exec -it u1 sh # echo 'hello u1'>>/foo/bar.sh # exit $ docker exec -it u3 cat /foo/bar.sh hello u1 .. TIP:: - 如果被共享的容器有多个volume,新容器也将有多个volume,并且其挂载的目的目录也与被共享的容器中的相同。 - 一个容器挂载了一个volume,即使这个容器停止运行,该volume仍然存在,其他容器也可以使用 ``--volumes-from`` 与这个容器共享volume。如果有一些数据,比如配置文件、数据文件等,要在多个容器之间共享,一种常见的做法是创建一个数据容器,其他的容器与之共享volume。 - 可以使用多个 ``--volumes-from`` 标签,使得容器与多个已有容器共享volume。 删除volume ------------------- 如果创建容器时从容器中挂载了volume,在 ``/var/lib/docker/volumes`` 下会生成与volume对应的目录,使用 ``docker rm`` 删除容器并不会删除与volume对应的目录,这些目录会占据不必要的存储空间,即使可以手动删除,因为有些随机生成的目录名称是无意义的随机字符串,要知道它们是否与未被删除的容器对应也十分麻烦。所以在删除容器时需要对容器的volume妥善处理。在删除容器时一并删除volume有以下3种方法。 - 使用 ``docker volume rm `` 删除volume - 使用 ``docker rm -v `` 删除容器 - 在运行容器时使用 ``docker run --rm, --rm `` 标签会在容器停止运行时删除容器以及容器所挂载的volume .. WARNING:: 在使用docker volume rm删除volume时,只有当没有任何容器使用该volume的时候,该volume才能成功删除.另外两种方法只会对挂载在该容器上的未命名的volume进行删除,而会对用户指定名字的volume进行保留 如果volume是在创建容器时从宿主机中挂载的,无论对容器进行任何操作都不会导致其在宿主机中被删除,如果不需要这些文件,只能手动删除它们。 备份、恢复或迁移volume ------------------------ `volume作为数据的载体,在很多情况下需要对其中的数据进行备份、迁移,或是从已有数据恢复。` 如果需要将Volume里面的数据备份,一个很容易想到的方法是使用docker inspect命令查找到volume在宿主机上对应的文件夹位置,然后复制其中的内容或是使用tar进行打包;同样地,如果需要恢复某个volume中的数据,可以查找到volume对应的文件夹,将数据复制进这个文件夹或是使用tar从存档文件中恢复。 这些做法可行但并不值得推荐,下面推荐一个用 ``--volumes-from`` 实现的volume的备份与恢复方法。 备份volume可以使用以下方法: .. code-block:: :linenos: :emphasize-lines: 4 :caption: backup container :name: backup container $ docker run --rm --volumes-from <被备份的容器> -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /<被备份的数据目录> # 例如备份刚才的u1容器中/foo目录的数据 $ 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可以使用以下方法: .. code-block:: :linenos: :caption: 恢复容器数据 :name: 恢复容器数据 $ docker run -it --name u_back ubuntu:v1 /bin/bash $ 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中了。 .. include:: ../../comment.rst