我的Docker之路(二):容器中的文件持久化
上一篇中简单介绍了Docker中的基本概念和相关术语,整体上对Docker的结构和功能有了一个大概的认识,接下来的事就是使用Docker进行简单的实际操作了。我们知道Docker的镜像是由多个只读层堆叠起来的,当我们启动一个容器时,在这些只读层的顶部会加载一个读写层,可以供容器写入数据。但是当容器销毁时,这个读写层也会一起销毁,所以想要在容器中实现数据的持久化就需要使用到其他手段。
容器中的数据持久化
在Docker中,有两种方式可将文件保存到主机中,换言之即使容器停用后文件依然存在,这两者分别是 volume (卷)和 Bind mounts (绑定挂载)。如果你的Docker运行在Linux系统下,还可以使用 tmpfs mount 将数据写入宿主机器的内存中,若你的Docker运行在Windows系统下你也可以选择使用命名管道,这两个方法在这里就不做介绍了。
关于Bind mounts方法,是将宿主机器上的一个路径(或文件)和容器中的一个路径(或文件)建立联接,可以简单理解为在建立了联结后,容器中对这个路径下的所有读写操作就是映射在宿主机器上的关联的路径中。在Windows系统中,与容器相关联的路径可以是Windows系统的路径也可以是运行Docker守护进程的Linux虚拟机上的路径。这种方式语义直观简单,并且性能不差,但是迁移的兼容性差。
关于Volume方法,我们只需要指定容器中某个路径作为挂载点(mount point),而与之相关联的宿主机器上的路径是由docker自行创建与管理的,并与卷建立存储关系。存储卷的默认路径是“/var/lib/docker/volumes/”。这种方式解耦了与容器挂载点相关联的宿主机器的路径,用户无需指定使用哪个宿主路径,全由Docker管理。从官方文档的口气来看貌似还是更推崇使用Volume。
上图概括的描述了在Windows系统中使用Volume和Bind mount与宿主机文件系统的对应关系,当然在Linux系统下由于Docker守护进程直接寄宿在主机下,省略了虚拟机的环节是对应关系更加简洁。接下来分别介绍Volume和Bind mounts的使用。
Volume的使用
1. 创建和管理“卷”
创建一个“卷”的命令是使用 docker volume create %卷的名称% 的写法,如
> docker volume create my-first-volume
列出Docker中所有的卷使用 docker volume ls 命令,如
> docker volume ls DRIVER VOLUME NAME local my-first-volume
接着可以去查看下某个卷的具体信息,使用 docker volume inspect %卷名% 命令即可,得到Json格式的卷的信息
> docker volume inspect my-first-volume [ { "CreatedAt": "2020-05-12T08:46:10Z", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/my-first-volume/_data", "Name": "my-first-volume", "Options": {}, "Scope": "local" } ]
注意看下返回的数据中, Mountpoint 这一项表示的是刚刚创建的卷所在的位置。由于本次实验是在Windows系统中进行的,此路径并非是Windows机上的某一路径,而是Docker守护进程所在的Linux虚拟机上的路径,我们可以做如下的验证。
可以使用如下的命令依次执行可以进入承载Docker守护进程的Linux虚拟机。
获取可访问Docker守护程序的容器 |
docker run --privileged -it -v /var/run/docker.sock:/var/run/docker.sock jongallant/ubuntu-docker-client |
运行具有完全root访问权限的容器 |
docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine /bin/sh |
切换到主机文件系统 | chroot /host |
进入虚拟机之后,我们按照Mountpoint显示的路径进入,便可以看到刚刚创建的卷“my-first-volume”。可以看出这个卷其实在这里也是一个路径,里面还存在一个_data的子路径。
# cd /var/lib/docker/volumes/ /var/lib/docker/volumes # ls -l total 28 -rw------- 1 root root 32768 May 12 08:46 metadata.db drwxr-xr-x 3 root root 4096 May 12 08:46 my-first-volume
最后提一下,删除卷的命令 docker volume rm %卷名% 。
2. 使用卷
在运行容器时,若要指定使用卷就需要用到 -v 或 --volume 标识来指定。之前的Docker版本中,针对单个独立的容器一般使用 -v 或者 --volume ,针对集群(Swarm)服务使用 --mount 。然后自动Docker 17.06版本之后, --mount 也可以使用在单个独立的容器上,而且 --mount 参数会更加的明确和详细。
语法:
(1)使用 -v 或者 --volume ,会跟3个可选参数,注意参数的顺序是不能改变,3个参数使用 “:” 分割。
- 在使用“命名卷”的情形下,第一个参数就是指这个卷的名字,并且这个名字在宿主机上是唯一的。而针对“匿名卷”来说,第一个参数便是省略的;
- 第二个参数是一个路径,这个路径指定的是卷挂载在容器中的路径;
- 第三个参数是可选参数,并且是一个用逗号分隔的参数集合,来指定卷的一些属性,比如ro(read-only)表示只读
(2)使用 --mount ,后跟的参数是用“键值对”的形式来指定,键值对之间用逗号分隔,语法相对 -v 的语法会复杂一些,但是摆脱了参数顺序的限制,参数的语义更加容易理解。
键 | 值 |
type |
指定挂载的类型,可选值为 volume 、 bind 或 tmpfs , 由于这里是使用卷,那么就应该选择 volume |
source (或src) |
指定挂载的源,当使用“volume”模式时,指定卷的名称。 若是“匿名卷”的情形,这个参数是可以省略的 |
destination (或dst、target) | 指定source指定的卷挂载到容器中的哪个路径或则文件上 |
readonly | 如果标记了这个参数,那么卷便是只读的 |
volume-%option% | 还可以指定更多的属性,后面用到再说 |
接下来创建一个使用卷的容器。首先使用 -v 的语法来创建。简单起见我们就直接使用Ubuntu这个容器。
> docker run -it --name=TestContainer01 -v my-first-volume:/usr/data ubuntu
我们将卷“my-first-volume”挂载到路径“/usr/data”上,如果容器中不存在这个路径会自动创建,如果容器中存在这个路径并且这个路径中存在子路径和文件,那么这些路径和文件会被复制到卷中。接下来在容器中打开“/usr/data”路径并在其中手动创建一个路径,并添加一个txt文件。
root@20fe562e185a:/# cd usr/data root@20fe562e185a:/usr/data# ls root@20fe562e185a:/usr/data# mkdir test_volume01 root@20fe562e185a:/usr/data# cd test_volume01/ root@20fe562e185a:/usr/data/test_volume01# touch test1.txt root@20fe562e185a:/usr/data/test_volume01# echo 3.1415926 >> "test1.txt" root@20fe562e185a:/usr/data/test_volume01# cat test1.txt 3.1415926
接下来回到Docker守护进程所在的虚拟机,检查刚刚创建的Ubuntu容器。
# docker inspect TestContainer01
在输出中可以看到这样的数据,这显示了挂载的方式是使用“volume”卷的方式,并可以看到具体的挂载路径。
接着在Docker宿主(Linux虚拟机)上查看卷内的内容,进入卷的路径下可以看到存在了刚刚新建的文件,并且文件中有相应的内容。
/ # cd /var/lib/docker/volumes/my-first-volume/_data /var/lib/docker/volumes/my-first-volume/_data # ls test_volume01 /var/lib/docker/volumes/my-first-volume/_data # cd test_volume01/ /var/lib/docker/volumes/my-first-volume/_data/test_volume01 # ls test1.txt /var/lib/docker/volumes/my-first-volume/_data/test_volume01 # cat test1.txt 3.1415926
如果在创建容器时,没有给定“卷”,只指定了卷要挂载的路径,那么Docker会帮我们自动创建一个匿名卷,并将其挂载到我们指定的路径上。如下面这个命令
> docker run -t -i --rm -v /usr/data ubuntu
我们检视刚刚创建的这个容器,可以发现Docker为我们创建了一个匿名卷。
来到DockerDocker宿主(Linux虚拟机)上查看卷的目录同样也可以看到出现了一个新的卷。
/var/lib/docker/volumes # ls -l total 32 drwxr-xr-x 3 root root 4096 May 12 11:07 424db435375a6e9c590f7ae3828abb0de75b9ed704c13b078b5820f6fbccb2b6 -rw------- 1 root root 32768 May 12 11:07 metadata.db drwxr-xr-x 3 root root 4096 May 12 08:46 my-first-volume
接下来使用 --mount 的语法来指定卷,注意target前的逗号之前不能有空格。
> docker run -it --rm --name=TestContainer02 | --mount source=my-first-volume,target=/usr/data | ubuntu
进入容器后我们去挂载卷“my-first-volume”的路径下,仍然可以看到我们在容器TestContainer01中创建的路径test_volume01和1.txt文件,这说明卷中的文件得到了持久化,也说明卷是可以跨容器访问的。
root@c95b048d05c4:/# cd usr/data root@c95b048d05c4:/usr/data# ls test_volume01 root@c95b048d05c4:/usr/data# cd test_volume01/ root@c95b048d05c4:/usr/data/test_volume01# ls test1.txt root@c95b048d05c4:/usr/data/test_volume01# cat test1.txt 3.1415926
3. 容器间共享卷
当多个容器都想使用同一个或多个卷的情形下,除了显式指定卷的名称外,还可以使用 --volumes-from %容器% 的方法将某个容器中的卷挂载到新建的容器中。有些情形下的做法是建立一个只用来存放共用卷的容器,称之为数据卷容器(参考这里)。其他容器通过引用数据卷容器中的卷来将这些卷挂载到自己路径下。
创建一个用来声明数据卷的数据卷容器,如下,为这个容器指定了两个卷。
> docker run --name volumeContainer -d -v /var/app/config -v /var/app/resource ubuntu
创建一个新的容器直接挂载”volumeContainer“中的卷。
> docker run -t -i --rm --volumes-from volumeContainer --name MyContainer ubuntu
root@0127778fb068:/# ls /var/app/config root@0127778fb068:/# ls /var/app/resource root@0127778fb068:/# echo "data from MyContainer" > /var/app/resource/1.txt
可以看到MyContainer容器中看到这两个卷,并向其中一个卷中写入了一份数据。随后再创建第三个容器去从数据卷容器中去挂载这两个卷(当然也可以通过容器MyContainer来挂载)。
docker run -t -i --rm --volumes-from volumeContainer --name xuhyContainer ubuntu root@e841004ea40e:/# ls /var/app/resource 1.txt root@e841004ea40e:/# cat /var/app/resource/1.txt data from MyContainer
可以看到这两个卷,并看到在第二个容器MyContainer容器中所作的修改。
Bind Mount的使用
在使用Bind Mounts方式时,容器中需要挂载的是宿主机器上的路径或者文件,这些路径或者文件在主机上需要事先存在,这种方式有点类似于软链接。Bind Mount方式的性能是很好的,但是这种方式依赖宿主机器特定的文件目录结构,在容器进行迁移时,特别时不同操作系统迁移时就会出现问题了,因为Windows操作系统的路径和Linux的路径肯定时截然不同的。
1.语法
与卷一样,bind mounts中也是通过 -v 或者 --mount 来做参数标识的。
(1)使用 -v 或者 --volume 时同样会跟3个可选参数,注意参数的顺序是不能改变,3个参数使用 “:” 分割。
- 第一个参数是宿主机器上的路径或者文件
- 第二个参数是在容器中要挂载的路径或者文件
- 第三个是可选参数集合,使用英文逗号分隔,和使用卷时类似
可以看到在使用 -v 时用法和卷是很类似的,区别在于第一个参数指定的是本机的路径。
(2)使用 --mount 时参数依然是使用键值对的模式
键 | 值 |
type | 指定挂载的类型,这里介绍使用bind mounts,type参数给定 bind 即可 |
source(或src) | Docker宿主机器的路径或者文件 |
target(或destination,dst) | 与容器中关联的路径或者文件 |
readonly | 可选参数,如果标记了这个参数,那么容器对挂载的路径是只读的权限 |
bind-propagation | 可选参数,设定绑定传播的属性,不常用参数,可参考这篇文档或这篇文档 |
使用 -v 和使用 --mount 是有些区别的,当使用 -v 来bind一个路径或者文件时,如果路径或文件在宿主机器上不存在,会在主机上自动创建这个路径。但是当我们使用 --mount 时如果路径不存在便会报错。
2.使用bind mount
我们先使用Windows系统的目录进行绑定,这里使用的D盘下的dockerTest目录挂载到容器中的“/usr/data"目录。注意一点,这里Windows路径的写法不能使用我们在windows系统下常用的”D:\dockerTest“写法,因为会把路径中的冒号当成参数的分隔符了。
> docker run -t -i --name TestContainer03 -v/d/dockerTest:/usr/data --rm ubuntu
当执行上面命令后Docker会弹出一个提示对话框询问共享”D:\dockerTest“目录的权限,点击”Share it“后路径便绑定完成了。
在容器中打开挂载的路径,可以看到原先存在于”D:\dockerTest“中的目录和文件都可以访问得到。
使用Docker的检视命令inspect下这个容器,可以看到挂载的方式时bind,并且可看到源路径和目标路径。
这时我们打开Docker的设置面板,在”File sharing“项中也可以看到当前可以使用bind mount与容器关联的路径。其实现在你如果进入承载Docker的Linux虚拟机中,也可以看到在虚拟机的根目录下多了一个”D:“的目录。
接下来试着使用容器在挂载的路径下写入数据,如下图所示,写入完成后回到Windows下查看文件得到更改,关云长得到了赤兔马(手动滑稽~)。
以上使用了Windows系统上的路径进行绑定,如果绑定的路径是Linux虚拟机中的路径结果也是一样的,并且Docker不会再询问Share的权限了。
> docker run -t -i --name TestContainer04 -v/var/app:/usr/data --rm ubuntu
使用 --mount 来绑定目录的话需要注意输入的主机上的路径必须是已存在的,否则会报错。使用 --mount 后赋值参数的语义更简明,官文中也是更推荐使用。
> docker run -t -i --rm --mount type=bind,source="${pwd}",target=/usr/data --name TestContainer05 ubuntu root@8f66285f7e64:/# cd usr/data/5-general root@8f66285f7e64:/usr/data/5-general# ls GuanYunChang HuangHanSheng MaMengQi ZhangYiDe ZhaoZiLong
参考文献:
[1] 官文:https://docs.docker.com/storage/volumes/
[2] 官文:https://docs.docker.com/storage/bind-mounts/
[3] 官文:https://docs.docker.com/storage/
[4] 博客:https://www.cnblogs.com/along21/p/10237219.html
[5] 博客:https://www.cnblogs.com/kevingrace/p/6238195.html
[6] 访问Windows上的docker虚拟机:https://blog.jongallant.com/2017/11/ssh-into-docker-vm-windows/