本文转载自:DOCKER基础技术:DEVICEMAPPER

在上一篇介绍AUFS的文章中,大家可以看到,Docker的分层镜像是怎么通过UnionFS这种文件系统做到的,但是,因为Docker首选的AUFS并不在Linux的内核主干里,所以,对于非Ubuntu的Linux分发包,比如CentOS,就无法使用AUFS作为Docker的文件系统了。于是作为第二优先级的DeviceMapper就被拿出来做分层镜像的一个实现。

Device Mapper 简介

DeviceMapper自Linux 2.6被引入,成为了Linux最重要的一个技术。它是内核中支持逻辑卷管理的通用设备映射机制,它为实现用于存储资源管理的块设备驱动提供了一个高度模块化的内核架构,它包含三个重要的对象概念,Mapped Device、Mapping Table、Target device。

Mapped Device 是一个逻辑抽象,可以理解成为内核向外提供的逻辑设备,它通过Mapping Table描述的映射关系和 Target Device 建立映射。Target device 表示的是 Mapped Device 所映射的物理空间段,对 Mapped Device 所表示的逻辑设备来说,就是该逻辑设备映射到的一个物理设备。

Mapping Table里有 Mapped Device 逻辑的起始地址、范围、和表示在 Target Device 所在物理设备的地址偏移量以及Target 类型等信息(注:这些地址和偏移量都是以磁盘的扇区为单位的,即 512 个字节大小,所以,当你看到128的时候,其实表示的是128*512=64K)。

DeviceMapper 中的逻辑设备Mapped Device不但可以映射一个或多个物理设备Target Device,还可以映射另一个Mapped Device,于是,就是构成了一个迭代或递归的情况,就像文件系统中的目录里除了文件还可以有目录,理论上可以无限嵌套下去。

DeviceMapper在内核中通过一个个模块化的 Target Driver 插件实现对 IO 请求的过滤或者重新定向等工作,当前已经实现的插件包括软 Raid、加密、多路径、镜像、快照等,这体现了在 Linux 内核设计中策略和机制分离的原则。如下图所示。从图中,我们可以看到DeviceMapper只是一个框架,在这个框架上,我们可以插入各种各样的策略,在这诸多“插件”中,有一个东西叫Thin Provisioning Snapshot,这是Docker使用DeviceMapper中最重要的模块

图片来源:http://people.redhat.com/agk/talks/FOSDEM_2005/

Thin Provisioning 简介

Thin Provisioning要怎么翻译成中文,真是一件令人头痛的事,我就不翻译了。这个技术是虚拟化技术中的一种。它是什么意思呢?你可以联想一下我们计算机中的内存管理中用到的——“虚拟内存技术”——操作系统给每个进程N多N多用不完的内址地址,但是呢,我们知道,物理内存是没有那么多的,如果按照进程内存和物理内存一一映射来玩的话,那么,我们得要多少的物理内存啊。所以,操作系统引入了虚拟内存的设计,意思是,我逻辑上给你无限多的内存,但是实际上是实报实销,因为我知道你一定用不了那么多,于是,达到了内存使用率提高的效果。(今天云计算中很多所谓的虚拟化其实完全都是在用和“虚拟内存”相似的Thin Provisioning的技术)

好了,话题拉回来,我们这里说的是存储。看下面两个图(图片来源),第一个是Fat Provisioning,第二个是Thin Provisioning,其很好的说明了是个怎么一回事(和虚拟内存是一个概念)

那么,Docker是怎么使用Thin Provisioning这个技术做到像UnionFS那样的分层镜像的呢?答案是,Docker使用了Thin Provisioning的Snapshot的技术。下面我们来介绍一下Thin Provisioning的Snapshot。

Thin Provisioning Snapshot 演示

下面,我们用一系列的命令来演示一下Device Mapper的Thin Provisioning Snapshot是怎么玩的,请切换到root用户运行命令。

首先,我们需要先建两个文件,一个是data.img,另一个是meta.data.img:
dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M
dd if=/dev/zero of=/tmp/meta.data.img bs=1K count=1 seek=100K

注意命令中seek选项,表示在备份时对of后面的部分也就是目标文件跳过多少块再开始写内容。因为bs是1K个字节,即块大小为1KB,所以当seek=10M时,也就是在备份时对data.img跳过10M个块(即10GB)再开始写内容。但此刻data.img在硬盘上占有空间只有1k的内容,当向其写入内容时,才会在硬盘上为其分配空间。我们可以用ls命令看一下data.img和meta.data.img实际分配的空间。

ls -lsh /tmp/data.img
ls -lsh /tmp/meta.data.img

然后,我们为这些文件创建loopback设备

losetup /dev/loop2015 /tmp/data.img
losetup /dev/loop2016 /tmp/meta.data.img

losetup -a

现在,我们为设备建一个Thin Provisioning的Pool,用dmsetup命令

dmsetup create hchen-thin-pool --table "0 20971520 thin-pool /dev/loop2016 /dev/loop2015 128 65536 1 skip_block_zeroing"

