1. 介绍

Device Mapper 不同于 AUFS、ext4、NFS 等 , 因为它并不是一个文件系统(File System) ,而是 Linux 内核映射块设备的一种技术框架。提供的一种从逻辑设备(虚拟设备)到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略。

当前比较流行的 Linux 下的逻辑卷管理器如 LVM2(Linux Volume Manager 2 version)、EVMS(Enterprise Volume Management System)、dmraid(Device Mapper Raid Tool)等都是基于该机制实现的。

值得一提的是Device Mapper工作在块级别(block),并不工作在文件级别(file)。Device Mapper自Linux 2.6.9 后编入Linux内核 , 所有基于Linux内核2.6.9以后的发行版都内置Device Mapper , 但你需要进行一些额外的配置才能在docker中使用它。比如在RHEL和CentOS系统中 , docker默认使用的存储驱动是overlay。

devicemapper 存储驱动使用专用于docker的块设备 , 它运行在块级别上而不是文件级别。使用块设备比直接使用文件系统性能更好 , 通过向Docker的宿主机添加物理存储可以扩展块设备的存储空间。

Device Mapper主要分为用户空间部分和内核空间部分

用户空间相关部分主要负责配置具体的策略和控制逻辑 , 比如逻辑设备和哪些物理设备建立映射 , 怎么建立这些映射关系等 , 包含device mapper库和dmsetup工具。对用户空间创建删除device mapper设备的操作进行封装

内核中主要提供完成这些用户空间策略所需要的机制 , 负责具体过滤和重定向IO请求。通过不同的驱动插件,转发 IO 请求至目的设备上。附上 Device Mapper 架构图。

20210415191935350

2. Device Mapper技术分析

它在内核中支持逻辑卷管理的通用设备映射机制 , 它为实现用于存储资源管理的块设备驱动提供了一个高度模块化的内核和架构 , 包含三个重要的对象概念 : 映射设备(mapped device),映射表(map table),目标设备(target device)。

  • 映射设备 (Mapper Device) : 是一个逻辑抽象 , 可以理解成为内核向外提供的逻辑设备, 映射设备通过映射表描述的映射关系和目标设备建立联系,映射设备向下寻找必须找到支撑的目标设备。DeviceMapper中的逻辑设备Mapped Device不但可以映射一个或多个目标设备(Target Device ) , 还可以映射另一个Mapped Device , 于是 , 就是构成了一个迭代或递归的情况 , 就像文件系统中的目录里除了文件还可以有目录, 理论上可以无限嵌套下去。

  • 映射表(map table ) : 存储映射设备和目标设备的映射关系。是由用户空间创建,传递到内核空间。映射表里有映射设备(Mapped Device)逻辑的起始地址、范围、和表示在目标设备(Target Device)所在物理设备的地址偏移量以及Target类型等信息。(注: 这些地址和偏移量都是以磁盘的扇区为单位的 , 即 512 个字节大小 , 所以 , 当你看到128的时候 , 其实表示的是128*512=64K) 。

  • 目标设备(target device) : 可以是映射设备或者物理设备,如果目标设备是一块映射设备,则属于嵌套,理论上可以无限迭代下去。目标设备本质上是映射设备所映射的物理空间段 , 对映射设备所表示逻辑设备来说 , 就是该逻辑设备映射到的一个物理设备。

简而言之,Device Mapper 对外提供一个虚拟设备供使用,而这块虚拟设备可以通过映射表找到相应的地址,该地址可以指向一块物理设备,也可以指向一个虚拟设备。

20210415192036732

映射驱动在内核空间是插件 , Device Mapper 在内核中通过一个一个模块化的Target Driver插件实现对 IO 请求的过滤或者重新定向等工作,当前已经实现的插件包括软 Raid、加密、多路径、镜像、快照等,这体现了在 Linux 内核设计中策略和机制分离的原则。Thin Provisioning Snapshot , 这是Docker使用DeviceMapper中最重要的插件模块。

Device Mapper 中的 IO 流处理,从虚拟设备(逻辑设备)根据映射表并指定特定的映射驱动转发到目标设备上。

3. Device Mapper 检测安装

一般情况下,目前Linux 都会自带 , 不需要我们额外安装。下面是额外安装需要的步骤:

首先我们查看Device Mapper , 如下:

