容器 | UnionFS 工作原理-AUFS 概念和 Docker 实现

AUFS 概念

AUFS 是一种 Union File System,Union File System 就是把不同物理位置的目录合并 mount 到同一个目录中。比如可以把一张 CD/DVD 和一个硬盘目录给联合 mount 在一起,然后就可以对只读的 CD/DVD 上的文件进行修改,当然修改的文件是存于硬盘上的目录里。

AUFS 一开始叫 Another UnionFS,后来又叫 Alternative UnionFS,最后直接改为 Advance UnionFS。它是对 Linux 原生 UnionFS 的重写和改进。但是无论怎么改,它就是进不了 Linux 的主线。但是,我们可以在 Ubuntu 和 Debian 这些发行版上使用它。

AUFS 示例

那么 AUFS 的效果到底是怎么样的呢?下面根据耗子叔博客中的例子来演示一下。

  • 首先我们建立两个目录 ./fruits./vegetables,并在目录中放入一些文件。

  • 之后我们以 AUFS 的方式将这两个目录同时 mount 到 ./mnt 目录中。我们可以看到 ./mnt 目录下有三个文件:apple、carrots、tomato,相当于 ./fruits./vegetables 这两个目录被 union 到了 ./mnt 目录。

    1
    mount -t aufs -o dirs=./fruits:./vegetables none ./mnt/

  • 接下去我们修改 ./mnt/apple 这个文件的内容,可以看到 ./fruits/apple 的内容也被修改了。

  • 再接下去我们修改 ./mnt/carrots 这个文件的内容,但是我们可以看到 ./vegetables/carrots 文件的内容并没有改变,反而是 ./fruits 目录中多出了 carrots 文件,这个内容跟我们修改的内容是一样。

    这个主要是因为在 mount aufs 命令中,我们并没有指定 fruits 和 vegetables 的目录权限。那么,默认上来说,命令行第一个(最左边)的目录是可读可写的,后面的都是可读的。

    假设修改一开始的 mount aufs 命令如下,那么上述修改 ./mnt/carrots 文件时, ./vegetables/carrots 这个文件才会被改变。

    1
    mount -t aufs -o dirs=./fruits=rw:./vegetables=rw none ./mnt
  • 假如我上面的两个目录都配置为可读可写,那修改 ./mnt/tomato 这个文件的内容,影响到的其实是 ./fruits 这个目录下的。可见如果有重复的文件名,在 mount 命令中,越前面的目录中的文件的优先级就越高,也就是会被先改。

AUFS 特性

上述只阐述了简单的例子。实际上,AUFS 有所有 UnionFS 的特性,它可以把多个目录合并成同一个目录;并且为每个需要合并的目录指定相应的权限;实时地添加、删除、修改已经被 mount 好的目录;还能在多个可写的目录(分支)间实现负载均衡。

AUFS 中称要被 union 进来的目录为 Branch(也就是使用 mount 命令时 dirs 参数指定的目录),Branch 会根据 union 的顺序形成一个 stack,一般最上面的是可写的,下面是只读的。Branch stack 还是可以进行修改,比如修改顺序,加入新的 branch,或者删除其中的 branch,或者直接修改 branch 的权限。

