文章目录
  1. 1. 背景知识
  2. 2. 准备工作
  3. 3. AUFS
  4. 4. Device Mapper
  5. 5. Btrfs
  6. 6. Overlay
  7. 7. 总结

我们都知道docker支持多种存储驱动,默认在ubuntu上使用AUFS,其他Linux系统上使用devicemapper。这篇文章从零开始,用一些Linux的命令来使用这些不同的存储,包括AUFS、Device Mapper、Btrfs和Overlay。

背景知识

Docker最早只是运行在Ubuntu和Debian上,使用的存储驱动是AUFS。随着Docker越来越流行,很多人都希望能把它运行在RHEL系列上。可是Linux内核和RHEL并不支持AUFS,最后红帽公司和Docker公司一起合作开发了基于Device Mapper技术的devicemapper存储驱动,这也成为Docker支持的第二款存储。由于Linux内核2.6.9就已经包含Device Mapper技术了,所以它也非常的稳定,代价是比较慢。ZFS是被Oracle收购的Sun公司为Solaris 10开发的新一代文件系统,支持快照,克隆,写时复制(CoW)等。ZFS的“Z”是最后一个字母,表示终极文件系统,不需要开发其它的文件系统了。虽然ZFS各种好,但是毕竟它的Linux版本是移植过来的,Docker官方并不推荐在生产环境上使用,除非你对ZFS相当熟悉。而且由于软件许可证不同的关系,它也无法被合并进Linux内核里。这么NB的文件系统出来后,Linux社区也有所回应。Btrfs就是和ZFS比较类似的Linux原生存储系统,在Linux内核2.6.29里就包含它了。虽然Btrfs未来是要替换devicemapper的,但是目前devicemapper更安全,更稳定,更适合生产环境。所以如果不是有很经验的话,也不那么推荐在生产环境使用。OverlayFS是类似AUFS的联合文件系统,但是轻量级一些,而且还能快一点儿。更重要的是,它已经被合并到Linux内核3.18版了。虽然OverlayFS发展得很快,但是它还非常年轻,如果要上生产系统,还是要记得小心为上。Docker还支持一个VFS驱动,它是一个中间层的抽象,底层支持ext系列,ntfs,nfs等等,对上层提供一个标准的文件操作接口,很早就被包含到Linux内核里了。但是由于它不支持写时复制,所以比较占磁盘空间,速度也慢,同样并不推荐上生产环境。

说到这里,好几个存储驱动都上Linux内核了,怎么AUFS一直被拒于门外呢?AUFS是一个日本人岡島順治郎开发的,他也曾希望能把这个存储驱动提交到内核中。但是据说Linus Torvalds有点儿嫌弃AUFS的代码写得烂……

准备工作

我们需要先安装virtualBoxvagrant。通过vagrant来驱动virtualBox搭建一个虚拟测试环境。首先在本地任意路径新建一个空文件夹比如test,运行以下命令启动并连接虚拟机:

virtual box host
1
2
3
4
5
mkdir test
cd test
vagrant init minimum/ubuntu-trusty64-docker
vagrant up
vagrant ssh

AUFS

AUFS是一个联合文件系统,也就是说,它是一层层垒上去的文件系统。最上层能看到的就是下层的所有系统合并后的结果。我们创建几个文件夹,layer1是最底层,result用来挂载,再搞几个文件:

vagrant host
1
2
3
4
5
6
7
mkdir ~/aufs
cd ~/aufs
mkdir layer1 layer2 result
echo "file1 in layer1" > layer1/file1
echo "file2 in layer1" > layer1/file2
echo "file1 in layer2" > layer2/file1

现在文件夹的层级结构看起来是酱紫的:


└── aufs
├── layer1
│ ├── file1 # file1 in layer1
│ └── file2 # file2 in layer1
├── layer2
│ └── file1 # file1 in layer2
└── result

然后一层层地挂载到result文件夹去(none的意思是挂载的不是设备文件),就能看到result现在有两个文件,以及它们的内容:
vagrant host
1
2
3
4
5
sudo mount -t aufs -o br=layer2=rw:layer1=ro none result
ls result
cat result/file1 # file1 in layer2
cat result/file2 # file2 in layer1

file1是由layer2提供的,file2是由layer1提供的,因为layer2里没有file2。如果我们在挂载后的目录写入file1~3:

