本文主要介绍了 Docker 的另一个核心技术:Union File System。主要包括对 overlayfs 的演示,以及分析 docker 是如何借助 ufs 实现容器 rootfs 的。
<!--more-->
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅
1. 概述
Union File System
Union File System ,简称 UnionFS 是一种为 Linux FreeBSD NetBSD 操作系统设计的,把其他文件系统联合到一个联合挂载点的文件系统服务。
它使用 branch 不同文件系统的文件和目录“透明地
”覆盖,形成一个单一一致的文件系统。
这些 branches 或者是 read-only 或者是 read-write 的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为 unionfs 用到了一个重要的资管管理技术叫写时复制。
写时复制(copy-on-write,下文简称 CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。
它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源,这个资源可以被新旧实例共享。
创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。
UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。
比如,我现在有两个目录 A 和 B,它们分别有两个文件:
$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:
$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C
这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:
$ tree ./C
./C
├── a
├── b
└── x
可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。
这就是联合文件系统,目的就是将多个文件联合在一起成为一个统一的视图。
常见实现
AUFS
AuFS 的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来干脆改名叫作 Advance UnionFS。
AUFS 完全重写了早期的 UnionFS 1.x,其主要目的是为了可靠性和性能,并且引入了一些新的功能,比如可写分支的负载均衡。
AUFS 的一些实现已经被纳入 UnionFS 2.x 版本。
AUFS 只是 Docker 使用的存储驱动的一种,除了 AUFS 之外,Docker 还支持了不同的存储驱动,包括 aufs
、devicemapper
、overlay2
、zfs
和 vfs
等等,在最新的 Docker 中,overlay2
取代了 aufs
成为了推荐的存储驱动,但是在没有 overlay2
驱动的机器上仍然会使用 aufs
作为 Docker 的默认驱动。
overlayfs
Overlayfs 是一种类似 aufs 的一种堆叠文件系统,于 2014 年正式合入 Linux-3.18 主线内核,目前其功能已经基本稳定(虽然还存在一些特性尚未实现)且被逐渐推广,特别在容器技术中更是势头难挡。
Overlayfs 是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如 ext4fs 和 xfs 等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。
简单的总结为以下 3 点:
- 1)上下层同名目录合并;
- 2)上下层同名文件覆盖;
- 3)lower dir 文件写时拷贝。
这三点对用户都是不感知的。
假设我们有 dir1 和 dir2 两个目录:
dir1 dir2
/ /
a a
b c
然后我们可以把 dir1 和 dir2 挂载到 dir3 上,就像这样:
dir3
/
a
b
c
需要注意的是:在 overlay 中 dir1 和 dir2 是有上下关系的。lower 和 upper 目录不是完全一致,有一些区别,具体见下一节。
2. overlayfs 演示
当前 overlayfs 比较主流,因此使用 overlayfs 进行演示。
环境准备
具体演示如下:
创建一个如下结构的目录:
.
├── lower
│ ├── a
│ └── c
├── merged
├── upper
│ ├── a
│ └── b
└── work
具体命令如下:
mkdir ./{merged,work,upper,lower}
touch ./upper/{a,b}
touch ./lower/{a,c}
然后进行 mount 操作:
# -t overlay 表示文件系统为 overlay
# -o lowerdir=./lower,upperdir=./upper,workdir=./work 指定 lowerdir、upperdir以及 workdir这3个目录。
# 其中 lowerdir 是自读的,upperdir是可读写的,
sudo mount \
-t overlay \
overlay \
-o lowerdir=./lower,upperdir=./upper,workdir=./work \
./merged
此时目录结构如下:
.
├── lower
│ ├── a
│ └── c
├── merged
│ ├── a
│ ├── b
│ └── c
├── upper
│ ├── a
│ └── b
└── work
└── work
可以看到,merged 目录已经可以同时看到 lower 和 upper 中的文件了,而由于文件 a 同时存在于 lower 和 upper 中,因此 lower 中的被覆盖了,只显示了一个 a。
修改文件
虽然 lower 和 upper 中的文件都出现在了 merged 目录,但是二者还是有区别的。
lower 为底层目录,只提供数据,不能写。
upper 为上层目录,是可读写的。
测试:
# 分别对 merged 中的文件b和c写入数据
# 其中文件 c 来自 lower,b来自 upper
echo "will-persist" > ./merged/b
echo "wont-persist" > ./merged/c
修改后从 merged 这个视图进行查看:
$ cat ./merged/b
will-persist
$ cat ./merged/c
wont-persist
可以发现,好像两个文件都被更新了,难道上面的结论是错的?
再从 upper 和 lower 视角进行查看:
$ cat ./upper/b
will-persist
$ cat ./lower/c
(empty)
可以发现 lower 中的文件 c 确实没有被改变。
那么 merged 中查看的时候,文件 c 为什么有数据呢?
由于 lower 是不可写的,因此采用了 CoW 技术,在对 c 进行修改时,复制了一份数据到 overlay 的 upper dir,即这里的 upper 目录,进入 upper 目录查看是否存在 c 文件:
[root@iZ2zefmrr626i66omb40ryZ upper]$ ll
total 8
-rw-r--r-- 1 root root 0 Jan 18 18:50 a
-rw-r--r-- 1 root root 13 Jan 18 19:10 b
-rw-r--r-- 1 root root 13 Jan 18 19:10 c
[root@iZ2zefmrr626i66omb40ryZ upper]$ cat c
wont-persist
可以看到,upper 目录中确实存在了 c 文件,
因为是从 lower copy 到 upper,因此也叫做 copy_up。
删除文件
首先往 lower 目录中写入一个文件 f
[root@iZ2zefmrr626i66omb40ryZ ufs]$ cd lower/
[root@iZ2zefmrr626i66omb40ryZ lower]$ echo fff >> f
然后到 merge 目录查看,能否看到文件 f
[root@iZ2zefmrr626i66omb40ryZ lower]$ ls ../merged/
f
果然 lower 中添加后,merged 中也能直接看到了。
然后再 merged 中去删除文件 f:
[root@iZ2zefmrr626i66omb40ryZ lower]$ cd ../merged/
[root@iZ2zefmrr626i66omb40ryZ merged]$ rm -rf f
# merged 中删除后 lower 中文件还在
[root@iZ2zefmrr626i66omb40ryZ merged]$ ls ../lower/
a c e f
# 而 upper 中出现了一个大小为0的c类型文件f
[root@iZ2zefmrr626i66omb40ryZ merged]# ls -l ../upper/
total 0
c--------- 1 root root 0, 0 Jan 18 19:28 f
可以发现,overlay 中删除 lower 中的文件,其实也是在 upper 中创建一个标记,表示这个文件已经被删除了,而不会真正删除 lower 中的文件。
测试一下:
[root@iZ2zefmrr626i66omb40ryZ merged]$ rm -rf ../upper/f
[root@iZ2zefmrr626i66omb40ryZ merged]$ ls
f
[root@iZ2zefmrr626i66omb40ryZ merged]$ cat f
fff
把 upper 中的大小为 0 的 f 文件给删掉后,merged 中又可以看到 lower 中 f 了,而且内容也是一样的。
说明 overlay 中的删除其实是标记删除。再 upper 中添加一个删除标记,这样该文件就被隐藏了,从 merged 中看到的效果就是文件被删除了。
删除文件或文件夹时,会在 upper 中添加一个同名的
c
标识的文件,这个文件叫whiteout
文件。当扫描到此文件时,会忽略此文件名。
添加文件
最后再试一下添加文件
# 首先在 merged 中创建文件 g
[root@iZ2zefmrr626i66omb40ryZ merged]$ echo ggg >> g
[root@iZ2zefmrr626i66omb40ryZ merged]$ ls
g
# 然后查看 upper,发现也存在文件 g
[root@iZ2zefmrr626i66omb40ryZ merged]$ ls ../upper/
g
# 在查看内容,发送是一样的
[root@iZ2zefmrr626i66omb40ryZ merged]$ cat ../upper/g
ggg
说明 overlay 中添加文件其实就是在 upper 中添加文件。
测试一下删除会怎么样呢:
[root@iZ2zefmrr626i66omb40ryZ merged]$ rm -rf ../upper/g
[root@iZ2zefmrr626i66omb40ryZ merged]$ ls
f
把 upper 中的文件 g 删除了,果然 merged 中的文件 g 也消失了。
3. docker 是如何使用 overlay 的?
上一节分析了 overlayfs 具体使用,这里分享一下 docker 是怎么使用 overlayfs。
大致流程
每一个 Docker image 都是由一系列的 read-only layers 组成:
- image layers 的内容都存储在 Docker hosts filesystem 的 /var/lib/docker/aufs/diff 目录下
- 而 /var/lib/docker/aufs/layers 目录则存储着 image layer 如何堆栈这些 layer 的 metadata。
docker 支持多种 graphDriver,包括 vfs、devicemapper、overlay、overlay2、aufs 等等,其中最常用的就是 aufs 了,但随着 linux 内核 3.18 把 overlay 纳入其中,overlay 的地位变得更重。
docker info
命令可以查看 docker 的文件系统。
$ docker info
# ...
Storage Driver: overlay2
#...
比如这里用的就是 overlay2。
例如,假设我们有一个由两层组成的容器镜像:
layer1: layer2:
/etc /bin
myconf.ini my-binary
然后,在容器运行时将把这两层作为 lower 目录,创建一个空upper
目录,并将其挂载到某个地方:
sudo mount \
-t overlay \
overlay \
-o lowerdir=/layer1:/layer2,upperdir=/upper,workdir=/work \
/merged
最后将/merged
用作容器的 rootfs。
这样,容器中的文件系统就完成了。
具体分析
以构建镜像方式演示以下 docker 是如何使用 overlayfs 的。
先拉一下 Ubuntu:20.04 的镜像:
$ docker pull ubuntu:20.04
20.04: Pulling from library/ubuntu
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:20.04
docker.io/library/ubuntu:20.04
然后写个简单的 Dockerfile :
FROM ubuntu:20.04
RUN echo "Hello world" > /tmp/newfile
开始构建:
$ docker build -t hello-ubuntu .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM ubuntu:20.04
---> ba6acccedd29
Step 2/2 : RUN echo "Hello world" > /tmp/newfile
---> Running in ee79bb9802d0
Removing intermediate container ee79bb9802d0
---> 290d8cc1f75a
Successfully built 290d8cc1f75a
Successfully tagged hello-ubuntu:latest
查看构建好的镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-ubuntu latest 290d8cc1f75a 13 minutes ago 72.8MB
ubuntu 20.04 ba6acccedd29 3 months ago 72.8MB
使用docker history
命令,查看镜像使用的 image layer 情况:
$ docker history hello-ubuntu
IMAGE CREATED CREATED BY SIZE COMMENT
290d8cc1f75a 22 seconds ago /bin/sh -c echo "Hello world" > /tmp/newfile 12B
ba6acccedd29 3 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:5d68d27cc15a80653… 72.8MB
带 missing 标记的 layer 是自 Docker 1.10 之后,一个镜像的 image layer image history 数据都存储在 个文件中导致的,这是 Docker 官方认为的正常行为。
可以看到,290d8cc1f75a 这一层在最上面,只用了 12Bytes,而下面的两层都是共享的,这也证明了 AUFS 是如何高效使用磁盘空间的。
然后去找一下具体的文件:
docker 默认的存储目录是/var/lib/docker
,具体如下:
[root@iZ2zefmrr626i66omb40ryZ docker]$ ls -al
total 24
drwx--x--x 13 root root 167 Jul 16 2021 .
drwxr-xr-x. 42 root root 4096 Oct 13 15:07 ..
drwx--x--x 4 root root 120 May 24 2021 buildkit
drwx-----x 7 root root 4096 Jan 17 20:25 containers
drwx------ 3 root root 22 May 24 2021 image
drwxr-x--- 3 root root 19 May 24 2021 network
drwx-----x 53 root root 12288 Jan 17 20:25 overlay2
drwx------ 4 root root 32 May 24 2021 plugins
drwx------ 2 root root 6 Jul 16 2021 runtimes
drwx------ 2 root root 6 May 24 2021 swarm
drwx------ 2 root root 6 Jan 17 20:25 tmp
drwx------ 2 root root 6 May 24 2021 trust
drwx-----x 5 root root 266 Dec 29 14:31 volumes
在这里,我们只关心image
和overlay2
就足够了。
- image:镜像相关
- overlay2:docker 文件所在目录,也可能不叫这个名字,具体和文件系统有关,比如可能是 aufs 等。
先看 image
目录:
docker 会在/var/lib/docker/image
目录下按每个存储驱动的名字创建一个目录,如这里的overlay2
。
[root@iZ2zefmrr626i66omb40ryZ docker]$ cd image/
[root@iZ2zefmrr626i66omb40ryZ image]$ ls
overlay2
# 看下里面有哪些文件
[root@iZ2zefmrr626i66omb40ryZ image]$ tree -L 2 overlay2/
overlay2/
├── distribution
│ ├── diffid-by-digest
│ └── v2metadata-by-diffid
├── imagedb
│ ├── content
│ └── metadata
├── layerdb
│ ├── mounts
│ ├── sha256
│ └── tmp
└── repositories.json
这里的关键地方是imagedb
和layerdb
目录,看这个目录名字,很明显就是专门用来存储元数据的地方。
- layerdb:docker image layer 信息
- imagedb:docker image 信息
因为 docker image 是由 layer 组成的,而 layer 也已复用,所以分成了 layerdb 和 imagedb。
先去 imagedb 看下刚才构建的镜像:
$ cd overlay2/imagedb/content/sha256
$ ls
[root@iZ2zefmrr626i66omb40ryZ sha256]# ls
0c7ea9afc0b18a08b8d6a660e089da618541f9aa81ac760bd905bb802b05d8d5 61ad638751093d94c7878b17eee862348aa9fc5b705419b805f506d51b9882e7
// .... 省略
b20b605ed599feb3c4757d716a27b6d3c689637430e18d823391e56aa61ecf01
60d84e80b842651a56cd4187669dc1efb5b1fe86b90f69ed24b52c37ba110aba ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1
可以看到,都是 64 位的 ID,这些就是具体镜像信息,刚才构建的镜像 ID 为290d8cc1f75a
,所以就找290d8cc1f75a
开头的文件:
[root@iZ2zefmrr626i66omb40ryZ sha256]$ cat 290d8cc1f75a4e230d645bf03c49bbb826f17d1025ec91a1eb115012b32d1ff8
{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["bash"],"Image":"sha256:ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"ee79bb9802d0ff311de6d606fad35fa7e9ab0c1cb4113837a50571e79c9454df","container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","echo \"Hello world\" \u003e /tmp/newfile"],"Image":"sha256:ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2022-01-17T12:25:14.91890037Z","docker_version":"20.10.6","history":[{"created":"2021-10-16T00:37:47.226745473Z","created_by":"/bin/sh -c #(nop) ADD file:5d68d27cc15a80653c93d3a0b262a28112d47a46326ff5fc2dfbf7fa3b9a0ce8 in / "},{"created":"2021-10-16T00:37:47.578710012Z","created_by":"/bin/sh -c #(nop) CMD [\"bash\"]","empty_layer":true},{"created":"2022-01-17T12:25:14.91890037Z","created_by":"/bin/sh -c echo \"Hello world\" \u003e /tmp/newfile"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b","sha256:b3cce2ce0405ffbb4971b872588c5b7fc840514b807f18047bf7d486af79884c"]}}
这就是 image 的 metadata,这里主要关注 rootfs:
# 和 docker inspect 命令显示的内容差不多
// ...
"rootfs":{"type":"layers","diff_ids":
[
"sha256:9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b",
"sha256:b3cce2ce0405ffbb4971b872588c5b7fc840514b807f18047bf7d486af79884c"
]
}
// ...
可以看到 rootfs 的 diff_ids 是一个包含了两个元素的数组,这两个元素就是组成 hello-ubuntu 镜像的两个 Layer 的diffID
。
从上往下看,就是底层到顶层,即9f54eef412...
是 image 的最底层。
然后根据 layerID 去layerdb
目录寻找对应的 layer:
[root@iZ2zefmrr626i66omb40ryZ overlay2]# tree -L 2 layerdb/
layerdb/
├── mounts
├── sha256
└── tmp
在这里我们只管mounts
和sha256
两个目录,先打印以下 sha256 目录
$ cd /var/lib/docker/image/overlay2/layerdb/sha256/
$ ls
05dd34c0b83038031c0beac0b55e00f369c2d6c67aed11ad1aadf7fe91fbecda
// ... 省略
6aa07175d1ac03e27c9dd42373c224e617897a83673aa03a2dd5fb4fd58d589f
可以看到,layer 里也是 64 位随机 ID 构成的目录,找到刚才 hello-ubuntu 镜像的最底层 layer:
$ cd 9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b
[root@iZ2zefmrr626i66omb40ryZ 9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b]$ ls
cache-id diff size tar-split.json.gz
文件含义如下:
- cache-id:为具体
/var/lib/docker/overlay2/<cache-id>
存储路径 - diff:diffID,用于计算 ChainID
- size:当前 layer 的大小
docker 使用了 chainID 的方式来保存 layer,layer.ChainID 只用本地,根据 layer.DiffID 计算,并用于 layerdb 的目录名称。
chainID 唯一标识了一组(像糖葫芦一样的串的底层)diffID 的 hash 值,包含了这一层和它的父层(底层),
- 当然这个糖葫芦可以有一颗山楂,也就是 chainID(layer0)==diffID(layer0);
- 对于多颗山楂的糖葫芦,ChainID(layerN) = SHA256hex(ChainID(layerN-1) + " " + DiffID(layerN))。
# 查看 diffID,
$ cat diff
sha256:9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b
由于这是 layer0,所以 chainID 就是 diffID,然后开始计算 layer1 的 chainID:
ChainID(layer1) = SHA256hex(ChainID(layer0) + " " + DiffID(layer1))
layer0 的 chainID 是9f54...
,而 layer1 的 diffID 根据 rootfs 中的数组可知,为b3cce...
计算 ChainID:
$ echo -n "sha256:9f54eef412758095c8079ac465d494a2872e02e90bf1fb5f12a1641c0d1bb78b sha256:b3cce2ce0405ffbb4971b872588c5b7fc840514b807f18047bf7d486af79884c" | sha256sum| awk '{print $1}'
6613b10b697b0a267c9573ee23e54c0373ccf72e7991cf4479bd0b66609a631c
一定注意要加上 “sha256:”和中间的空格“ ” 这两部分。
因此 layer1 的 chainID 就是6613...
找到 layerdb 里面以sha256+6613
开头的目录
$ cd /var/lib/docker/image/overlay2/layerdb/sha2566613b10b697b0a267c9573ee23e54c0373ccf72e7991cf4479bd0b66609a631c
# 根据这个大小可以知道,就是hello-ubuntu 镜像的最上面层 layer
[root@iZ2zefmrr626i66omb40ryZ 6613b10b697b0a267c9573ee23e54c0373ccf72e7991cf4479bd0b66609a631c]$ cat size
12
# 查看 cache-id 找到 文件系统中的具体位置
[root@iZ2zefmrr626i66omb40ryZ 6613b10b697b0a267c9573ee23e54c0373ccf72e7991cf4479bd0b66609a631c]$ cat cache-id
83b569c0f5de093192944931e4f41dafb2d7f80eae97e4bd62425c20e2079f65
根据 cache-id 进入具体数据存储目录:
格式为 /var/lib/docker/overlay2/<cache-id>
# 进入刚才生成的目录
$ cd /var/lib/docker/overlay2/83b569c0f5de093192944931e4f41dafb2d7f80eae97e4bd62425c20e2079f65
[root@iZ2zefmrr626i66omb40ryZ 83b569c0f5de093192944931e4f41dafb2d7f80eae97e4bd62425c20e2079f65]# ls -al
total 24
drwx-----x 4 root root 55 Jan 17 20:25 .
drwx-----x 53 root root 12288 Jan 17 20:25 ..
drwxr-xr-x 3 root root 17 Jan 17 20:25 diff
-rw-r--r-- 1 root root 26 Jan 17 20:25 link
-rw-r--r-- 1 root root 28 Jan 17 20:25 lower
drwx------ 2 root root 6 Jan 17 20:25 work
# 查看 diff 目录
[root@iZ2zefmrr626i66omb40ryZ
83b569c0f5de093192944931e4f41dafb2d7f80eae97e4bd62425c20e2079f65]$ cd diff/
[root@iZ2zefmrr626i66omb40ryZ diff]$ ls
tmp
[root@iZ2zefmrr626i66omb40ryZ diff]$ cd tmp/
[root@iZ2zefmrr626i66omb40ryZ tmp]$ ls
newfile
[root@iZ2zefmrr626i66omb40ryZ tmp]# cat newfile
Hello world
可以看到,我们新增的 newfile 就在这里。
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。