docker之文件系统

docker的文件系统演进了很多代了。
从aufs,devicemapper,overlay,overlay2不一而足。

但是分层的思想是一致的。

aufs本质上一种Union File
System。就是把多个目录合并到一起。但是默认只有参数第一个的第一个目录是可读写的,当然你可以都设置为可读写。

耗子叔写了一篇很好的说明aufs的文章。
https://coolshell.cn/articles/17061.html

但是aufs没有被接纳进Linux主干分支,据说Linus觉得Junjiro Okajima(岡島順治郎)的代码实在写的太烂了。
https://en.wikipedia.org/wiki/Aufs 居然连wiki里都写了。

我在想等Linus百年以后,这个到底会谁说了算。不过这个世界的政治进程就已经告诉我们答案了。

这种分层的好处在docker里可以很明显的体现出来,就是系统的磁盘空间会比较节省,因为依赖的相同image会被共用。由于这些共用的image都是只读的,所以也不存在争抢的说法。

那么问题来了,aufs最多可以连接多少层呢?

https://stackoverflow.com/questions/39382518/whats-the-reason-for-the-42-layer-limit-in-docker
https://github.com/docker/docker.github.io/issues/8230

这里告诉我们docker在旧版的情况下是最多42层,而现在是127层,这个就是由于Linux的参数限制导致的,并没有更多的原因了。

正是没有进入到Linux主干的原因,所以aufs现在基本都是在ubuntu上才会被使用,而centos这些系统早期还是以device mapper为主。

下面这个链接着重将了device mapper的一些特点
https://www.infoq.cn/article/analysis-of-docker-file-system-aufs-and-devicemapper

这个东西虽然进入到了内核中,但是各种实现是相当的复杂,首先需要了解各种概念。

Snapshot
当一个snapshot创建的时候,仅拷贝原始卷里数据的元数据(meta- data)。创建的时候,并不会有数据的物理拷贝,因此snapshot的创建几乎是实时的,
当原始卷上有写操作执行时,snapshot跟踪原始卷块的改变,这个时候原始卷上将要改变的数据在改变之前被拷贝到snapshot预留的空间里,因此这个原理的实现叫做写时复制(copy-on- write)。

在写操作写入块之前,CoW(Copy on Write)将原始数据移动到snapshot空间里,这样就保证了所有的数据在snapshot创建时保持一致。
而对于snapshot的读操作,如果是读取数据块是没有修改过的,那么会将读操作直接重定向到原始卷上,如果是要读取已经修改过的块,那么就读取拷贝到snapshot中的块。

  1. 当 the origin 内容发生变化时,snapshot 对变化的部分做一个拷贝以用来对 the origin 进行重构。
  2. 因为只对变化的部分做拷贝,所以 Lvm 的 Snapshot 在读操作频繁而写操作不频繁的情况下占用很少的一部分空间便能完成特定任务。
  3. 当 Snapshot 大小耗尽或者远大于实际需求时,我们可以对其大小进行调节。
  4. 当对 Snapshot 的数据进行写操作的时候,Snapshot 实施相应操作,并丢弃从 the origin 的拷贝,以后的操作以写操作之后 Snapshot 中的数据为准。
  5. 在某些发行版的 Linux 系统下,可以使用 lvconvert 的 –merge 选项将 Snapshot 合并回 the origin。

Copy on Write概念的理解可以大概参考下redis里bgsave是怎么实现,fork()出来一个完全独立的进程,然后把这个独立进程里的内存数据写入到磁盘文件上。

fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。
当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。
中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。

Thinly-Provisioned Snapshot
这个其实虚拟磁盘里的一个概念。比如你现在要创建一个虚拟机,需要分配500G空间,但是初始的时候可能就10G,那如果使用Thinly-provisioned的话就只需要复制10G,而不需要一下子就复制500G,那样效率什么都非常低。
现在就是把Thinly-Provisioned和snatpshot结合起来:

可以将不同的 snaptshot 挂载到同一个 the origin 上,节省了磁盘空间。
当多个 Snapshot 挂载到了同一个 the origin 上,并在 the origin 上发生写操作时,将会触发 CoW 操作。这样不会降低效率。
Thin-Provisioning Snapshot 支持递归操作,即一个 Snapshot 可以作为另一个 Snapshot 的 the origin,且没有深度限制。(这个比aufs强,但是实际使用意义暂时不大)
在 Snapshot 上可以创建一个逻辑卷,这个逻辑卷在实际写操作(CoW,Snapshot 写操作)发生之前是不占用磁盘空间的。

