引言
Docker以插件的方式支持多种存储驱动,1.12.6版本的Docker支持的存储驱动包括overlay、aufs、btrfs、devicemapper、vfs、zfs。文章针对Docker1.12.6的版本对devicemapper存储驱动做了一些研究工作。
回环设备
回环设备的本质是将普通文件虚拟成块设备来用,因此对回环设备的读写操作就被重定向到了普通文件的操作。回环设备通常以/dev/loop0、/dev/loop1的样式命名。操作回环设备常用的命令为dmsetup,man dmsetup中的示例如下:
dd if=/dev/zero of=~/file.img bs=1MiB count=10
losetup --find --show ~/file.img /dev/loop0
mkfs -t ext2 /dev/loop0
mount /dev/loop0 /mnt
...
umount /dev/loop0
losetup --detach /dev/loop0
Device Mapper
Device Mapper是Linux2.6内核中支持的逻辑卷管理的通用设备映射机制,主要由内核的device mapper驱动与用户空间的device mapper库以及其提供的dmsetup工具两部分组成。Device Mapper框架的结构图如下:Device Mapper包含三个重要的对象概念:
- mapped device(一个逻辑对象,可以理解为内核向外提供的逻辑设备,它通过映射表描述的映射关系与target device建立映射)
- 映射表(由一个多元组表示,该多元组由表示mapped device逻辑的起始地址、范围和表示在target device所在物理设备的地址偏移量以及target类型等变量组成),比如:“0 1028160 linear /dev/hda 0”
- target device(mapped device所映射的物理空间段,可以为真正的物理设备,也可以为逻辑设备)
一个mapped device可以映射到一个或者多个target device上,而一个mapped device又可以作为它上层mapped device的target device被使用,该层次结构在理论上可以无线迭代。Device Mapper内核中对象的层次关系图如下:
Device Mapper这部分详细介绍可以参考https://www.ibm.com/developerworks/cn/linux/l-devmapper/,文章写的特别详细。
Thin-provisioning Snapshot
Thin-provisioning Snapshot结合Thin-Provisioning和Snapshot两种技术的亮点主要有
- 允许许多虚拟设备存储在同一个数据卷上,这简化了管理和允许在卷之间共享数据,从而减少磁盘使用量
- 支持任意深度的递归快照
这部分知识可以参考https://github.com/torvalds/linux/blob/master/Documentation/device-mapper/thin-provisioning.txt、http://www.infoq.com/cn/articles/analysis-of-docker-file-system-aufs-and-devicemapper/、https://coolshell.cn/articles/17200.html。这些文档会有比较详细的说明。
Thin-Provisioning Snapshot创建流程讲解(参考内核文档)
- 创建稀疏文件并将其虚拟成回环设备
dd if=/dev/zero of=/opt/metadata bs=1 count=0 seek=2G
dd if=/dev/zero of=/opt/data bs=1 count=0 seek=100G
losetup /dev/loop0 /opt/metadata
losetup /dev/loop1 /opt/data - 创建Pool device
dmsetup create pool --table "0 20971520 thin-pool $metadata_dev $data_dev $data_block_size $low_water_mark"
- 0表示起始的扇区(sector)的位置
- 20971520表示扇区的个数,一个sector为512Byte
- thin-pool是关键字,映射表的类型
- metadata_dev为metadata volume设备
- data_dev为data volume设备
- data_block_size为最小的磁盘空间单位,该值必须介于128(64KB)和2097152(1GB)之间。当thin-pool被创建后,该值无法更改。当要做很多快照时可能需要较小的值,如128(64KB)
- low_water_mark为最少可用sector的water mark,是一个threshold
- 创建thinly-provisioned volume
1)要创建一个新的thinly-provisioned卷,必须首先发送消息给active pool device(这里以/dev/mapper/pool为例)
dmsetup message /dev/mapper/pool 0 "create_thin 0"
create_thin是关键字
双引号中的0表示这个volume的ID
2)创建thinly-provisioned卷thin
dmsetup create thin --table "0 2097152 thin dev/mapper/pool 0"
双引号中的thin是关键字
双引号中尾部的0是/dev/mapper/pool设备的起始扇区位置 - 创建快照(这里以创建internal snapshot为例)
1)暂停origin device设备(如果想要在上面创建快照的origin device处于活动状态,则必须在创建快照之前将origin device暂停以避免将其损坏),然后恢复origin device设备
dmsetup suspend /dev/mapper/thin
dmsetup message /dev/mapper/pool 0 "create_snap 1 0"
dmsetup resume /dev/mapper/thin
create_snap是关键字
双引号中的1为指定的新的设备ID
双引号中的0是origin device 的ID号(这里是/dev/mapper/thin的ID)
2)创建快照
dmsetup create snap --table "0 2097152 thin /dev/mapper/pool 1"
至此,一个thin-provisioned snapshot就创建成功了。
实践
干净的centos7.2机器安装docker1.12.6前后宿主机上回环设备与device mapper的变化对比
- 安装docker之前
- docker安装成功后
docker的mapped device(逻辑设备,这里是docker-253:0-2001-pool)与target device(物理设备)之间的映射表信息解读如下:
1: docker创建的mapped device的名称为docker-253:0-2001-pool
2:从逻辑设备的0号扇区位置开始
3:共209715200个扇区,即逻辑设备0~209715199扇区
4:thin-pool类型的映射表,thin-pool支持设备精简配置,并提供更好的快照支持
5:表示回环设备/dev/loop0
6:表示回还设备/dev/loop1
7:表示最小的可分配的扇区数
8:表示空闲空间的阈值
9:表示特征参数的个数,这里指定一个特征参数
10:特征参数,表示略过用0填充的块
上述实践过程体现了docker创建thin pool的实现细节,首先创建两个稀疏文件,然后将稀疏文件虚拟成回环设备,最后联合两个稀疏文件并在其上创建docker-pool虚拟设备
研究一下docker的pool虚拟设备
- 查看pool设备的文件系统类型
- 对pool设备重新分区
- 创建ext3文件系统、挂载目录、写入文件
可知:
1.docker创建的虚拟设备docker pool是正常的块设备
2.docker pool虚拟设备的100G空间不是真实的,真正可用的大小取决于底层稀疏文件所在文件系统的大小
dmsetup命令使用
结合https://github.com/torvalds/linux/blob/master/Documentation/device-mapper/thin-provisioning.txt内核文档的说明与dmsetup命令的帮助信息(man dmsetup),能够掌握dmsetup命令的一些基本用法,比如:
创建设备:dmsetup create mydevice --table "0 209715200 linear /dev/sdc1 2048"
查看设备的依赖设备:dmsetup deps mydevice
查看设备信息:dmsetup info mydevice
列出所有设备:dmsetup ls
查看设备映射表信息:dmsetup table mydevice
发送设备给目标设备:dmsetup message $device_name $sector $message
删除设备:dmsetup remove $device_name
重置设备:dmsetup resume $device_name
暂停设备:dmsetup suspend $device_name
查看设备状态:dmsetup status mydevice
docker官方文档对devicemapper的讲解
Device Mapper是基于内核的框架,支持Linux上的许多高级卷管理技术。 Docker的devicemapper存储驱动程序利用此框架的精简配置(thin-provisioning)和快照(snapshot)功能进行镜像和容器管理。
devicemapper驱动程序使用专用于Docker的块设备,并在块级而不是文件级运行。 这些设备可以通过将物理存储添加到Docker主机来扩展,并且比在操作系统级别使用文件系统更好。
Docker将镜像和层内容存储在精简池(thin pool)中,并通过将它们挂载到 /var/lib/docker/devicemapper/的子目录(容器在创建时会在mnt目录下创建挂载点)下将其暴露到容器中。
/var/lib/docker/devicemapper/metadata目录包含有关Devicemapper配置本身的元数据,以及每个存在的镜像和容器层的元数据,这些元数据同时包含有关元数据的信息。比如:/var/lib/devicemapper/mnt/目录为每个镜像和容器层存放一个挂载点。镜像层挂载点是空的,但容器的挂载点显示容器的文件系统。
{自己对文档中这句话的理解:容器有挂载点这个比较容易理解,镜像层挂载点是空的这句话需要好好思考一下。Docker镜像是分层存储的,上一层是下一层的快照(使用的就是devicemapper的快照技术)。镜像每一层本质上是对应着一个块设备,那镜像层中的数据又是如何生成的呢?Docker首先做的是创建这个镜像层对应的块设备,然后通过对这个块设备挂载目录来组织该镜像层的数据,数据组织完整后卸载挂载点,同时采用一种处理机制(现在没有研究清楚)将镜像层对应的块设备给隐藏掉,使得操作系统只能看到容器的块设备,而看不到每个镜像层对应的块设备。
}
devicemapper存储驱动使用专用的块设备而不是格式化的文件系统,并在块级别上对文件进行操作,以便在写入时复制(CoW)操作期间实现最佳性能。
devicemapper的另一个功能是使用了快照(有时也称为精简设备或虚拟设备), 快照提供了许多好处:
- 容器之间共享的镜像层只存储在磁盘上一次
- 快照实现了写时复制(CoW)的策略
- 由于devicemapper在块级别上运行,因此可以同时修改可写层中的多个块
- 快照可以使用标准的操作系统级备份工具进行备份
容器中读数据操作:
应用程序向容器中的块0x44f发出读取请求。 由于容器是一个镜像的快照,其本身是没有该块的(读取的数据块实际都存储在镜像层中),通过指针从上往下逐层对镜像层进行寻找,找到包含该块的最近镜像层后将块上的数据读入容器的内存中。
容器中写文件操作:
devicemapper通过按需分配的操作首先将thin pool中新块(大小为64KB,128的扇区)分配到容器的读写层,然后在向块中写入数据。
容器中更改文件操作:
从镜像层中将想要更改文件的内容对应的块复制到容器的读写层中,然后执行更改数据操作。
容器中删除文件操作:
删除容器可读写层中的文件或目录 或者 镜像层删除其父层中存在的文件时,devicemapper存储驱动程序将截获对该文件或目录的进一步读取尝试,并响应该文件或目录不存在。
容器中写入然后删除文件操作:
写入与删除文件的操作均在容器的读写层中执行,在这种情况下,如果使用direct-lvm,块将被释放。如果使用loop-lvm,块可能不会被释放。这是在生产环境中不使用loop-lvm的另一个原因。
写时复制性能影响:容器首次修改特定块时,该块将被写入容器的可写层。由于这些写入是在块的级别而不是文件级别发生的,因此性能影响比较小。但是,编写大量数据块仍然会对性能造成负面影响,而devicemapper驱动实际上可能在此情况下性能比其他存储驱动差。对于写入繁重的工作负载,应使用完全绕过存储驱动程序的数据卷。
devicemapper驱动程序将每个镜像和容器存储在自己的虚拟设备上。 这些设备是精简配置的写时复制快照设备。 Device Mapper技术在块级而不是文件级工作。 这意味着devicemapper存储驱动程序的自动精简配置和写时复制操作可以使用块而不是整个文件。
实践
以nginx:1.12-alpine镜像为例,研究Docker存储中镜像层与容器分别对应的虚拟设备的情况
基于不同的镜像层创建快照,查看其内容
1.每个镜像层是一个虚拟设备
2.每个镜像层的内容是有差异的
3.同一镜像的镜像层创建的快照只允许挂载一个
容器的快照信息
对最上层的镜像层3a8be9aa99e1d8b22e541e81595edd82bc9f4c8124c80086a89a8ba8c6c00482打快照,然后与容器的内容进行对比
两者之间的差距
对容器创建快照
验证问题
根据上图呈现的结构验证两个问题:
1.验证docker文档中这一段描述:The base device is the lowest-level object. This is the thin pool itself,即图中的Base device与Pool为同一个设备
2.验证/var/lib/docker/devicemapper/metadata/base 这个文件中记录的是图中Base device设备的信息,{"device_id":1,"size":10737418240,"transaction_id":1,"initialized":true,"deleted":false}
问题一验证
成功安装docker后,fdisk -l信息如下:
查看/dev/mapper/docker-253:0-2424-pool设备上的文件系统类型
为/dev/mapper/docker-253:0-2424-pool设备挂载目录
问题二验证
对Base device设备上存放数据
通过base文件标识的设备号,对该设备打快照
device id等于1的设备已经创建好快照,对快照挂载目录,检查快照上的文件信息
Docker源码查看(moby-1.12.6/pkg/devicemapper/devmapper.go)
Docker本地存储结构
docker本地存储结构大概为:疑问点
Docker镜像的任一镜像层与镜像产生的容器本质上都是一个虚拟设备,按照当前对块设备的理解,当在操作系统中输入fdisk -l命令应该能够看到这些虚拟设备,但现在的情况是只能看到容器对应的虚拟设备,而看不到镜像层对应的虚拟设备,这一点不知道docker是怎么实现的?
结束语
上面内容是对docker的devicemapper存储驱动研究的一个记录,了解到docker引擎首先会创建两个稀疏文件并分别虚拟成回环设备,然后基于两个回环设备创建虚拟设备docker pool,最后docker的镜像层以及容器均是对docker pool上的thin volume打快照所得。由于对go语言不是很了解,因此有些点不能通过扒源码的方式得到最直接的结论,对于上面探索的内容以及疑问点还希望与大家一起交流、学习、实践、验证。
引用
- https://www.ibm.com/developerworks/cn/linux/l-devmapper/
- https://github.com/torvalds/linux/blob/master/Documentation/device-mapper/thin-provisioning.txt
- http://www.infoq.com/cn/articles/analysis-of-docker-file-system-aufs-and-devicemapper/
- https://coolshell.cn/articles/17200.html
- https://github.com/torvalds/linux/blob/master/Documentation/device-mapper/thin-provisioning.txt
- https://docs.docker.com/v1.12/