docker storage-driver

作用:

  • 容器镜像的存储和管理。
  • 容器启动时的文件系统的准备。

概念:

写时复制(copy-on-write)
只有当快照出来的对象,它需要进行写操作时,它会从父镜像中将需要修改的块拷贝到自己的文件系统,然后在进行修改。
所以通过cow,容器启动拷贝只是拷贝一些元数据,所以容器启动速度特别快。
用时分配(allocate-on-daemand)
thin-pool用的就是此技术
当分配一个100G的空间时,并不会立刻占满这100,只是占用了一些文件的元数据,当写入数据时会根据实际的大小动态的分配,类似linux中的稀疏文件。

常见的storage-driver

aufs:

linux上较老牌的storage_driver,基于文件级的存储driver,将多层合并成文件系统的单层表示。底层都是只读层,只有最上层文件系统可写,当写和修改文件时也是使用cow技术。

优点:拥有较长的历史,在大量生产环境中验证过,稳定性、可靠性较高
缺点:aufs对文件第一次进行修改操作时需要将整个文件复制到读写层,哪怕你只是修改文件的一小部分,这样会造成很大的性能开销。

overlay:

与aufs设计非常类似,也是基于文件级的存储driver同时 也是 一个union filesystem,overlay主要分两层upperdir和lowerdir,其中lowerdir对应的docker中的镜像层,upperdir对应的 是容器层,merged对应的是有效的容器挂载点。


优点:overlay的速度比aufsh和devicemapper快,因为aufs有很多层,而overlay只有两层。
overlay支持页缓存共享,多个容器访问同一个文件,可以共享一个或多个页缓存选项。

缺点:1、overlay是个新生儿,还不具备在生产中使用的条件,但overlay2在centos的3.10.0-514及以上内核版本可以使用。
2、因为overlay也是基于文件级的存储,所以根aufs 一样对文件进行第一次修改操作时需要将整个文件拷贝到读写层但比aufs好,因为overlay只有两层,而aufs有多层。
3、overlay只有两层,镜像中的每一层并不对应overlay中的层,对应的是/var/lib/docker/overlay/中一个文件夹,然后用硬连接的方式将下面层文件引用到上层。
overlayfs的详细原理https://blog.csdn.net/luckyapple1028/article/details/78075358

overlay2:

overlay2:overlay的改进版,只支持4.0以上内核因为在4.0的内核上添加了Multiple lower layers in overlayfs的特性,所以overlay2可以直接造成muitiple lower layers不用像overlay一样要通过硬链接的方式(最大128层) centos的话支持3.10.0-514及以上内核版本也有此特性,所以消耗更少的inode。

docker官方overlay2的PR:
https://github.com/moby/moby/pull/22126

LINUX KERNERL 4.0 release说明:
https://kernelnewbies.org/Linux_4.0

为什么overlay相比overlay2要更加占用inode?

1、overlay只支持两层lowerdir和upperdir,并且只支持一个lowerdir,所以如果你的容器镜像有多层的话,层与层之前的数据共享是通过硬链接来实现的,我们也知道硬链接本身是同一个inode但不同的文件名而已,但为什么还是会大量消耗inode这里简单做的实验
我们在一台配置了storage-driver的机器上PULL ubuntu:18.04镜像

1
2
3
4
5
6
7
8
[root@master dir]# docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
32802c0cfa4d: Pull complete
da1315cffa03: Pull complete
fa83472a3562: Pull complete
f85999a86bef: Pull complete
Digest: sha256:48eb09a5c832172357f172593ce5e98e027814f758f6aeaef59a7fd658e50d49
Status: Downloaded newer image for ubuntu:18.04

整个镜像是一个四层镜像层组成的镜像,在/var/lib/docker/overlay/下每层对应一个目录,通过tree命令可以看见每个目录内只包含一个root文件夹

