什么是联合文件系统

联合挂载是一种文件系统,它可以在不修改其原始(物理)源的情况下创建多个目录,并把内容合并为一个文件的错觉。这可能很有用,因为我们可能将相关文件集存储在不同位置中,但我们希望在单个合并视图中显示它们。例如,来自远程 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"]|

联合文件系统对应的层次结构如下图所示

fs

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是如何工作的

  1. 读取文件

    当我们在容器中读取文件时,可能会有以下场景。

    • 文件在容器层中存在时:当文件存在于容器层时,直接从容器层读取。
    • 当文件在容器层中不存在时:当容器运行时需要读取某个文件,如果容器层中不存在时,则从镜像层查找该文件,然后读取文件内容。
    • 文件既存在于镜像层,又存在于容器层:当我们读取的文件既存在于镜像层,又存在于容器层时,将会从容器层读取该文件。
  2. 修改文件或目录

    AUFS 对文件的修改采用的是写时复制的工作机制,这种工作机制可以最大程度节省存储空间。

    具体的文件操作机制如下。

    • 第一次修改文件:当我们第一次在容器中修改某个文件时,AUFS 会触发写时复制操作,AUFS 首先从镜像层复制文件到容器层,然后再执行对应的修改操作。

      AUFS 写时复制的操作将会复制整个文件,如果文件过大,将会大大降低文件系统的性能,因此当我们有大量文件需要被修改时,AUFS 可能会出现明显的延迟。好在,写时复制操作只在第一次修改文件时触发,对日常使用没有太大影响。

    • 删除文件或目录:当文件或目录被删除时,AUFS 并不会真正从镜像中删除它,因为镜像层是只读的,AUFS 会创建一个特殊的文件或文件夹,这种特殊的文件或文件夹会阻止容器的访问。

演示

构建目录和文件

首先在 /tmp 目录下创建 aufs 目录:

1
2
$ cd /tmp
$ mkdir aufs

准备挂载点目录:

1
2
$ cd aufs
$ mkdir mnt

接下来准备容器层内容:

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