vagrant host
1
2
3
4
5
6
7
8
9
echo "file1 in result" > result/file1
echo "file2 in result" > result/file2
echo "file3 in result" > result/file3
cat layer1/file1 # file1 in layer1
cat layer1/file2 # file2 in layer1
cat layer2/file1 # file1 in result
cat layer2/file2 # file2 in result
cat layer2/file3 # file3 in result

就会看到这些文件都是写入到layer2的。测试完成,清场~

vagrant host
1
2
3
4
sudo umount result
cd ..
rm -rf aufs

想要了解更细致点的话可以参考Docker基础技术:AUFS这篇文章。

回头来看Docker官方的这幅图:

虽然是删除文件的示例,但是也能清楚看到AUFS是怎么工作的。然后再结合docker一起看:

一切就都很清楚明了:一层层地累加所有的文件,最终加载到镜像里。

Device Mapper

Device Mapper是块设备的驱动,它的写时复制是基于块而非文件的。它包含3个概念:原设备,快照和映射表,它们的关系是:原设备通过映射表映射到快照去。一个快照只能有一个原设备,而一个原设备可以映射成多个快照。快照还能作为原设备映射到其他快照中,理论上可以无限迭代。

Device Mapper还提供了一种Thin-Provisioning技术。它实际上就是允许存储的超卖,用以提升空间利用率。当它和快照结合起来的时候,就可以做到许多快照挂载在一个原设备上,除非某个快照发生写操作,不然不会真正给快照们分配空间。这样的原设备叫做Thin Volume,它和快照都会由thin-pool来分配,超卖就发生在thin-pool之上。它需要两个设备用来存放实际数据和元数据。下面我们来创建两个文件,用来充当实际数据文件和元数据文件:

vagrant host
1
2
3
4
5
6
7
8
9
10
mkdir ~/devicemapper
cd ~/devicemapper
mkdir thin
mkdir snap1
mkdir snap11
mkdir snap12
dd if=/dev/zero of=metadata.img bs=1024K count=1
dd if=/dev/zero of=data.img bs=1024K count=10

文件建好了之后,用Loop device把它们模拟成块设备:

vagrant host
1
2
3
sudo losetup /dev/loop0 metadata.img
sudo losetup /dev/loop1 data.img
sudo losetup -a

然后创建thin-pool(参数的含义可以参考这篇文章):

vagrant host
1
2
sudo dmsetup create pool --table "0 20480 thin-pool /dev/loop0 /dev/loop1 128 32768 1 skip_block_zeroing"
ls /dev/mapper/ # 这里就会多一个pool

之后创建Thin Volume并格式化:

vagrant host
1
2
3
4
sudo dmsetup message /dev/mapper/pool 0 "create_thin 0"
sudo dmsetup create thin --table "0 2048 thin /dev/mapper/pool 0"
sudo mkfs.ext4 /dev/mapper/thin
ls /dev/mapper/ # 这里又会多一个thin

加载这个Thin Volume并往里写个文件。我的测试机器上需80秒左右才能把这个文件同步回thin-pool去。如果不等待,可能接下来的快照里就不会有这个文件;如果等待时间不足(小于30秒),可能快照里会有这个文件,但是内容为空。这个时间跟thin-pool的参数,尤其是先前创建的实际数据和元数据文件有关。

vagrant host
1
2
3
sudo mount /dev/mapper/thin thin
sudo sh -c "echo file1 in thin > thin/file1"
sleep 80s

睡饱后,给thin这个原设备添加一份快照snap1:

vagrant host
1
2
3
sudo dmsetup message /dev/mapper/pool 0 "create_snap 1 0"
sudo dmsetup create snap1 --table "0 2048 thin /dev/mapper/pool 1"
ls /dev/mapper/ # 这里又会多一个snap1

加载这个快照,能看见先前写的file1文件被同步过来了。再往里写个新文件。还是要保证睡眠充足:

vagrant host
1
2
3
4
5
sudo mount /dev/mapper/snap1 snap1
sudo ls -l snap1
sudo cat snap1/file1 # file1 in thin
sudo sh -c "echo file2 in snap1 > snap1/file2"
sleep 80s

快照是能作为原设备映射成其他快照的,下面从snap1映射一份snap11:

vagrant host
1
2
sudo dmsetup message /dev/mapper/pool 0 "create_snap 2 1"
sudo dmsetup create snap11 --table "0 2048 thin /dev/mapper/pool 2"

加载完后就能看到file1和file2都被同步过来了:

vagrant host
1
2
3
4
sudo mount /dev/mapper/snap11 snap11
sudo ls -l snap11
sudo cat snap11/file1 # file1 in thin
sudo cat snap11/file2 # file2 in snap1

