Device Mapper是一个基于内核的框架,支持Linux上的许多高级卷管理技术。Docker的devicemapper存储驱动利用这个框架的精简置备和快照功能来管理镜像和容器。本文简称Device Mapper存储驱动为devicemapper,内核框架为Device Mapper。
Docker刚开始是运行在Ubuntu和Debian Linux系统,使用AUFS作为存储后端。随着Docker变得流行,许多公司想在Red Hat Enterprise Linux上使用Docker。不过因为Linux内核上游主线没有包括AUFS,RHEL也没有用AUFS。
要解决这个问题,Red Hat开发人员着手调查让AUFS进内核主线。最终,他们决定开发一个新的存储后端。此外,它们基于现有的Device Mapper技术来开发新的存储后端。
Red Hat与Docker公司合作开发这个新的驱动。由于这次合作,Docker公司把Engine的存储后端重新设计成可插拔的。所以devicemapper成为Docker支持的第二个存储驱动。
从Linux内核版本2.6.9起,Device Mapper已经包含在Linux内核主线中。它是RHEL系列Linux发行版的核心部分。这意味着devicemapper存储驱动基于稳定的代码,具有大量实际生产环境部署和强大的社区支持。
devicemapper驱动存储每个镜像和容器到它自己的虚拟设备上。这些设备是精简置备写时拷贝快照设备。Device Mapper技术工作在块级别而不是文件级别。意味着devicemapper存储驱动的精简置备和写时拷贝操作的是块而不是整个文件。
使用devicemapper创建一个镜像的过程如下:
使用devicemapper驱动时,容器数据层是从其创建的镜像的快照。与镜像一样,容器快照是精简置备写时拷贝快照。容器快照存储着容器的所有更改。当数据写入容器时,devicemapper从存储池按需分配空间。
下图显示一个具有一个base设备和两个镜像的精简池。
如果你仔细查看图表你会发现快照一个连着一个。每一个镜像数据层是它下面数据层的一个快照。每个镜像的最底端数据层是存储池中base设备的快照。此base设备是Device Mapper的工件,而不是Docker镜像数据层。
一个容器是从其创建的镜像的一个快照。下图显示两个容器 – 一个基于Ubuntu镜像和另一个基于Busybox镜像。
我们来看下使用devicemapper存储驱动如何进行读和写。下图显示在示例容器中读取一个单独的块[0x44f]的过程。
使用devicemapper驱动,通过按需分配(allocate-on-demand)操作来实现写入新数据到容器。更新存在的数据使用写时拷贝(copy-on-write)操作。由于Device Mapper是基于块的技术,这些操作发生在块级别上。
例如,当更新容器中一个大文件的一小部分,devicemapper存储驱动不会复制整个文件。它仅复制要更改的数据块。每个数据块是64KB。
要写入56KB的新数据到容器:
首次更改已存在的数据时:
容器中的应用程序不知道这些按需分配和写时拷贝操作。不过,这些操作可能会增加应用程序的读和写操作延迟。
在一些Linux发行版本中,devicemapper是Docker的默认存储驱动。包括RHEL和它的大多数分支。目前,支持此驱动的发行版本如下:
Docker主机运行devicemapper存储驱动时,默认的配置模式为loop-lvm。此模式使用空闲的文件来构建用于镜像和容器快照的精简存储池。该模式设计为无需额外配置开箱即用(out-of-the-box)。不过生产部署不应该以loop-lvm模式运行。
你可以使用docker info命令来检查目前使用的模式:
上面的输出显示Docker主机运行的devicemapper存储驱动的模式为loop-lvm。因为Data loop file和Metadata loop file指向/var/lib/docker/devicemapper/devicemapper下的文件。这些文件是环回挂载(loopback mounted)文件。
生产部署首选配置是direct-lvm。这个模式使用块设备来创建存储池。下面展示使用配置在使用devicemapper存储驱动的Docker主机上配置使用direct-lvm模式。
下面的步骤创建一个逻辑卷,配置用作存储池的后端。我们假设你有在/dev/xvdf的充足空闲空间的块设备。也假设你的Docker daemon已停止。
你也可以在daemon.json启动配置文件设置它们,例如:
启动Docker daemon后,确保你监控存储池和卷组的可用空间。虽然卷组会自动扩展,它仍然会占满空间。要监控逻辑卷,使用lvs或lvs -a来查看数据和元数据大小。要监控卷组可用空间,使用vgs命令。
当达到阈值时,可以通过日志查看存储池的自动扩展,使用如下命令:
你可以使用lsblk命令来查看以上创建的设备文件和存储池。
下图显示由lsblk命令输出的之前镜像的详细信息。
在这个图中,存储池名称为Docker-202:1-1032-pool,包括之前创建的data和metadata设备。devicemapper构造池名称如下:
MAJ,MIN和INO指主设备号和次设备号和inode。
因为Device Mapper在块级别操作,所以更难以看到镜像层和容器之间的差异。Docker 1.10和更高版本不再将镜像层ID与/var/lib/docker中的目录名称匹配。 但是,有两个关键目录。/var/lib/docker/devicemapper/mnt目录包含镜像层和容器层的挂载点。/var/lib/docker/devicemapper/metadata目录包含每个镜像层和容器快照对应的一个文件。这个文件包含每个快照的JSON格式元数据。
你可以增加使用中的存储池的容量。如果你的数据逻辑卷已满这会有所帮助。
在这个场景下,存储池配置为使用loop-lvm模式。使用docker info查看目前的配置:
Data Space值显示存储池总共大小为100GB。此示例扩展存储池到200GB。
1.列出设备的大小。
2.扩展data文件大小为200GB。
3.验证更改的大小。
4.重载数据loop设备
5.重载devicemapper存储池。
1) 先获取存储池名称。
冒号前面部分是名称。
2) 导出device mapper表:
3) 现在计算存储池的实际总扇区。
更改表信息的第二个数字(即磁盘结束扇区),以反映磁盘中512字节扇区的新数。 例如,当新loop大小为200GB时,将第二个数字更改为419430400。
4) 使用新扇区号重新加载存储池
Docker项目的contrib目录不是核心分发的一部分。 这些工具通常很有用,但也可能已过期。这个目录的device_tool.go可以调整loop-lvm精简池的大小。
要使用该工具,首先编译它。 然后,执行以下操作调整池大小:
在此示例中,你将扩展使用direct-lvm模式设备的容量。此示例假设你使用的是/dev/sdh1磁盘分区。
1.扩展卷组(VG)vg-docker。
2.扩展数据逻辑卷(LV)vg-docker/data
3.重载devicemapper存储池。
1) 获取存储池名称。
2)导出device mapper表。
3)现在计算存储池的实际总扇区。 我们可以使用blockdev来获取数据lv的实际大小。
更改表信息的第二个数(即扇区数)以反映磁盘中512个字节扇区的新数。 例如,由于新数据lv大小为264132100096字节,请将第二个数字更改为515883008。
4)然后使用新的扇区号重新加载存储池。
了解按需分配和写时拷贝操作对整体容器性能的影响很重要。
devicemapper存储驱动通过按需分配操作给容器分配新的数据块。这意味着每次应用程序写入容器内的某处时,一个或多个空数据块从存储池中分配并映射到容器中。
所有数据块为64KB。 写小于64KB的数据仍然分配一个64KB数据块。写入超过64KB的数据分配多个64KB数据块。这可能会影响容器性能,特别是在执行大量小写的容器中。不过一旦数据块分配给容器,后续的读和写可以直接在该数据块上操作。
每当容器首次更新现有数据时,devicemapper存储驱动必须执行写时拷贝操作。这会从镜像快照复制数据到容器快照。此过程对容器性能产生显着影响。因此,更新一个1GB文件的32KB数据只复制一个64KB数据块到容器快照。这比在文件级别操作需要复制整个1GB文件到容器数据层有明显的性能优势。
不过在实践中,使用devicemapper执行大量小块写入(device mapper其它性能注意事项
还有其他一些影响devicemapper存储驱动性能的因素。
最后一点,数据卷提供了最好的和最可预测的性能。这是因为他们绕过存储驱动,并且没有精简置备和写时拷贝引入的潜在开销。