1
2
3
4
5
6
7
8
9
10
11
12
tree -L 2 /var/lib/docker/overlay/
/var/lib/docker/overlay/
|-- 0a9cd41c44a802a325bfc5bfdda757bced8eaf69a8d6004e5d6fcb730591ab31
| `-- root
|-- 1f9c95c6642a98930a14d914ac9bcfe9535b5a604dc27851cd76266326c76ed7
| `-- root
|-- 2d79f688e1bf505ef20100f7450ad7e5ea550500bd07a17b7b9b08513fc96988
| `-- root
`-- e8b1fcddec600628a75964619da3d0de7310fcbd6350b0c07ddf038d71c25d8b
`-- root
8 directories, 0 files

这个root目录内包含的是该层独有的文件和根lowdir共享的数据硬链接,这里看见共享的数据他们本身都是通过硬链接方式连接起来的。

1
2
3
4
5
6
7
8
9
10
11
[root@master overlay]# ls -i /var/lib/docker/overlay/0a9cd41c44a802a325bfc5bfdda757bced8eaf69a8d6004e5d6fcb730591ab31/root/bin/ls
70832997 /var/lib/docker/overlay/0a9cd41c44a802a325bfc5bfdda757bced8eaf69a8d6004e5d6fcb730591ab31/root/bin/ls
[root@master overlay]# ls -i /var/lib/docker/overlay/1f9c95c6642a98930a14d914ac9bcfe9535b5a604dc27851cd76266326c76ed7/root/bin/ls
70832997 /var/lib/docker/overlay/1f9c95c6642a98930a14d914ac9bcfe9535b5a604dc27851cd76266326c76ed7/root/bin/ls
[root@master overlay]# ls -i /var/lib/docker/overlay/2d79f688e1bf505ef20100f7450ad7e5ea550500bd07a17b7b9b08513fc96988/root/bin/ls
70832997 /var/lib/docker/overlay/2d79f688e1bf505ef20100f7450ad7e5ea550500bd07a17b7b9b08513fc96988/root/bin/ls
[root@master overlay]# ls -i /var/lib/docker/overlay/e8b1fcddec600628a75964619da3d0de7310fcbd6350b0c07ddf038d71c25d8b/root/bin/ls
70832997 /var/lib/docker/overlay/e8b1fcddec600628a75964619da3d0de7310fcbd6350b0c07ddf038d71c25d8b/root/bin/ls

但为什么overlay还是会占用大量inode呢?根本原因在于那些文件夹,每层的root目录内存放的都是完整的rootfs文件夹,但它们都是新建出来
的,它们inode都不一样,所以在overlay下一个容器镜像层数越多,占用的inode就越多

我们看看overlay2的方式
因为overlay2是支持linux kernel的Multiple lower layers in overlayfs的特性所以原生就支持多个lower所以不需要在通过硬链接方式
同样拉取个ubuntu:18.04的镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/var/lib/docker/overlay2/
|-- 6e599f35a3366d95287390fc00183a80bfc9a34492aaf9694836ec7b5722d0b6
| |-- diff
| |-- link
| |-- lower
| |-- merged
| `-- work
|-- 6f66846fe6a29834193bf36380eb1664947f21435edd8ce8fbc9b79dea92c51b
| |-- diff
| |-- link
| |-- lower
| |-- merged
| `-- work
|-- a1869d02b056d66742372c4b6d31d64f98b477590cdd76a842a653424f46710b
| |-- diff
| |-- link
| |-- lower
| |-- merged
| `-- work
|-- backingFsBlockDev
|-- da05539957d703ae81cf279c76059c947c96bd1f91fd24691b9e7630dcb13ff7
| |-- diff
| `-- link
`-- l
|-- 2LYVTSJQWPVOB46BCA74GFHJ7T -> ../da05539957d703ae81cf279c76059c947c96bd1f91fd24691b9e7630dcb13ff7/diff
|-- HEXAQW5O23XL6HHRVOMKWGAI4Z -> ../6f66846fe6a29834193bf36380eb1664947f21435edd8ce8fbc9b79dea92c51b/diff
|-- KXIOHK4OI6PPPFZ56I3ZORNHS7 -> ../6e599f35a3366d95287390fc00183a80bfc9a34492aaf9694836ec7b5722d0b6/diff
`-- PHQQ6RCEXR6BLZJGKBP6GYS55Q -> ../a1869d02b056d66742372c4b6d31d64f98b477590cdd76a842a653424f46710b/diff

在overlay2每层的内容都是不一样的,diff是文件系统的统一挂载点,link文件描述的是该层的标识符,lower文件描述了层与层之间的组织关系,overlay2是将底层多个lowerdir和upperdir和workdir联合挂载,形成最终的merged挂载点。

devicemapper:

在linux2.6内核版本中并入内核,devicemapper将所有的镜像和容器存储在自己的虚拟块设备上,devicemapper根上面aufs和overlay不一样的是devicemapper工作在块层次上而不是文件层次上。devicemapper驱动会从配置好的thin-pool中创建一个带一个带有文件系统的块设备,所有镜像都是创建的块设备的快照,容器层是镜像层的快照,都是采用copy-on-write策略。
优点:
1、devicemapper的copy-on-write策略以64KB为最小单元,相比aufs和overlay整个文件的复制,这个效率很高了。
缺点:
2、启动n个容器就复制n分文件在内存中,对内存影响大,不合适单台host容器密度高的业务场景。

从上图可以看出,镜像的每一层都是它下面一层的快照,镜像是thin-pool里面创建的块设备的快照。
devicemapper分两种模式loop-lvm,direct-lvm,这两种模式的主要区别就是创建thin-pool的方式不一样。
loop-lvm是在/var/lib/docker/devicemapper/创建data和metada两个稀疏文件通过losetup将这两个文件映射成块设备,然后通过devicemapper映射成thin-pool。

direct-lvm是直接通过lvm的方式将设备创建成pv,组成两个vg分别为data和metadata,将这两个vg组成lvm的thin-pool。
需要注意的docker官方推荐centos和rhel生产中使用devicmapper的direct-lvm模式,不推荐loop-lvm因为loop-lvm在挂载多个容器后性能会急剧下降。

适用场景:

没有好坏分只有合适分,在不同业务场景下使用不同的storage-driver。
这是docker官方给出的一个不同linux发行版的推荐表
https://docs.docker.com/engine/userguide/storagedriver/selectadriver/#docker-ce

不同storage-driver所支持的文件系统

不同的存储驱动在不同的应用场景下性能不同。例如,AUFS、Overlay、Overlay2 操作在文件级别,内存使用相对更高效,但大文件读写时,容器层会变得很大;DeviceMapper、Btrfs、ZFS 操作在块级别,适合工作在io负载高的场景;容器层数多,且写小文件频繁时,devicemapper 效率比 Overlay 更高;Btrfs、ZFS 更耗内存。
此图片来源七牛布道师 陈爱珍在docker.io的分享http://dockone.io/article/1513

性能对比
http://dockone.io/article/1513

docker默认是使用操作系统本身默认的storage-driver,比如说你在ubuntu上安装docker那么默认就是aufs,在centos7或rhel7上部署docker默认就是overlay。
将centos7默认的overlay改成生产可用的devicemapper的direct-lvm模式。
安装device-mapper-persistent-data, lvm2包

1
yum install device-mapper-persistent-data, lvm2 -y

创建pv

1
pvcreate /dev/sdb

创建vg

1
vgcreate docker /dev/sdb

创建lv,这里需要注意,我们分两个lv,一个是data,一个是metadata,最后要将这个两个lv组成一个thin-pool

1
2
lvcreate --wipesignatures y -n thinpool docker -l 95%VG #使用95% VG剩余空间
lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG

将lv docker/thinpool做为thinpool ,docker/thinpoolmeta做为pool的metadata

1
lvconvert -y --zero n -c 512K --thinpool docker/thinpool --poolmetadata docker/thinpoolmeta

配置lvm thinpool的自动扩容

vi /etc/lvm/profile/docker-thinpool.profile

1
2
3
4
activation {
thin_pool_autoextend_threshold=80
thin_pool_autoextend_percent=20
}

thin_pool_autoextend_threshold:已用空间百分比 ,100为关闭
thin_pool_autoextend_percent=20:每次扩容thinpool的容量比例,0为关闭

当磁盘使用达到80%时,再扩展当前容量的20%。更新/etc/lvm/profile/docker-thinpool.profile

激活此规则

1
lvchange --metadataprofile docker-thinpool docker/thinpool

对逻辑卷启用监视

1
lvs -o+seg_monitor

如果之前/var/lib/docker有数据,建议进行备份,没有的可以直接运行。

1
2
3
4
5
6
7
8
9
10
编辑daemon.json文件
/etc/docker/daemon.json
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.thinpooldev=/dev/mapper/docker-thinpool",
"dm.use_deferred_removal=true",
"dm.use_deferred_deletion=true"
]
}

重启docker

1
systemctl restart docker

验证

我们可以直接pull一个镜像看看逻辑卷使用会不会改变。

1
docker pull centos


在次查看发现使用率提高了

扩容
按照传统的lvm扩容的方式扩容就可以了,假设我添加一个100G的sdc设备
先创建pv

1
pvcreate /dev/sdc

扩容vg

1
vgextend docker /dev/sdc

扩容thin-pool

1
lvextend -l+100%FREE -n docker/thinpool

检查

当启动一个容器时

1
docker run -itd centos

就生成了对应的块设备

进入对应目录可以看见运行的容器对应的rootfs

参考链接:
http://blog.csdn.net/M2l0ZgSsVc7r69eFdTj/article/details/78153724
http://blog.csdn.net/vchy_zhao/article/details/70238690
http://blog.csdn.net/a85880819/article/details/52448654
https://docs.docker.com/engine/userguide/storagedriver/selectadriver/#related-information