一份原设备是可以映射成多个快照的,下面从snap1再映射一份snap12:

vagrant host
1
2
3
4
5
6
7
sudo dmsetup message /dev/mapper/pool 0 "create_snap 3 1"
sudo dmsetup create snap12 --table "0 2048 thin /dev/mapper/pool 3"
sudo mount /dev/mapper/snap12 snap12
sudo ls -l snap12
sudo cat snap11/file1 # file1 in thin
sudo cat snap11/file2 # file2 in snap1

测试完成,清场~

vagrant host
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo umount snap1
sudo umount snap11
sudo umount snap12
sudo umount thin
sudo dmsetup remove snap11
sudo dmsetup remove snap12
sudo dmsetup remove snap1
sudo dmsetup remove thin
sudo dmsetup remove pool
sudo losetup -d /dev/loop0
sudo losetup -d /dev/loop1
cd ..
rm -rf devicemapper

在RHEL,CentOS系列上,Docker默认使用loop-lvm,类似上文的机制(配的是稀疏文件),虽然性能比本文要好,但也是堪忧。官方推荐使用direct-lvm,也就是直接使用raw分区。这篇文章介绍了如何在CentOS 7上使用direct-lvm。另外,剖析Docker文件系统对AUFS和Device Mapper有很详细的讲解。

回头来看Docker官方的这幅图:

一切就都很清楚明了:最底层是两个文件:数据和元数据文件。这两个文件上面是一个pool,再上面是一个原设备,然后就是一层层的快照叠加上去,直至镜像,充分共享了存储空间。

Btrfs

Btrfs的Btr是B-tree的意思,元数据用B树管理,比较高效。它也支持块级别的写时复制,性能也不错,对SSD有优化,但是不支持SELinux。它支持把文件系统的一部分配置为Subvolume子文件系统,父文件系统就像一个pool一样给这些子文件系统们提供底层的存储空间。这就意味着子文件系统无需关心设置各自的大小,反正背后有父文件系统撑腰。Btrfs还支持对子文件系统的快照,速度非常快,起码比Device Mapper快多了。快照在Btrfs里也是一等公民,同样也可以像Subvolume那样再快照、被加载,享受写时复制技术。

要使用Btrfs,得先安装工具包:

vagrant host
1
sudo apt-get install btrfs-tools

下面我们来创建一个文件,用Loop device把它模拟成块设备:

vagrant host
1
2
3
4
5
6
7
8
9
mkdir ~/btrfs
cd ~/btrfs
mkdir result
dd if=/dev/zero of=data.img bs=1024K count=10
sudo losetup /dev/loop0 data.img
sudo losetup -a

把这个块设备格式化成btrfs:

vagrant host
1
2
sudo mkfs.btrfs -f /dev/loop0
sudo mount /dev/loop0 result/

新建一个subvolumn并往里写个文件:

vagrant host
1
2
sudo btrfs subvolume create result/origin/
sudo sh -c "echo file1 in origin > result/origin/file1"

给result/origin这个subvolumn添加一份快照snap1,能看见先前写的file1文件被同步过来了。再往里写个新文件:

vagrant host
1
2
3
4
sudo btrfs subvolume snapshot result/origin/ result/snap1
sudo ls -l result/snap1
sudo cat result/snap1/file1 # file1 in origin
sudo sh -c "echo file2 in snap1 > result/snap1/file2"

快照也像Device Mapper那样能生成其他的快照,下面从snap1生成一份snap11:

vagrant host
1
2
3
4
sudo btrfs subvolume snapshot result/snap1/ result/snap11
sudo ls -l result/snap11
sudo cat result/snap11/file1 # file1 in origin
sudo cat result/snap11/file2 # file2 in snap1

也是可以生成多个快照的,下面从snap1再生成一份snap12:

vagrant host
1
2
3
4
sudo btrfs subvolume snapshot result/snap1/ result/snap12
sudo ls -l result/snap12
sudo cat result/snap12/file1 # file1 in origin
sudo cat result/snap12/file2 # file2 in snap1

可以使用这个命令来查看所有快照:

vagrant host
1
sudo btrfs subvolume list result

可以使用这个命令来查看这个文件系统:

vagrant host
1
sudo btrfs filesystem show /dev/loop0

测试完成,清场~

vagrant host
1
2
3
4
5
6
sudo umount result
sudo losetup -d /dev/loop0
cd ..
rm -rf btrfs