AUFS 中被 Union 的目录(分支)有以下这些权限:

  • rw 表示可读可写 read-write

  • ro 表示只读 read-only,那么对于 ro 目录来说,是永远不会收到写操作的,也不会收到查找 whiteout 的操作。

  • rr 表示 read-read-only,与 read-only 不同的是,rr 标记的是天生就是只读的目录。这样一来, AUFS 可以提高性能,比如不再设置 inotify 来检查文件变动的通知。

  • wh 表示 whiteout,它通常和 ro 一起使用,比如 [dir]=ro+wh

    whiteout 主要用于隐藏底层分支的文件。当 union 中要删除的某个文件实际上位于 read-only 的目录上的,由于在 read-only 上我们无法做任何的修改,此时我们就可以对这个 read-only 目录里的文件做 whiteout。具体做法就是在可写的目录中创建对应的 whiteout 隐藏文件来实现,比如 demo 这个文件位于 read-only 目录中,那么要 union 的目录中要删除这个文件了,那么就在可写的目录中创建一个名为 .wh.demo 的文件即可。除此之外,whiteout 还可以用于阻止 readdir 进入低层分支,此时的名字应该是 .wh..wh..opq 或者 .wh.__dir_opaque

    假设我们有三个目录,它们的情况如下所示

    接下去对这三个目录进行 mount,结果如下图所示

    1
    mount -t aufs -o dirs=./test=rw:./fruits=ro:./vegetables=ro none ./mnt

    之后,我们在 test 目录中创建一个 whiteout 的隐藏文件 .wh.apple,当查看 ./mnt 目录时,你会发现 该目录下的 apple 这个文件已经消失了。这个效果等同于 rm ./mnt/apple

除此之外,AUFS 还有以下这些特性:

  • 被 mount 到同一个目录下的文件,如果在原来的地方被修改了,那么 union 之后的目录中的内容会改变吗?这个主要看用户对 udba 的参数设置:
    • udba=none,那些不在 union 之后的目录里发生的修改,aufs 是不会同步的,所以此时会有数据出错的问题,但是 AUFS 运转很快。
    • udba=reval,AUFS 会检查有没有被修改,如果有的话,那么把修改 mount 到目录内
    • udba=notify,AUFS 会为所有的目录注册 inotify,这样可以让 AUFS 在更新文件修改的性能更高一些
  • 如果有多个 rw 的目录被 union 进来了,那么当创建文件时,aufs 会将这个文件创建在哪个 rw 目录中呢?这个主要是看 create 参数的设置:
    • create=rr (round-robin),新创建的文件轮流写到每个 rw 目录中,使用方式为:mount -t aufs -o dirs=./1=rw:./2=rw:./3=rw -o create=rr none ./mnt
    • create=mfs[:second](most−free−space[:second]),选一个可用空间最好的分支。
    • create=mfsrr:low[:second],选一个空间大于 low 的目录,如果空间小于 low 了,那么 aufs 会使用 round-robin 方式。

更加具体得请 man aufs

AUFS 性能

性能上,AUFS 在查找文件上是比较慢的,因为要遍历所有的 branch。但是,一旦 AUFS 找到要读写的文件之后,因为有了这个文件 inode 所以之后的读写和操作和原文件基本是一样了的。

Docker 实现

Docker 的镜像就采用了 UnionFS 技术,从而实现了分层的镜像。早期的 Ubunt 使用的是 AUFS,但是在较新的 Ubuntu 发行版中 Docker 采用的文件驱动是 overlay2。Overlay 和 AUFS 还有 DeviceMapper 都是 UnionFS,在细节上会有所不同,但是不影响理解。

所谓镜像其实就是文件系统,也就是一些目录和文件的组合。相关的镜像文件可以在 /var/lib/docker/overlay2 中看到。下面我们使用 docker inspect 这个命令来查看 ubuntu 这个镜像文件,输出了以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/0da6195c221c2cc469d542dc0977a4e820af1acecdee826e33327d180952523f/diff:/var/lib/docker/overlay2/a318ce550c10b22bc71ca5a382c82b70a3a808c2ec48740f13307c183c460ee1/diff",
"MergedDir": "/var/lib/docker/overlay2/6bd4e397e20f9e9e78f84fce335494daf7f903f9f95b917891c4493f7568d6bb/merged",
"UpperDir": "/var/lib/docker/overlay2/6bd4e397e20f9e9e78f84fce335494daf7f903f9f95b917891c4493f7568d6bb/diff",
"WorkDir": "/var/lib/docker/overlay2/6bd4e397e20f9e9e78f84fce335494daf7f903f9f95b917891c4493f7568d6bb/work"
},
"Name": "overlay2"
},