其中的参数解释如下(更多信息可参看Thin Provisioning的man page):

  • dmsetup create是用来创建thin pool的命令
  • hchen-thin-pool 是自定义的一个pool名,不冲突就好。
  • –table是这个pool的参数设置
    • 0代表数据起始的sector号
    • 20971520数据结束的sector号,一个sector是512字节,所以,20971520个正好是10GB
    • /dev/loop2016是meta文件的设备(前面我们建好了)
    • /dev/loop2015是data文件的设备(前面我们建好了)
    • 128是最小的可分配的sector数
    • 65536是最少可用sector的water mark,也就是一个threshold
    • 1 代表有一个附加参数
    • skip_block_zeroing是个附加参数,表示略过用0填充的块

然后,我们就可以看到一个Device Mapper的设备了:
ll /dev/mapper/hchen-thin-pool

我们的初始化工作还没有完成,接下来,还要创建一个Thin Provisioning 的 Volume:

dmsetup message /dev/mapper/hchen-thin-pool 0 "create_thin 0"
dmsetup create hchen-thin-volumn-001 --table "0 2097152 thin /dev/mapper/hchen-thin-pool 0"

其中:

  • 第一个命令中的create_thin是关键字,后面的0表示这个Volume的device 的 id
  • 第二个命令,是真正的为这个Volumn创建一个可以mount的设备,名字叫hchen-thin-volumn-001。

好了,在mount前,我们还要格式化一下:
mkfs.ext4 /dev/mapper/hchen-thin-volumn-001

好了,我们可以mount了(下面的命令中,我还创建了一个文件)
mkdir -p /mnt/base
mount /dev/mapper/hchen-thin-volumn-001 /mnt/base
echo "hello world, I am a base" > /mnt/base/id.txt
cat /mnt/base/id.txt

好了,接下来,我们来看看snapshot怎么搞:
dmsetup message /dev/mapper/hchen-thin-pool 0 "create_snap 1 0"
dmsetup create mysnap1 --table "0 2097152 thin /dev/mapper/hchen-thin-pool 1"

ll /dev/mapper/mysnap1

上面的命令中:

  • 第一条命令是向hchen-thin-pool发一个create_snap的消息,后面跟两个id,第一个是新的dev id,第二个是要从哪个已有的dev id上做snapshot(0这个dev id是我们前面就创建了了)
  • 第二条命令是创建一个mysnap1的device,并可以被mount。

下面我们来看看:
mkdir -p /mnt/mysnap1
mount /dev/mapper/mysnap1 /mnt/mysnap1

ll /mnt/mysnap1/

cat /mnt/mysnap1/id.txt

我们来修改一下/mnt/mysnap1/id.txt,并加上一个snap1.txt的文件:
echo "I am snap1" >> /mnt/mysnap1/id.txt
echo "I am snap1" > /mnt/mysnap1/snap1.txt

cat /mnt/mysnap1/id.txt

cat /mnt/mysnap1/snap1.txt

我们再看一下/mnt/base,你会发现没有什么变化:
ls /mnt/base
cat /mnt/base/id.txt

你是不是已经看到了分层镜像的样子了?

你还可以继续在刚才的snapshot上再建一个snapshot
dmsetup message /dev/mapper/hchen-thin-pool 0 "create_snap 2 1"
dmsetup create mysnap2 --table "0 2097152 thin /dev/mapper/hchen-thin-pool 2"

ll /dev/mapper/mysnap2

mkdir -p /mnt/mysnap2
mount /dev/mapper/mysnap2 /mnt/mysnap2
ls /mnt/mysnap2

好了,我相信你看到了分层镜像的样子了。

看完演示,我们再来补点理论知识吧:

  • Snapshot来自LVM(Logic Volumn Manager),它可以在不中断服务的情况下为某个device打一个快照。
  • Snapshot是Copy-On-Write的,也就是说,只有发生了修改,才会对对应的内容进行拷贝。

另外,这里有篇文章Storage thin provisioning benefits and challenges可以前往一读。

Docker的DeviceMapper

上面基本上就是Docker的玩法了。

ls -alhs /var/lib/docker/devicemapper/devicemapper

下面是相关的thin-pool。其中,有个一大串hash的device是正在启动的容器:
sudo ll /dev/mapper/dock*

我们可以看一下它的device id(Docker都把它们记下来了):
cat /var/lib/docker/devicemapper/metadata/29ef919ba2a241fa4fdb024fb66923522cea89e155162ca83b454212569b8f21

device_id是1326,size是10737418240,除以512,就是20971520 个 sector,我们用这些信息来做个snapshot看看(注:我用了一个比较大的dev id – 2048):
dmsetup message "/dev/mapper/docker-thinpool" 0 "create_snap 2048 1326"
dmsetup create dockersnap --table "0 20971520 thin /dev/mapper/docker-thinpool 2048"
mkdir /mnt/docker
mount /dev/mapper/dockersnap /mnt/docker/
ls /mnt/docker/
ls /mnt/docker/rootfs/

我们在docker的容器里用findmnt命令也可以看到相关的mount的情况(因为太长,下面只是摘要):
findmnt

如果你在使用loopback的devicemapper的话,当你的存储出现了问题后,正确的解决方案是:
rm -rf /var/lib/docker