docker联合文件系统
什么是联合文件系统
联合挂载是一种文件系统,它可以在不修改其原始(物理)源的情况下创建多个目录,并把内容合并为一个文件的错觉。这可能很有用,因为我们可能将相关文件集存储在不同位置中,但我们希望在单个合并视图中显示它们。例如,来自远程 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 | FROM ubuntu:14.04 |
联合文件系统对应的层次结构如下图所示
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 首先从镜像层复制文件到容器层,然后再执行对应的修改操作。
AUFS 写时复制的操作将会复制整个文件,如果文件过大,将会大大降低文件系统的性能,因此当我们有大量文件需要被修改时,AUFS 可能会出现明显的延迟。好在,写时复制操作只在第一次修改文件时触发,对日常使用没有太大影响。
- 删除文件或目录:当文件或目录被删除时,AUFS 并不会真正从镜像中删除它,因为镜像层是只读的,AUFS 会创建一个特殊的文件或文件夹,这种特殊的文件或文件夹会阻止容器的访问。
-
演示
构建目录和文件
首先在 /tmp 目录下创建 aufs 目录:
1 | cd /tmp |
准备挂载点目录:
1 | cd aufs |
接下来准备容器层内容:
1 | # 创建镜像层目录 |
最后准备镜像层内容:
1 | # 创建两个镜像层目录 |
准备好的目录和文件结构如下:
1 | tree . |
创建AUFS联合文件系统
在使用aufs之前,可以通过下面的命令确认当前系统是否支持aufs,如果不支持,请自行根据相应发行版的文档安装
1 | 下面的命令如果没有输出,表示内核不支持aufs |
使用 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 | mount -t aufs |
我们每创建一个 AUFS 文件系统,AUFS 都会为我们生成一个 ID,这个 ID 在 /sys/fs/aufs/
会创建对应的目录,在这个 ID 的目录下可以查看文件挂载的权限。
1 | cat /sys/fs/aufs/si_9553b4ca12858995/* |
可以看到 container1 目录的权限为 rw(代表可读写),image1 和 image2 的权限为 ro(代表只读)。
为了验证 mnt 目录下可以看到 container1、image1 和 image2 目录下的所有内容,我们使用 ls 命令查看一下 mnt 目录:
1 | ls -l mnt |
可以看到 mnt 目录下已经出现了我们准备的所有镜像层和容器层的文件。下面让我们来验证一下 AUFS 的写时复制。
aufs写时复制
AUFS 的写时复制是指在容器中,只有需要修改某个文件时,才会把文件从镜像层复制到容器层,下面我们通过修改联合挂载目录 mnt 下的内容来验证下这个过程。
我们使用以下命令修改 mnt 目录下的 image1.txt 文件:
1 | echo Hello, Image layer1 changed! > mnt/image1.txt |
然后我们查看下 image1/image1.txt 文件内容:
1 | cat image1/image1.txt |
发现“镜像层”的 image1.txt 文件并未被修改。
然后我们查看一下"容器层"对应的 image1.txt 文件内容:
1 | ls -l container1/ |
发现 AUFS 在“容器层”自动创建了 image1.txt 文件,并且内容为我们刚才写入的内容。
至此,我们完成了 AUFS 写时复制的验证。我们在第一次修改镜像内某个文件时,AUFS 会复制这个文件到容器层,然后在容器层对该文件进行修改操作,这就是 AUFS 最典型的特性写时复制。
Docker 如何使用 OverlayFS
为了演示 Docker 如何使用 OverlayFS,我们将尝试模拟 Docker 如何装载容器和镜像层。在执行此操作之前,我们首先需要清理工作区并获得一个镜像:
1 | ~ $ docker image prune -af |
我们有一个镜像(nginx)可以测试,所以接下来,让我们检查它的镜像层。我们可以通过 docker inspect 在镜像上运行并检查 GraphDriver
字段或通过浏览/var/lib/docker/overlay2
存储所有镜像层的目录来检查镜像层。最后看看里面有什么:
1 | ~ $ cd /var/lib/docker/overlay2 |
查看上面的输出,它看起来与我们在 mount 命令中看到的非常相似,进一步来说:
-
LowerDir: 是只读镜像层的目录,以冒号分隔
-
MergedDir:镜像和容器中所有图层的合并视图
-
UpperDir:写入更改的读写层
-
WorkDir:Linux OverlayFS 用于准备合并视图的工作目录
接下来,让我们更进一步,运行一个容器并检查它的层:
1 | ~ $ docker run -d --name container nginx |
上面的输出显示 docker inspect nginx 的输出中列出的目录与 MergedDir、UpperDir 和 WorkDir(id 为 3d963d191b2101b3406348217f4257d7374aa4b4a73b4b4a6dd4ab0f365d38dfbd)相同,现在是容器的 LowerDir 的一部分。这里的下半部分是由所有的 nginx 镜像层叠加在一起组成的。在它们之上是 UpperDir 中的可写层,它包含/etc、/run 和/var。同样,如果我们在上面列出 MergedDir,您将看到容器可以使用的整个文件系统,包括 UpperDir 和 LowerDir 中的所有内容。
最后,为了模拟 Docker 的行为,我们可以使用这些相同的目录来手动创建我们自己的合并视图:
1 | ~ $ mount -t overlay -o \ |
在这里,我们只是从前面的代码片段中获取值并将它们传递给 mount 命令中的适当参数,唯一的区别是我们用于/mnt/merged 合并视图而不是/var/lib/docker/overlay2/…/merged.
这就是 Docker 中整个 OverlayFS 的真正含义——mount 跨多个堆叠层的单个命令。下面是负责这个的 Docker 代码的一部分 -lowerdir=…,upperdir=…,workdir=…值的替换,然后是 unix.Mount
1 | // https://github.com/moby/moby/blob/1ef1cc8388165b2b848f9b3f53ec91c87de09f63/daemon/graphdriver/overlay2/overlay.go#L580 |
https://jishuin.proginn.com/p/763bfbd61dab