我们看到它比Device Mapper更简单一些,并且速度很快,不需要sleep以待同步完成。这篇文章虽然有点儿旧了,但是对Btrfs的原理讲得挺清楚的。

回头来看Docker官方的这幅图:

一切就都很清楚明了:最底层是个subvolume,再它之上层层累加快照,镜像也不例外。

Overlay

最初它叫做OverlayFS,后来被合并进Linux内核的时候被改名为Overlay。它和AUFS一样都是联合文件系统。Overlay由两层文件系统组成:upper(上层)和lower(下层)。下层可以是只读的任意的Linux支持的文件系统,甚至可以是另一个Overlay,而上层一般是可读写的。所以模型上比AUFS要简单一些,这就是为什么我们会认为它更轻量级一些。

uname -r可以看到我们现在这个vagrant虚拟机的Linux内核版本是3.13,而内核3.18之后才支持Overlay,所以我们得先升级一下内核,否则在mount的时候会出错:mount: wrong fs type, bad option, bad superblock on overlay。运行以下命令来升级ubuntu 14.04的内核:

vagrant host
1
2
3
4
5
6
7
8
9
cd /tmp/
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v3.18-vivid/linux-headers-3.18.0-031800-generic_3.18.0-031800.201412071935_amd64.deb
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v3.18-vivid/linux-headers-3.18.0-031800_3.18.0-031800.201412071935_all.deb
wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v3.18-vivid/linux-image-3.18.0-031800-generic_3.18.0-031800.201412071935_amd64.deb
sudo dpkg -i linux-headers-3.18.0-*.deb linux-image-3.18.0-*.deb
sudo update-grub
sudo reboot

等待重启之后,重新连接进vagrant虚拟机:

virtual box host
1
vagrant ssh

完成之后再用uname -r看一下,现在应该已经是3.18了。下面我们开搞吧:

vagrant host
1
2
3
4
5
6
7
mkdir ~/overlay
cd ~/overlay
mkdir lower upper work merged
echo file1 in lower > lower/file1
echo file2 in lower > lower/file2
echo file1 in upper > upper/file1

现在的文件层级结构看起来是酱紫的:


├── lower
│ ├── file1 # file1 in lower
│ └── file2 # file2 in lower
├── merged
├── upper
│ └── file1 # file1 in upper
└── work

然后我们加载merged,让它的下层是lower,上层是upper。除此之外还需要一个workdir,据说是用来做一些内部文件原子性操作的,必须是空文件夹:
vagrant host
1
2
3
4
sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged
cat merged/file1 # file1 in upper
cat merged/file2 # file2 in lower

所以我们最终得到了类似AUFS一样的结果。测试完成,清场~

vagrant host
1
2
3
4
sudo umount merged
cd ..
rm -rf overlay

在Linux内核3.19之后,overlay还能够支持多层lower(Multiple lower layers),这样就能更好地支持docker镜像的模型了。多层的mount命令是酱紫的:mount -t overlay overlay -olowerdir=/lower1:/lower2:/lower3 /merged,有兴趣的朋友可以再次升级Linux内核试试。

回头来看Docker官方的这幅图:

很好地说明了OverlayFS驱动下容器和镜像的存储是怎么工作的,lower、upper和merged各自的关系。然后看看docker镜像:

因为目前docker支持的还不是多层存储,所以在镜像里只是用硬链接来在较低层之间共享数据。今后docker应该会利用overlay的多层技术来改善镜像各层的存储。

总结

ZFS和VFS由于官方都不推荐上生产我们就不试了,虽然OverlayFS也不推荐,但是它毕竟代表着未来的趋势,还是值得我们看一看的。下表列出了docker所支持的存储驱动特性对比:

驱动 联合文件系统 写时复制 内核 SELinux 上生产环境 速度 存储空间占用
AUFS 文件级别 不支持 支持 推荐
Device Mapper 不是 块级别 2.6.9 支持 有限推荐 较大
Btrfs 不是 块级别 2.6.29 不支持 有限推荐 较快 较小
OverlayFS 文件级别 3.18 不支持 不推荐
ZFS 不是 块级别 不支持 不支持 不推荐 较快 较小
VFS 不是 不支持 2.4 支持 不推荐 很慢
文章目录
  1. 1. 背景知识
  2. 2. 准备工作
  3. 3. AUFS
  4. 4. Device Mapper
  5. 5. Btrfs
  6. 6. Overlay
  7. 7. 总结