"RootFS": {
"Type": "layers",
"Layers": [
"sha256:d42a4fdf4b2ae8662ff2ca1b695eae571c652a62973c1beb81a296a4f4263d92",
"sha256:90ac32a0d9ab11e7745283f3051e990054616d631812ac63e324c1a36d2677f5",
"sha256:782f5f011ddaf2a0bfd38cc2ccabd634095d6e35c8034302d788423f486bb177"
]
},

可以看到 ubunut 这个镜像由三层镜像层组成,这些镜像层都位于 /var/lib/docker/overlay2 目录中。该目录中的 l 文件夹存放着到各层 diff 目录的软链接。

那么这三层镜像层的内容如下图所示,每一层镜像层都是 Ubuntu 操作系统文件与目录的一部分。每层镜像层的文件具体存放在 diff 子目录下,这些文件被组织起来之后只能是只读的;link 文件描述了该层标识符的精简版;lower 文件描述了层序的组织关系。其中 a318ce.../diff 子目录的内容跟 Ubuntu 的文件系统(Linux 文件系统)的内容几乎一致吧。

接下来,我们来看一下这些文件到底是如何被组织起来的。我们先启动一个容器

1
$ docker run -it --rm ubuntu

可以看到 /var/lib/docker/overlay2 目录多出了两个目录 d00891...d00891...-init,这两个目录是启动容器之后生成的。

其中 d00891...-init 的内容如下所示,diff/etc 子目录包含了 hostname、hosts、resolv.conf 等文件。

d00891... 目录的内容如下所示,发现这层镜像 merged 子目录的内容跟 ubuntu 文件系统的组织几乎一致。

下面,我们进入容器内部,查看一下容器的 mount 情况。可以看到 overlay2 将 lowerdir、upperdir、workdir 联合挂载。其中 lowerdir 是容器启动之后的只读层;upperdir 是容器的可读可写层;workdir 是 lowerdir 执行 copy_up 操作的中转层,copy_up 操作是指当要修改的文件不存在于 upperdir 而仅存在于 lower 时,要先将数据从 lower 拷贝到 upper 的这个操作。

AUFS 中会指明每个目录的权限,那么在 Overlay2 会找不到相关的目录权限(我是没找到),那么这个应该是 Overlay2 的规定,也就是说在采用 Overlay2 之后,如果是 lowerdir 中的目录,那么就是只读,如果是 upperdir 中的目录那么就是可读可写。

我们将这个挂载情况和上面的链接情况对照起来,可以发现 lowerdir 包含了 d00891...-init/diff6bd4e3.../diif0da619.../diffa318ce.../diff 这四个目录;upperdir 则包含 d008919.../diff 目录;workdir 则包含 d008919.../work 目录。

那么,再结合前面看到的 d00891.../merged 目录,那么相当于 Docker 把 lowerdir、upperdir、workdir 涉及到的目录都以联合文件的方式挂载到了 d00891.../merged 目录

1
2
$ mount |grep 'overlay'
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/KQNKITRZAH2XGWS6CFE4ZPX7IN:/var/lib/docker/overlay2/l/2PH5HBCMRLYXSGO5YTKS7LK5I2:/var/lib/docker/overlay2/l/5QZ3LNRWDEOYCV7VO4QGJZ55NW:/var/lib/docker/overlay2/l/5O6GSST3GYTXRPKJZCZ4AZ4GF6,upperdir=/var/lib/docker/overlay2/d008919d5201db7980cba9b1ba8dd2908be58b396ca09d3b2cc98272a541154e/diff,workdir=/var/lib/docker/overlay2/d008919d5201db7980cba9b1ba8dd2908be58b396ca09d3b2cc98272a541154e/work)

接下去我们做个实验来看一下,先在容器内部创建一个文件 test.txt

1
root@b9585329155a:/# touch test.txt

之后我们分别查看 d00891.../mergedd00891.../diffa318ce.../diff 这三个目录的内容,发现在根目录创建的 test.txt 只在前两个存在,也就是只在用于挂载的目录和可读可写层存在。

总结

