什么是联合文件系统
联合挂载是一种文件系统,它可以在不修改其原始(物理)源的情况下创建多个目录,并把内容合并为一个文件的错觉。这可能很有用,因为我们可能将相关文件集存储在不同位置中,但我们希望在单个合并视图中显示它们。例如,来自远程 NFS 服务器的/home 主目录全部联合到一个目录中,或者将分割的 ISO 镜像合并到一个完整的目录中。
联合挂载或联合文件系统是文件系统;但不是文件系统类型,而是一个包含许多实现的概念。其中一些速度更快性能更好,一些更简单,有不同的使用场景或不同的成熟度。因此,在我们开始深入了解细节之前,让我们快速浏览一下常见文件系统的实现:
- UnionFS - 让我们从原始的联合文件系统开始。UnionFS 似乎不再积极开发,其最新提交是从 2014 年 8 月开始的。您可以在其网站https://unionfs.filesystems.org/上阅读更多有关它的信息。
- aufs - 原始 UnionFS 的重新实现,添加了许多新功能,但因合并到主线 Linux 内核而被拒绝。Aufs 是 Ubuntu/Debian 上 Docker 的默认驱动程序,但被 OverlayFS 取代(对于 Linux 内核 >4.0)。与 Docker 文档页面中描述的其他联合文件系统相比,它具有一些优势。
- OverlayFS - 接下来是 OverlayFS,自 3.18(2014 年 10 月 26 日)起包含在 Linux 内核中。这是默认 overlay2Docker 驱动程序使用的文件系统(您可以使用 验证 docker system info | grep Storage)。它通常比 aufs 具有更好的性能,并且具有一些不错的功能,例如页面缓存共享。
- ZFS - ZFS 是由 Sun Microsystems(现在是 Oracle)创建的联合文件系统。它有一些有趣的功能,如分层校验和、快照和备份/复制的本机处理或本机数据压缩和重复数据删除。但是,由 Oracle 维护,它具有非 OSS 友好许可 (CDDL),因此不能作为 Linux 内核的一部分提供。但是,您可以在 Linux (ZoL)项目上使用 ZFS,Docker 文档中将其描述为健康和成熟的…,但尚未准备好用于生产。如果你想尝试一下,那么你可以在这里找到它。
- Btrfs - 另一种选择是 Btrfs,它是多家公司(包括 SUSE、WD 或 Facebook )的联合项目,在 GPL 许可下发布,是 Linux 内核的一部分。Btrfs 是 Fedora 33 的默认文件系统。它还具有一些有用的功能,例如块级操作、碎片整理、可写快照等等。如果您真的想解决为 Docker 切换到非默认存储驱动程序的麻烦,那么具有其功能和性能的 Btrfs 可能是您要走的路。
Docker 镜像、容器的基石——联合文件系统(UnionFS)
假设Dockerfile 内容如下
1 2 3 4
| FROM ubuntu:14.04 ADD run.sh / VOLUME /data CMD ["./run.sh"]|
|
联合文件系统对应的层次结构如下图所示

