前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >容器底层-UnionFS 工作原理-AUFS 和 Docker 实现

容器底层-UnionFS 工作原理-AUFS 和 Docker 实现

作者头像
syy
发布2021-01-05 11:03:26
3K0
发布2021-01-05 11:03:26
举报
文章被收录于专栏:多选参数多选参数

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 目录。

代码语言:javascript
复制
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 这个文件才会被改变。

代码语言:javascript
复制
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,结果如下图所示

代码语言:javascript
复制
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 这个镜像文件,输出了以下内容。

代码语言:javascript
复制
"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 文件系统)的内容几乎一致吧。

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

代码语言:javascript
复制
$ 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 目录

代码语言:javascript
复制
$ 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

代码语言:javascript
复制
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。而这种增量的方式加上共享之后,就可以使得多个容器镜像真正需要的总空间,比每个镜像的总和要小。同时在拉取或者推送的时候,假如已经有相应的层了,那么这个也不会被拉取或者推送了。

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

巨人的肩膀

参考资料

  1. 极客时间-《深入剖析 Kubernetes》-张磊老师."白话容器基础(三):深入理解容器镜像"
  2. DOCKER基础技术:AUFS
  3. Docker笔记(一)- 镜像与容器,Overlay2
  4. 把玩overlay文件系统

推荐链接

  1. Union file systems: Implementations, part I
  2. Union file systems: Implementations, part 2
  3. Another union filesystem approach
  4. Unioning file systems: Architecture, features, and design choices
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 多选参数 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • AUFS
    • AUFS 示例
      • AUFS 特性
        • AUFS 性能
        • Docker 实现
          • 总结
          • 容器镜像总结
          • 巨人的肩膀
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档