[root@bogon ~]# ls -l /sys/class/misc/device-mapper/
总用量 0
-r--r--r--. 1 root root 4096 3月 23 10:55 dev
drwxr-xr-x. 2 root root 0 3月 23 10:55 power
lrwxrwxrwx. 1 root root 0 3月 23 10:55 subsystem -> ../../../../class/misc
-rw-r--r--. 1 root root 4096 3月 23 10:55 uevent

或者也可以再/proc/devices文件中检查是否有device-mapper条目 , 如下:

[root@bogon ~]# grep device-mapper /proc/devices 
253 device-mapper

如果没有的话,就需要安装,安装命令如下:

yum install -y device-mapper

安装完成后,还需要加载dm_mod内核模块,命令如下:

modprobe dm_mod

加载完毕后,就可以了。

4. Docker 中的 Device Mapper 核心技术

Docker 的 devicemapper 驱动有三个核心概念,copy on-write(写复制),thin-provisioning(精简配置)。snapshot(快照),首先简单介绍一下这三种技术。

  • CoW(copy on write)写复制:一些文件系统提供的写时复制策略。

    aufs 的cow原理如下:

    当容器需要修改一个文件 , 而该文件位于低层branch时 , 顶层branch会直接复制低层branch的文件至顶层再进行修改 , 而低层的文件不变 , 这种方式即是CoW技术(写复制)。

    当容器删除一个低层 branch 文件时,只是在顶层 branch 对该文件进行重命名并隐藏,实际并未删除文件,只是不可见。

    下图所示 , 容器层所见 file1 文件为镜像层文件 ,当需要修改file1时 , 会从镜像层把文件复制到容器层 , 然后进行修改 , 从而保证镜像层数据的完整性和复用性。

    2021041519283856

    下图所示 , 当需要删除file1时 , 由于file1是镜像层文件 , 容器层会创建一个.wh前置的隐藏文件 , 从而实现对 file1的隐藏 , 实际并未删除file1 , 从而保证镜像层数据的完整性和复用性。

    image-20230224165421594

    devicemapper 支持在块级别(block)写复制。

  • Snapshot(快照技术): 关于指定数据集合的一个完全可用拷贝,该拷贝包括相应数据在某个时间点(拷贝开始的时间点)的映像。快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。而从具体的技术细节来讲,快照是指向保存在存储设备中的数据的引用标记或指针。

  • Thin-provisioning(精简配置),直译为精简配置。Thin-provisioning 是动态分配,需要多少分配多少,区别于传统分配固定空间从而造成的资源浪费。

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

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

    20210415193841940
    20210415193925816

下图中展示了某位用户向服务器管理员请求分配 10TB 的资源的情形。实际情况中这个数值往往是峰值,根据使用情况,分配 2TB 就已足够。因此,系统管理员准备 2TB 的物理存储,并给服务器分配 10TB 的虚拟卷。服务器即可基于仅占虚拟卷容量 1/5 的现有物理磁盘池开始运行。这样的“始于小”方案能够实现更高效地利用存储容量。

20210415193944724

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

5. Thin Provisioning Snapshot演示

首先 , 我们需要先建两个文件 , 一个是data.img , 一个是meta.data.img:

$ dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M
​
1+0 records in
1+0 records out
1024 bytes (1.0 kB) copied, 0.000621428 s, 1.6 MB/s
​
$ dd if=/dev/zero of=/tmp/meta.data.img bs=1K count=1 seek=1G
​
1+0 records in
1+0 records out
1024 bytes (1.0 kB) copied, 0.000140858 s, 7.3 MB/s

注意命令中 seek 选项,其表示为略过 of 选项指定的输出文件的前10G个output的 bloksize 的空间后再写入内容。

因为bs是1个字节 , 所以也就是10G的尺寸 , 但其实在硬盘上是没有占有空间的 , 占有空间只有1k的内容。当向其写入内容时 , 才会在硬盘上为其分配空间。

我们可以用 ls 命令看一下 , 实际分配了12K和4K。

$ ls -lsh /tmp/data.img
​
12K -rw-r--r--. 1 root root 11G Aug 25 23:01 /tmp/data.img
​
$ ls -slh /tmp/meta.data.img
​
4.0K -rw-r--r--. 1 root root 101M Aug 25 23:17 /tmp/meta.data.img

然后,我们为这个文件创建一个 loopback 设备。(loop2015和loop2016是我乱取的两个名字)

$ losetup /dev/loop2015 /tmp/data.img
$ losetup /dev/loop2016 /tmp/meta.data.img
​
$ losetup -a
/dev/loop2015: [64768]:103991768 (/tmp/data.img)
/dev/loop2016: [64768]:103991765 (/tmp/meta.data.img)

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