Docker 文件系统(图片来源于网络).png
- FROM ubuntu:14.04 :设置基础镜像,此时会使用基础镜像ubuntu:14.04的所* 有镜像层,为简单起见,图中将其作为一个整体展示。
- ADD run.sh /:将Dockerfile所在目录的文件run.sh加至镜像的根目录,此时新一层的镜像只有一项内容,即根目录下的run.sh.
- VOLUME /data:设定镜像的VOLUME,此VOLUME在容器内部的路径为/data。需要注意的是,此时并未在新一层的镜像中添加任何文件,但更新了镜像的json文件,以便通过此镜像启动容器时获取这方面的信息。
- CMD [“./run.sh”]:设置镜像的默认执行入口,此命令同样不会在新建镜像中添加任何文件,仅仅在上一层镜像json文件的基础上更新新建镜像的json文件。
图中的顶上两层,是Docker为Docker容器新建的内容,而这两层属于容器范畴。 这两层分别为Docker容器的初始层(Init Layer)与可读写层(Read-Write Layer)。
- 初始层: 大多是初始化容器环境时,与容器相关的环境信息,如容器主机名,主机host信息以及域名服务文件等。
- 读写层: Docker容器内的进程只对可读写层拥有写权限,其他层对进程而言都是只读的(Read-Only)。 另外,关于VOLUME以及容器的hosts、hostname、resolv.conf文件等都会挂载到这里。
为什么
为什么 docker 等容器系统要使用类似的联合文件系统呢?
我们用来启动容器的许多镜像无论 ubuntu 是 72MB 还是 nginx 133MB 的大小都非常庞大。每次我们想从这些镜像创建一个容器时,分配这么多空间将是非常昂贵的。有了联合文件系统,Docker 只需要在镜像之上创建一个瘦文件层,其余的可以在所有容器之间共享。这还提供了减少启动时间的额外好处,因为无需复制镜像文件和数据。
联合文件系统还提供隔离功能,因为容器对共享镜像层具有只读访问权限。如果他们需要修改任何只读共享文件,他们会使用写时复制策略将内容复制到可以安全修改的可写层。
overlay是如何工作的
https://jishuin.proginn.com/p/763bfbd61dab
aufs是如何工作的
-
读取文件
当我们在容器中读取文件时,可能会有以下场景。
- 文件在容器层中存在时:当文件存在于容器层时,直接从容器层读取。
- 当文件在容器层中不存在时:当容器运行时需要读取某个文件,如果容器层中不存在时,则从镜像层查找该文件,然后读取文件内容。
- 文件既存在于镜像层,又存在于容器层:当我们读取的文件既存在于镜像层,又存在于容器层时,将会从容器层读取该文件。
-
修改文件或目录
AUFS 对文件的修改采用的是写时复制的工作机制,这种工作机制可以最大程度节省存储空间。
具体的文件操作机制如下。
- 删除文件或目录:当文件或目录被删除时,AUFS 并不会真正从镜像中删除它,因为镜像层是只读的,AUFS 会创建一个特殊的文件或文件夹,这种特殊的文件或文件夹会阻止容器的访问。
演示
构建目录和文件
首先在 /tmp 目录下创建 aufs 目录:
准备挂载点目录:
接下来准备容器层内容:
1 2 3 4
| # $ mkdir container1 # $ echo Hello, Container layer! > container1/container1.txt
|
最后准备镜像层内容:
1 2 3 4 5
| # $ mkdir image1 && mkdir image2 # $ echo Hello, Image layer1! > image1/image1.txt $ echo Hello, Image layer2! > image2/image2.txt
|
准备好的目录和文件结构如下:
1 2 3 4 5 6 7 8 9 10
| $ tree . . |-- container1 | `-- container1.txt |-- image1 | `-- image1.txt |-- image2 | `-- image2.txt `-- mnt 4 directories, 3 files
|
创建AUFS联合文件系统
在使用aufs之前,可以通过下面的命令确认当前系统是否支持aufs,如果不支持,请自行根据相应发行版的文档安装
1 2 3 4 5
| #下面的命令如果没有输出,表示内核不支持aufs #由于ubuntu 16.04的内核中已经将aufs编译进去了,所以默认就支持 root@ubuntu:~$ grep aufs /proc/filesystems nodev aufs #这里nodev表示该文件系统不需要建在设备上
|
使用 mount 命令可以创建 AUFS 类型的文件系统,命令如下:
1
| $ sudo mount -t aufs -o dirs=./container1:./image2:./image1 none ./mnt
|
-t aufs: 指定挂载类型为aufs
-o dirs=./container1:./image2:./image1 : 表示将当前目录下的container1,image2,image1三个文件夹联合到一起。这里要注意,dirs 参数第一个冒号默认为读写权限,后面的目录均为只读权限,与 Docker 容器使用 AUFS 的模式一致。
none:aufs不需要设备,只依赖于-o dir指定的文件夹,所以这里填none即可
./mnt:表示将最后联合的结果挂载到当前的mnt目录下,然后我们就可以往这个目录里面读写文件了
执行完上述命令后,mnt 变成了 AUFS 的联合挂载目录,我们可以使用 mount 命令查看一下已经创建的 AUFS 文件系统:
1 2
| $ mount -t aufs none on /tmp/aufs/mnt type aufs (rw,relatime,si=9553b4ca12858995)
|
我们每创建一个 AUFS 文件系统,AUFS 都会为我们生成一个 ID,这个 ID 在 /sys/fs/aufs/ 会创建对应的目录,在这个 ID 的目录下可以查看文件挂载的权限。
1 2 3 4 5 6 7 8
| $ cat /sys/fs/aufs/si_9553b4ca12858995/* /tmp/aufs/container1=rw /tmp/aufs/image2=ro /tmp/aufs/image1=ro 64 65 66 /tmp/aufs/container1/.aufs.xino
|
可以看到 container1 目录的权限为 rw(代表可读写),image1 和 image2 的权限为 ro(代表只读)。
为了验证 mnt 目录下可以看到 container1、image1 和 image2 目录下的所有内容,我们使用 ls 命令查看一下 mnt 目录:
1 2 3 4 5
| $ ls -l mnt total 12 -rw-r--r-- 1 root root 24 May 11 07:56 container1.txt -rw-r--r-- 1 root root 21 May 11 07:56 image1.txt -rw-r--r-- 1 root root 21 May 11 07:56 image2.txt
|
可以看到 mnt 目录下已经出现了我们准备的所有镜像层和容器层的文件。下面让我们来验证一下 AUFS 的写时复制。
aufs写时复制
AUFS 的写时复制是指在容器中,只有需要修改某个文件时,才会把文件从镜像层复制到容器层,下面我们通过修改联合挂载目录 mnt 下的内容来验证下这个过程。
我们使用以下命令修改 mnt 目录下的 image1.txt 文件:
1
| $ echo Hello, Image layer1 changed! > mnt/image1.txt
|
然后我们查看下 image1/image1.txt 文件内容:
1 2
| $ cat image1/image1.txt Hello, Image layer1!
|
发现“镜像层”的 image1.txt 文件并未被修改。
然后我们查看一下"容器层"对应的 image1.txt 文件内容:
1 2 3 4 5 6 7
| $ ls -l container1/ total 8 -rw-r--r-- 1 root root 24 May 11 07:56 container1.txt -rw-r--r-- 1 root root 29 May 11 08:32 image1.txt # $ cat container1/image1.txt Hello, Image layer1 changed!
|
发现 AUFS 在“容器层”自动创建了 image1.txt 文件,并且内容为我们刚才写入的内容。
至此,我们完成了 AUFS 写时复制的验证。我们在第一次修改镜像内某个文件时,AUFS 会复制这个文件到容器层,然后在容器层对该文件进行修改操作,这就是 AUFS 最典型的特性写时复制。
Docker 如何使用 OverlayFS
为了演示 Docker 如何使用 OverlayFS,我们将尝试模拟 Docker 如何装载容器和镜像层。在执行此操作之前,我们首先需要清理工作区并获得一个镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ~ $ docker image prune -af ... Total reclaimed space: ...MB ~ $ docker pull nginx Using default tag: latest latest: Pulling from library/nginx a076a628af6f: Pull complete 0732ab25fa22: Pull complete d7f36f6fe38f: Pull complete f72584a26f32: Pull complete 7125e4df9063: Pull complete Digest: sha256:10b8cc432d56da8b61b070f4c7d2543a9ed17c2b23010b43af434fd40e2ca4aa Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest
|
我们有一个镜像(nginx)可以测试,所以接下来,让我们检查它的镜像层。我们可以通过 docker inspect 在镜像上运行并检查 GraphDriver 字段或通过浏览/var/lib/docker/overlay2 存储所有镜像层的目录来检查镜像层。最后看看里面有什么:
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 29 30
| ~ $ cd /var/lib/docker/overlay2 ~ $ ls -l total 0 drwx------. 4 root root 55 Feb 6 19:19 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd drwx------. 3 root root 47 Feb 6 19:19 410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46 drwx------. 4 root root 72 Feb 6 19:19 685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e brw-------. 1 root root 253, 0 Jan 31 18:15 backingFsBlockDev drwx------. 4 root root 72 Feb 6 19:19 d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e drwx------. 4 root root 72 Feb 6 19:19 fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505 drwx------. 2 root root 176 Feb 6 19:19 l
~ $ tree 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/ 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/ ├── diff │ └── docker-entrypoint.d │ └── 20-envsubst-on-templates.sh ├── link ├── lower └── work
~ $ docker inspect nginx | jq .[0].GraphDriver.Data { "LowerDir": "/var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff: /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff: /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff: /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff", "MergedDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/merged", "UpperDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff", "WorkDir": "/var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/work" }
|
查看上面的输出,它看起来与我们在 mount 命令中看到的非常相似,进一步来说:
-
LowerDir: 是只读镜像层的目录,以冒号分隔
-
MergedDir:镜像和容器中所有图层的合并视图
-
UpperDir:写入更改的读写层
-
WorkDir:Linux OverlayFS 用于准备合并视图的工作目录
接下来,让我们更进一步,运行一个容器并检查它的层:
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 29 30
| ~ $ docker run -d --name container nginx ~ $ docker inspect container | jq .[0].GraphDriver.Data { "LowerDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff: /var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff: /var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff: /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff: /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff: /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff", "MergedDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/merged", "UpperDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff", "WorkDir": "/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work" }
~ $ tree -l 3 /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff # The UpperDir /var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff ├── etc │ └── nginx │ └── conf.d │ └── default.conf ├── run │ └── nginx.pid └── var └── cache └── nginx ├── client_temp ├── fastcgi_temp ├── proxy_temp ├── scgi_temp └── uwsgi_temp
|
上面的输出显示 docker inspect nginx 的输出中列出的目录与 MergedDir、UpperDir 和 WorkDir(id 为 3d963d191b2101b3406348217f4257d7374aa4b4a73b4b4a6dd4ab0f365d38dfbd)相同,现在是容器的 LowerDir 的一部分。这里的下半部分是由所有的 nginx 镜像层叠加在一起组成的。在它们之上是 UpperDir 中的可写层,它包含/etc、/run 和/var。同样,如果我们在上面列出 MergedDir,您将看到容器可以使用的整个文件系统,包括 UpperDir 和 LowerDir 中的所有内容。
最后,为了模拟 Docker 的行为,我们可以使用这些相同的目录来手动创建我们自己的合并视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ~ $ mount -t overlay -o \ lowerdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4-init/diff: /var/lib/docker/overlay2/3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd/diff: /var/lib/docker/overlay2/fb18be50518ec9b37faf229f254bbb454f7663f1c9c45af9f272829172015505/diff: /var/lib/docker/overlay2/d487622ece100972afba76fda13f56029dec5ec26ffcf552191f6241e05cab7e/diff: /var/lib/docker/overlay2/685374e39a6aac7a346963bb51e2fc7b9f5e2bdbb5eac6c76ccdaef807abc25e/diff: /var/lib/docker/overlay2/410c05aaa30dd006fc47d8c23ba0d173c6d305e4d93fdc3d9abcad9e78862b46/diff,\ upperdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/diff,\ workdir=/var/lib/docker/overlay2/59bcd145c580de3bb3b2b9c6102e4d52d0ddd1ed598e742b3a0e13e261ee6eb4/work \ overlay /mnt/merged
~ $ ls /mnt/merged bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var boot docker-entrypoint.d etc lib media opt root sbin sys usr
~ $ umount overlay
|
在这里,我们只是从前面的代码片段中获取值并将它们传递给 mount 命令中的适当参数,唯一的区别是我们用于/mnt/merged 合并视图而不是/var/lib/docker/overlay2/…/merged.
这就是 Docker 中整个 OverlayFS 的真正含义——mount 跨多个堆叠层的单个命令。下面是负责这个的 Docker 代码的一部分 -lowerdir=…,upperdir=…,workdir=…值的替换,然后是 unix.Mount
1 2 3 4 5 6 7 8
| // https://github.com/moby/moby/blob/1ef1cc8388165b2b848f9b3f53ec91c87de09f63/daemon/graphdriver/overlay2/overlay.go#L580 opts := fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", strings.Join(absLowers, ":"), path.Join(dir, "diff"), path.Join(dir, "work")) mountData := label.FormatMountLabel(opts, mountLabel) mount := unix.Mount mountTarget := mergedDir
rootUID, rootGID, err := idtools.GetRootUIDGID(d.uidMaps, d.gidMaps) // ...
|
Docker 核心组件之 联合文件系统
https://jishuin.proginn.com/p/763bfbd61dab
https://www.jianshu.com/p/5ec3d4dbf580
Linux文件系统之aufs