通过上述的方式,我们可以看到 Docker 对 overlay2 的使用其实和我们在 AUFS概念 中的使用是很像的,毕竟都是 UnionFS。Docker 相当于把 /var/lib/docker/overlay2 中相应的只读镜像层文件的 diff 目录、容器启动之后新建的只读 init 镜像层文件的 diff 目录(hostname、hosts、resolv.conf )和容器启动之后新建的可读可写镜像层文件的 diff 目录以 UnionFS 方式的挂载到新建的可读可写镜像层文件的 merged 目录。因此,容器启动之后的文件系统如下图所示:

  • 只读层是 Ubuntu 这个镜像的组成内容,这些只读层都以增量的方式包含了 Ubuntu 操作系统的一部分。
  • Init 层,位于只读层和可读可写层之间。这是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/hostname、/etc/resolv.conf 等信息。那么为什么需要这一层呢?因为这些文件本身是属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在容器时写入一些指定的值,比如 hostname,那么假如没有这一层的话,那么修改就会在可读可写层了。但是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 之后,这些信息也跟可读写层一起提交掉。所以 Docker 的做法是,在启动容器的时候,先修改这些值,然后以一个单独的层挂载出来(通过上面我们也可以看到这是单独的一层),并且设置为可读(这样容器使用的信息就是修改之后的了)。之后再创建一个层作为可读可写层,那么 docker commit 只会提交可读写层,但又不包括这些内容。
  • 可读写层是 rootfs 最上面的一层,专门用来存放修改只读层文件后产生的增量,增删改都发生在这里。之后我们可以还可以使用 docker commit 命令将这个可读写层的内容保存下来,改为只读层,然后更新镜像的信息。

而这三层最终被联合挂载到同一个目录下,之后再结合 mount namespace,那么就能为容器中的进程构建出一个完善的文件系统隔离环境(当然还得感谢 chroot 这个系统调用切换根目录的能力)。

我相信也能更好地理解网上对容器文件系统的阐述了(附上网上的一张图):最上层是可读可写的,而下层是镜像。

容器镜像总结

在基本介绍完容器的镜像之后,可以说说容器的另一个重要特性:一致性。

在 PaaS 时代,由于云端与本地服务器环境不同,应用的打包的过程是一个及其痛苦的过程。然而对于容器来说,有了容器镜像(rootfs)之后,这个问题显得就不再是大问题了。因为 rootfs 打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用及其所需要的所有依赖都被封装在 rootfs 中了(实际上,对一个应用来说,操作系统本身才是它运行时所需要的最完整的“依赖库”)。

有了容器镜像“打包操作系统”的能力,那么应用最基础的依赖环境也变成了应用沙盒的一部分。这就使得容器有了一致性:无论在本地、云端,还是在一台任何地方的机器上,应用程序只需要使用这个容器镜像,那么这个应用所需要的完整的执行环境就被重现出来了。这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。这种价值也是支撑 Docker 公司在 2014-2016 年间迅猛发展的核心动力。

另外, Docker 在镜像的设计中并没有沿用以前制作 rootfs 的标准流程,而是引入了层的概念。用户制作镜像的每一步操作,都会生成一个层,也就是增量的 rootfs。而这种增量的方式加上共享之后,就可以使得多个容器镜像真正需要的总空间,比每个镜像的总和要小。同时在拉取或者推送的时候,假如已经有相应的层了,那么这个也不会被拉取或者推送了。

容器镜像的发明不仅打通了“开发-测试-部署”流程的每一个环节,更重要的是:容器镜像将会成为未来软件的主流发布方式

巨人的肩膀

参考资料

极客时间—-《深入剖析 Kubernetes》—-张磊老师

DOCKER基础技术:AUFS

Docker笔记(一)- 镜像与容器,Overlay2

把玩overlay文件系统

推荐链接

Union file systems: Implementations, part I

Union file systems: Implementations, part 2

Another union filesystem approach

Unioning file systems: Architecture, features, and design choices

程序锅 wechat
欢迎关注微信公众号【一口程序锅】,不定期的技术分享、资源分享。
让我多买本书学习学习
0%