Docker(三)数据管理
一、Docker存储驱动
官网文档:https://docs.docker.com/engine/admin/volumes/
为了支持(镜像分层与写时复制机制)这些特性,Docker提供了存储驱动的接口。存储驱动根据操作系统底层的支持提供了针对某种文件系统的初始化操作以及对镜像层的增、删、改、查和差异比较操作。目前存储系统的接口已经有aufs、btrfs、device mapper、vfs、overlay这5种具体实现,其中vfs不支持写时复制,是为使用volume(Docker提供的文件管理方式)提供的存储驱动,仅仅做了简单的文件挂载操作;剩下4种存储驱动支持写时复制,它们的实现由一定的相似之处。
1.1 存储驱动的功能与管理
Docker中管理文件系统的驱动为graphdriver。其中定义了统一的接口对不同的文件系统进行管理,在Docker daemon启动时就会根据不同的文件系统选择合适的驱动。
存储驱动接口定义
GraphDriver中主要定义了Driver和ProtoDriver两个接口,所有的存储驱动通过实现Driver接口提供相应的功能,而ProtoDriver接口则负责定义其中的基本功能。这些基本功能包括如下8种:
String() :返回一个代表这个驱动的字符串,通常是这个驱动的名字
Create() : 创建一个新的镜像层,需要创建一个唯一的ID和所需的父镜像的ID。
Remove() : 尝试根据指定的ID删除一个层。
Get() : 返回指定ID的层的挂载点的绝对路径。
Put() : 释放一个层使用的资源,比如卸载一个已经挂载的层。
Exists() : 查询指定的ID对应的层是否存在。
Status() : 返回这个驱动的状态,这个状态用一些键值对表示。
Cleanup() : 释放由这个驱动管理的所有资源,比如卸载所有的层。
而正常的Driver接口实现则通过包含一个ProtoDriver的匿名对象实现上述8个基本功能,除此之外,Driver还定义了其他4个内部方法用于对数据层之间的差异(diff)进行管理,包括比较某个镜像和父镜像的差异,应用某个差异的内容等。
GraphDriver还提供了naiveDiffDriver结构,这个结构就包含了一个ProtoDriver对象并实现了Driver接口中与差异有关的方法,可以看做Driver接口的一个实现。
存储驱动的创建过程
首先,前面提到的各类存储驱动都需要定义一个属于自己的初始化过程,并且在初始化过程中向GraphDriver注册自己。GraphDriver维护了一个driver列表,提供从驱动名到驱动初始化方法的映射,这用于将来根据驱动名称查找对应驱动的初始化方法。而所谓的注册过程,则是存储驱动通过调用GraphDriver提供自己的名字和对应的初始化函数,这样GraphDriver就可以将驱动名和这个初始化方法保存到drivers。
当需要创建一个对应的驱动时(比如aufs的driver),GraphDriver会根据名字从driver中查找到这个驱动对应的初始化方法,然后调用这个初始化函数得到对应的Driver对象。创建过程如下所示:
依次检查环境变量DOCKER_DRIVER和变量DefaultDriver是否提供了合法的驱动名字(比如aufs),其中DefaultDriver是从Docker daemon启动时的-storage-driver配置中读出的。获知了驱动名称后,GraphDriver就调用对应的初始化方法创建一个对应的Driver对象实体。
若环境变量和配置默认是空的,则GraphDriver会从驱动的优先级列表中查找一个可用的驱动。“可用”包含两个意思:第一,这个驱动曾经注册过自己;第二,这个驱动对应的文件系统被操作系统底层支持(这个支持性检查会在该驱动的初始化过程中执行)。目前优先级列表依次包含了这些驱动:aufs、btrfs、drive mapper、vfs和overlay。
如果在上述5种驱动中查找不到可用的,则GraphDriver会查找所用注册过的驱动,找到第一个注册过的、可用的驱动并返回。不过这一设计只是为了将来的可扩展性而存在,因为现在有且仅由的上述5种驱动一定会注册自己的。
1.2 aufs与Device Mapper驱动
aufs
aufs(AnotherUnionFS)是一种支持联合挂载的文件系统,简单说就是支持将不同目录挂载到同一个目录下,这些挂载操作对用户来说是透明的,用户在操作该目录时并不会觉得与其他目录又什么不同。这些目录的挂载是分层次的,通过来说最上层是可读写层,下层是只读层。所以,aufs的每一层都是一个普通文件系统。当需要读取一个文件A时,会从最顶层的读写层开始向下寻找,本层没有,则根据层之间的关系到下一层开始找,直到找到第一个文件A并打开它。当需要写入一个文件A时,如果这个文件不存在,则在读写层新建一个;否则像上面的过程一样从顶层开始查找,直到找到最近的文件A,aufs会把这个文件复制到读写层进行修改。
由此可以看出,在第一次修改某个已有文件时,如果这个文件很大,即使只要修改几个字节,也会产生巨大的磁盘开销。
当需要删除一个文件时候,如果这个文件仅仅存在于读写层中,则可以直接删除这个文件;否则就需要先删除它在读写层中的备份,再在读写层中创建一个whiteout文件来标志这个文件不存在,而不是真正删除底层的文件。
当新建一个文件时,如果这个文件在读写层存在对应的whitout文件,则先将whitout文件删除再新建。否则直接再读写层新建即可。
Device Mapper
Device Mapper是一个基于kernel的框架,它增强了很多Linux上的高级卷管理技术。Docker的devicemapper驱动在镜像和容器管理上,利用了该框架的超配和快照功能。Docker最初运行在Ubuntu和Devian上,并且使用AUFS作为存储后端。当Docker变得流行后,很多想使用它的公司正在使用RHEL。不幸的是,因为Linux主线kernel并不包含AUFS,所以RHEL并没有支持AUFS。为了改变这种情况,Red Hat开发者研究将AUFS包含进kernel主线。最后,他们认为开发一种新的存储后端是更好的主意。此外,他们打算使用已经存在的Device Mapper技术作为新存储后端的基础。Red Hat与Docker公司合作贡献这个新驱动。因为这次合作,Docker Engine被重新设计为存储驱动插件化。因此devicemapper成为Docker支持的第二个存储驱动。Device Mapper在2.6.9之后就被合入Linux kernel主线,也是RHEL家族发布包的核心部分。这意味着devicemapper存储驱动基于稳定的代码,有着大量的工作产品和极强的社区支持。
Device Mapper包括3个概念:映射设备、映射表和目标设备。如下图:
#如上图所示,映射设备是内核向外提供的逻辑设备。一个映射设备通过一个映射表与多个目标设备映射起来,映射表包含了多个多元组,每个多元组记录了这个映射设备的起始地址、范围与一个目标设备的地址偏移量的映射关系。目标设备可以是一个物理设备,也可以是一个映射设备,这个映射设备可以继续向下迭代。一个映射设备最终通过一颗映射树映射到物理设备上。Device Mapper本质功能就是根据映射关系描述IO处理规则,当映射设备接收到IO请求的时候,这个IO请求会根据映射表逐级转发,直到这个请求最终传到最底层的物理设备上。
Device Mapper存储驱动使用Device Mapper的精简配置模块实现镜像的分层,这个模块使用了两个块设备,一个用于存储数据,另一个用于存储元数据。数据区可以看做是一个资源池,为生成其他块设备提供资源,元信息存储了虚拟设备和物理设备的映射关系。Copy on Write发生再快存储级别,Device Mapper通过从已有设备创建快照的方式创建新的设备(最开始有一个基础设备,所有的设备都直接或间接的从这个设备创建快照),这些新创建的块设备在写入内容之前并不会分配资源。所有的容器和镜像都有自己的块设备,它们在任何时候都能创建快照供新的内容或镜像使用。
Docker使用Device Mapper文件系统时,在/var/lib/docker/devicemapper目录下有三个子文件件,其中mnt为设备挂载目录,devicemapper下存储了具体的文件内容(它作为一个资源池存在),metadata下存储了每个块设备的信息。
#从上图可以看出/var/lib/docker/devicemapper/devicemapper/data是一个100G的文件,它包含了所有镜像和容器的实际文件内容。每个容器被限制在10G大小的卷内,可以调整,但是调整时会删除所有容器和镜像。
#从上图可以看到实际占用不到1G,当再次pull新的镜像或者启动容器在其增加文件时,也只增加了data文件的大小,其他文件并没有变化。
二、Docker数据卷的使用
容器中管理数据主要有两种方式:数据卷(Data Volumes),数据卷容器(Data Volume Dontainers)。
Docker的镜像是由一系列的只读层组合而来的,当启动一个容器时,Docker加载镜像的所有只读层,并在最上层加入一个读写层。这个设计使得Docker可以提高镜像构建、存储和分发的效率,节省了时间和存储空间,然而也存在如下问题:
容器中的文件在宿主机上存在形式复杂,不能再宿主机上很方便地对容器中的文件进行访问。 多个容器之间的数据无法共享。 当删除容器时,容器产生的数据将丢失。
为了解决上面的是哪个问题,Docker引入了数据卷(volume)机制。volume是存在于一个或多个容器中的特定文件或文件夹,这个目录能够以独立于联合文件系统的形式再宿主机中存在,并为数据的共享与持久化提供以下便利
volume在容器创建时就会初始化,在容器运行时就可以使用其中的文件
volume能在不同的容器之间共享和重用
对volume中数据的操作会马上失效
对volume中数据的操作不会影响到镜像本身。
volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何容器使用volume也不会被Docker删除。
2.1 从容器挂载olume
在用docker run命令的时候,使用-v 标记可以在容器内创建一个数据卷。多次使用-v标记可以创建多个数据卷。
# docker run -it -P --name vol_no1 -v /opt/data01 centos /bin/bash
#这条命令在创建容器会将容器中的/opt/data01作为一个volume挂载点。volume的路径必须写绝对路径。如果镜像中不存在/opt/data01文件夹,容器启动后就会创建一个名为/opt/data01的空文件夹;反之,如果景象中存在/opt/data01文件夹,这个文件夹中的内容将全部被复制到宿主机对应的文件夹中,并且根据容器中的文件为宿主机中的文件设置合适的权限和所有者。
# docker inspect --format={{.Config.Volumes}} vol_no1 #从下面的结果可以看到vol_no1容器的挂载目录是/opt/data01
map[/opt/data01:{}]
# docker inspect --format={{.Mounts}} vol_no1 #可以查看vol_no1容器挂载的详细信息
[{22b07e24feb9b753feed9d2fd67e5449876abf41d7285447d2d50d8ee7fefa71 /var/lib/docker/volumes/22b07e24feb9b753feed9d2fd67e5449876abf41d7285447d2d50d8ee7fefa71/_data /opt/data01 local true }]
#从上面的结果可以看到volume ID,以及volume ID的绝对路径,以及容器的挂载点目录,是本机挂载的形式,是挂载可读写状态。
# cd /var/lib/docker/volumes/22b07e24feb9b753feed9d2fd67e5449876abf41d7285447d2d50d8ee7fefa71/_data #进入到volume ID的目录
# cat /var/lib/docker/volumes/22b07e24feb9b753feed9d2fd67e5449876abf41d7285447d2d50d8ee7fefa71/_data/nihao #我们在此目录下创建了个nihao的文件,其内容为下面的内容
hahahha this is vol_no1
[root@3c6d1cd41a61 data01]# cat /opt/data01/nihao #在容器上面查看一下
hahahha this is vol_no1
#然后你再容器上面的/opt/data01目录下面创建文件修改文件,发现宿主机上面的volume ID目录下面也会随之变化。这就是挂载也就是映射。
# docker run -it --name vol_no3 -v /opt/data01 -v /opt/data02 -v /opt/data03 centos /bin/bash #可以测试挂载三个目录
#容器里面已经在/opt下面产生了三个目录。
# docker inspect --format={{.Config.Volumes}} vol_no3 #宿主机上面查看
map[/opt/data01:{} /opt/data02:{} /opt/data03:{}]
# docker inspect --format={{.Mounts}} vol_no3 #可以看到产生了三个volume ID以及目录
[{abee1b4f891df80b2ea6f96c26eefd67bfac9879a43d472d745cc97a8c425114 /var/lib/docker/volumes/abee1b4f891df80b2ea6f96c26eefd67bfac9879a43d472d745cc97a8c425114/_data /opt/data01 local true } {157f301dbafa5115e749f0a252116f6186d381356a70f933dce1ab97f989ac0d /var/lib/docker/volumes/157f301dbafa5115e749f0a252116f6186d381356a70f933dce1ab97f989ac0d/_data /opt/data02 local true } {0ec2eb2e2319b60cba3c01247cd8f62b11172ea99d4af97805c6e08369626e29 /var/lib/docker/volumes/0ec2eb2e2319b60cba3c01247cd8f62b11172ea99d4af97805c6e08369626e29/_data /opt/data03 local true }]
2.2 从宿主机挂载volume
在创建新容器的时候可以挂载一个主机上特定的目录到容器中。
# docker run -it --name vol_from_host_no1 -v /docker/data01:/opt/data01 centos /bin/bash
#商用上面的命令,将宿主机的/docker/data01文件夹作为一个volume挂载到容器中的/opt/data01。文件夹必须使用绝对路径,如果宿主机不存在/docker/data01,将创建一个空文件夹。在/docker/data01文件夹中的所有文件或文件夹可以再容器的/opt/data01文件夹下被访问。如果镜像中原本存在/opt/data01文件夹,该文件夹下原有的内容将被删除,以保持与宿主机中的文件夹一致。
#从上图可以看出,容器Docker挂载数据卷的默认权限是读写(rw),用户也可以通过,ro指定为只读。(比如我这个挂载的目录,如果让其他的docker容器只读就好了,它们不要操作这个挂载目录。)
# docker run -it --name vol_from_host_no2 -v /docker/data01:/opt/data01:ro centos /bin/bash #现在已只读的形式挂载上面的那个宿主机上面的共享挂载点/docker/data01
#虽然从目录权限上面看不出什么差别,但是当写入的时候就会报错了,当然是可以查看的,可以看到no1这个文件就是vol_from_host_no1这个容器写的内容。
#从宿主机上面查看一下,这两个容器对/docker/data01是有差别的,上面的只有ro,下面的是true也就是可读写。
# touch /docker/history/test1_history #这个test1_history文件一定要提前创建啊,不然就又成了挂载目录了。
# docker run -it --name test1 -v /docker/history/test1_history:/root/.bash_history centos /bin/bash #这样就把/docker/history/test1_history 挂载到了test1容器的/root/.bash_history
#然后再新创建的容器上面随便操作点什么然后退出。
# cat /docker/history/test1_history #这个机器用root在shell界面操作了什么,我可以看到了。
df -h history --help ls -l /data ls -l /opt/ history exit
2.3 使用Dockerfile添加volume
VOLUME /data
在使用docker build命令生成镜像并且以该镜像启动容器时会挂载一个volume到/data。如果镜像中存在/data文件夹,这个文件夹中的内容将全部被复制到宿主机中对应的文件夹中,并且根据容器中的文件设置合适的权限和所有者。
类似地,可以使用VOLUME指令添加多个volume:
VOLUME ["/data1","data2"]
#与使用docker run -v不同的是,VOLUME指令不能挂载主机中指定的文件夹。这是为了保证Dockerfile的可移植性,因为不能保证所有的宿主机都有对应的文件夹。
#需要注意的是,在Dockerfile中使用VOLUME指定之后的代码,如果尝试对这个volume镜像修改,这些修改都不会生效。这是由于Dockerfile中除了FROM指令的每一行都是基于上一行生成的临时镜像运行一个容器,执行一条指令并执行类似docker commit的命令得到一个新的镜像,docker commit命令不会对挂载的volume进行保存,VOLUME指令是在容器运行时才去挂载volume,而RUN指令在构建镜像的时候就会执行;所以上面的Dockerfile最后两行执行时,都会在一个临时的容器上挂载/data,并对这个临时的volume进行操作,但是这一行指令执行并提交后,这个临时的volume没有被保存,我们通过最后生成的镜像创建的容器所挂载的volume是没有操作的。
2.4 共享volume(数据卷容器)
在使用docker run或docker create创建新容器时,可以使用--volumes-from标签使得容器与已有的容器共享volume.
# docker run --rm -it --name test2 --volumes-from vol_from_host_no1 centos /bin/bash
#新创建的容器test2与之前创建的容器vol_from_host_no1共享volume,这个volume挂载在/opt/data01上,如果被共享的容器有多个volume,新容器也将有多个volume。
#在Docker容器退出时,默认容器内部的文件系统仍然被保留,以方便调试并保留用户数据。但是,对于foreground容器,由于其只是在开发调试过程中短期运行,其用户数据并无保留的必要,因而可以在容器启动时设置--rm选项,这样在容器退出时就能够自动清理容器内部的文件系统。显然,--rm选项不能与-d同时使用,即只能自动清理foreground容器,不能自动清理detached容器注意,--rm选项也会清理容器的匿名data volumes。所以,执行docker run命令带--rm命令选项,等价于在容器退出后,执行docker rm -v。
#也就是你exit这个容器,再次查看这个容器已经不存在了,但是它对共享卷的操作还是存在的,因为写操作已经写入到宿主机的挂载卷所在的文件了。
# docker run --rm -it --name test2 --volumes-from vol_from_host_no1 centos --volumes-from vol_no1 /bin/bash #可以多次使用--volumes-from标签与多个已有容器共享volume。
#因为我们上面vol_no1做测试的时候volume目录也挂的是/opt/data01,因为命令vol_no1在后面,所以vol_no1就把vol_from_host_no1的/opt/data01挂载给替换掉了。
#从上图可以看出vol_no1容器并没有再运行,所以一个容器挂载了volume,即使这个容器停止运行,该volume仍然存在,其他容器也可以使用--volumes-from与这个容器共享volume。如果有一些数据,比如配置文件、数据文件等,要与多个容器之间共享,一种常见的做法就是创建一个数据容器,其他容器与之共享volume。然后这个数据容器就可以停止运行避免浪费资源了。
#这种做法就叫做数据卷容器,如果删除了挂载的容器,数据卷并不会被自动删除。如果要删除一个数据卷,必须再删除最后一个还挂载着它的容器时。使用docker rm -v 命令来指定同时删除关联的容器。使用数据卷容器可以让用户在容器之间自由地升级和移动数据卷。
2.5 备份、恢复或迁移volume(利用数据卷容器迁移数据)
可以利用数据卷容器对其中的数据卷进行备份、恢复,以实现数据的迁移。volume作为数据的载体,在很多情况下需要对其中的数据进行备份、迁移,或是从已有数据恢复。以上面vol_no1为例,该容器在/opt/data01挂载了一个volume。如果需要将这里的数据备份,一个很容易想到的方法就是使用docker inspect命令查找到/opt/data01在宿主机上对应的文件夹位置,然后复制其中内容或是使用tar进行打包;同样地,如果需要恢复某个volume中的数据,可以查找到volume对应的文件夹,将数据复制进这个文件夹或是使用tar从文档文件中恢复。下面用命令来备份一下。
# docker run --rm --volumes-from vol_from_host_no2 -v /docker/backup/vol_from_host_no2:/backup --name worker_backup centos tar cvf /backup/backup.tar /opt/data01 #下面是输出结果
tar: Removing leading `/' from member names /opt/data01/ /opt/data01/no1
# 上面命令的意思是,先用--rm启动一个临时的容器worker_backup, 然后将vol_from_host_no2的volume共享给worker_backup,然后将本地的/docker/backup/vol_from_host_no2目录挂载到worker_backup的/backup目录上,然后将worker_backup容器上的/opt/data01打包到worker_backup容器的/backup/目录下包名叫做backup.tar。然后因为是临时容器,容器做完操作就消亡了。
#从宿主机本地目录查看一下,可以看到/opt/data01的目录结构和里面的内容都备份了出来。
如果要恢复数据到一个容器:
# docker run -d -it --name vol_no2_bak -v /opt/data01 centos /bin/bash #-d是放到后台运行,先创建一个新容器作为数据恢复的目录。
# docker run --rm --volumes-from vol_no2_bak -v /docker/backup/vol_from_host_no2:/backup centos tar xvf /backup/backup.tar -C /
opt/data01/ opt/data01/no1
#上面第一行先搞了个新容器作为数据恢复的目标,第二行指令启动了一个临时容器(既然是临时容器就不用指定名字了),这个容器挂载了两个volume,第一个volume与要恢复的volume共享,第二个volume将宿主机刚才的辈分目录挂载到容器的/backup下,然后将这个存放文件中的backup.tar恢复到根目录下,然后执行结束后,临时容器就消失了,恢复后的数据就在vol_no2_bak的volume中了。如下图:
#可以看到之前备份的数据已经恢复到了vol_no2_bak容器里面。
2.6 删除volume
如果创建容器时,从容器中挂载了volume,在/var/lib/docker/volumes下会生成与volume对应的目录,使用docker rm删除容器并不会删除与volume对应的目录,这些目录会占据不必要的存储空间,即使可以手动删除,因为这些目录名称是无意义的随机字符串,要知道它们是否与未被删除的容器对应也十分麻烦。所以在删除容器时需要对容器的volume妥善处理。在删除容器时,一并删除volume有以下两种方法。
第一种方法(docker rm -v):
# docker inspect --format={{.Mounts}} vol_no2_bak
# docker rm -v vol_no2_bak #-v 删除容器
#可以看到随机字符串的目录一起删除掉了。
第二种方法(docker run --rm):
# docker run --rm #在运行容器时,使用docker rm --rm,--rm标签会在容器停止时删除容器以及容器所挂载的volume。
需要注意的是,以上方法只能在对应volume是被最后一个容器使用时才会将其删除,如果容器的volume被多个容器共享,在删除最后一个共享它的容器时将其删除。
如果volume是在创建容器时从宿主机中挂载的,无论对容器进行任何操作都不会导致其在宿主机被删除,如果不需要这些文件,只能手工删除它们。