$ dmsetup create hchen-thin-pool \
  --table "0 20971522 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 位置

    • 20971522 代表结尾的sector号 , 前面说过 , 一个sector是512字节 , 所以 , 20971522个正好是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
​
lrwxrwxrwx. 1 root root 7 Aug 25 23:24 /dev/mapper/hchen-thin-pool -> ../dm-4

接下来,我们的初始还没有完成,还要创建一个 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。2097152 只有 1GB。

好了,在 mount 前,我们还要格式化一下:

$ mkfs.ext4 /dev/mapper/hchen-thin-volumn-001
​
mke2fs 1.42.9 (28-Dec-2013)
Discarding device blocks: done
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=16 blocks, Stripe width=16 blocks
65536 inodes, 262144 blocks
13107 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=268435456
8 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376
 
Allocating group tables: done
Writing inode tables: done
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done

好了,我们可以 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
​
hello world, I am a base

接下来,我们来看看 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
​
lrwxrwxrwx. 1 root root 7 Aug 25 23:49 /dev/mapper/mysnap1 -> ../dm-5

上面的命令中:

  • 第一条命令是向 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/
​
total 20
-rw-r--r--. 1 root root 25 Aug 25 23:46 id.txt
drwx------. 2 root root 16384 Aug 25 23:43 lost+found
​
$ cat /mnt/mysnap1/id.txt
​
hello world, I am a base

我们来修改一下 /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
​
hello world, I am a base
I am snap1
​
$ cat /mnt/mysnap1/snap1.txt
​
I am snap1

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

$ ls /mnt/base
​
id.txt      lost+found
​
$ cat /mnt/base/id.txt
​
hello world, I am a base

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

6. Docker 的 devicemapper


上面基本上就是 Docker 的玩法了,我们可以看一下 docker 的 loopback 设备:

$ losetup -a
​
/dev/loop0: [64768]:38050288 (/var/lib/docker/devicemapper/devicemapper/data)
/dev/loop1: [64768]:38050289 (/var/lib/docker/devicemapper/devicemapper/metadata)

其中 data 100GB,metadata 2.0GB

$ ls -alsh /var/lib/docker/devicemapper/devicemapper
​
506M -rw-------. 1 root root 100G Sep 10 20:15 data
1.1M -rw-------. 1 root root 2.0G Sep 10 20:15 metadata

下面是相关的 thin-pool。其中,有个当一大串 hash 串的 device 是正在启动的容器:

$ ll /dev/mapper/dock*
​
lrwxrwxrwx. 1 root root 7 Aug 25 07:57 /dev/mapper/docker-253:0-104108535-pool -> ../dm-2
lrwxrwxrwx. 1 root root 7 Aug 25 11:13 /dev/mapper/docker-253:0-104108535-deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf -> ../dm-3

我们可以看一下它的 device id(Docker 都把它们记下来了):

$ cat /var/lib/docker/devicemapper/metadata/deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf

device_id 是 24,size 是 10737418240,除以 512,就是 20971520 个 sector。

我们用这些信息来做个 snapshot 看看(注:我用了一个比较大的 dev id – 1024):

$ dmsetup message "/dev/mapper/docker-253:0-104108535-pool" 0 \
  "create_snap 1024 24"
  
$ dmsetup create dockersnap --table \
  "0 20971520 thin /dev/mapper/docker-253:0-104108535-pool 1024"
  
$ mkdir /mnt/docker
​
$ mount /dev/mapper/dockersnap /mnt/docker/
​
$ ls /mnt/docker/
​
id lost+found rootfs
​
$ ls /mnt/docker/rootfs/
​
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var

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

$ findmnt
​
TARGET                SOURCE               
/                 /dev/mapper/docker-253:0-104108535-deefcd630a60[/rootfs]
/etc/resolv.conf  /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/resolv.conf]
/etc/hostname     /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hostname]
/etc/hosts        /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hosts]

参考链接

Device Mapper 存储介绍 - yanling0813 - 博客园 (cnblogs.com)

【Linux命令】dmsetup--device mapper 管理工具(更底层的管理工具) - yanling0813 - 博客园 (cnblogs.com)

大话Docker(二):Device Mapper - 简书 (jianshu.com)

Device Mapper系列基础教程:Device Mapper 的原理 – 云原生实验室 - Kubernetes|Docker|Istio|Envoy|Hugo|Golang|云原生 (icloudnative.io)


熊熊