Thin-Provisioning Snapshot 是作为 device mapper 的一个 target 在内核中实现的。Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制。在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略,如条带化,镜像,快照等。

如果仅此来看的话,device mapper实现的简洁性跟aufs没法比啊,绕了很多的圈子。

于是后面overlayFS和overlayFS2就出来了。这2个可以作为aufs的继承了,并且进入了内核。分别为3.18和4.0
https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt
https://arkingc.github.io/2017/05/05/2017-05-05-docker-filesystem-overlay/

看看基础的命令

1
mount -t overlay overlay -olowerdir=/lower,upperdir=/upper,workdir=/work /merged

看着跟aufs挺相像,这里分为了lowdir和upperdir这2个概念。

这个图是不是看着跟aufs的有点类似啊。

当容器层和镜像层拥有相同的文件时,容器层的文件可见,隐藏了镜像层相同的文件。容器挂载目录(merged)提供了统一视图

overlay只使用2层,意味着多层镜像不会被实现为多个OverlayFS层。每个镜像被实现为自己的目录,这个目录默认路径在/var/lib/docker/overlay下。硬链接被用来索引和低层共享的数据,节省了空间

现在当我们启动docker直接用df就可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/vda1 296G 153G 128G 55% /
devtmpfs 16G 0 16G 0% /dev
tmpfs 16G 0 16G 0% /dev/shm
tmpfs 16G 620K 16G 1% /run
tmpfs 16G 0 16G 0% /sys/fs/cgroup
tmpfs 3.2G 0 3.2G 0% /run/user/995
overlay 296G 153G 128G 55% /data/docker/overlay2/1887dcd0ee37acd5286edd2df6052ddd296b905f081653823177e4c7b990affa/merged
overlay 296G 153G 128G 55% /data/docker/overlay2/c1813bc772fa050245b2de7f73a1ea072079f1c23039133ed997207af68783ed/merged
overlay 296G 153G 128G 55% /data/docker/overlay2/8ca5dcf2da63092651e91edeb17a8cadc4822c42fc20165de62e92a6024da809/merged
overlay 296G 153G 128G 55% /data/docker/overlay2/7c9dc6f9ee609cf81afdbdcfb1bae2cb3c56df003e54ef2a0760274b9829215b/merged
shm 64M 0 64M 0% /data/docker/containers/39fb5b1bd0c009419b367b377948d30de7405907f1c025fb8f08f9812279929f/mounts/shm
shm 64M 0 64M 0% /data/docker/containers/bde1211481b7afb91d9e27efa806b8b7fe566c935fa55b23d8672e35b669e7f2/mounts/shm
shm 64M 0 64M 0% /data/docker/containers/10255f80dbf230c36f592df94a0c9d138177392627c9b553bf5eb96a2abcaa04/mounts/shm
shm 64M 0 64M 0% /data/docker/containers/30523a9e92b26da8fdd2bb8a34866378874e97620e0f4c27284c8d7add19903c/mounts/shm
overlay 296G 153G 128G 55% /data/docker/overlay2/b2890455e33109b25e924960dc19a327850175caddcf6fd6aef7120e84f8ca13/merged
shm 64M 0 64M 0% /data/docker/containers/2d6dac84d96c5d80cfe1e991b3e0f692c3564aac6ec0fc3e46dfe486cd5d8115/mounts/shm
tmpfs 3.2G 0 3.2G 0% /run/user/1000

http://people.redhat.com/vgoyal/papers-presentations/vault-2017/vivek-overlayfs-and-containers-presentation-valult-2017.pdf
https://blog.csdn.net/luckyapple1028/article/details/77916194

这个overlay最大的问题应该是:

Overlayfs的lower layer文件写时复制机制让某一个用户在修改来自lower层的文件不会影响到其他用户(容器),但是这个文件的复制动作会显得比较慢,后面我们会看到为了保证文件系统的一致性,这个copy-up实现包含了很多步骤,其中最为耗时的就是文件数据块的复制和fsync同步。用户在修改文件时,如果文件较小那可能不一定能够感受出来,但是当文件比较大或一次对大量的小文件进行修改,那耗时将非常可观。虽然自Linux-4.11起内核引入了“concurrent copy up”特性来提高copy-up的并行性,但是对于大文件也还是没有明显的效果。不过幸运的是,如果底层的文件系统支持reflink这样的延时拷贝技术(例如xfs)那就不存在这个问题了

我们考虑下这个问题是不是在aufs中存在呢?