Docker镜像分层机制
起源
公司做云桌面,导师给我讲镜像分层机制,说到了一个弊端,即保存用户数据的层即使写入一个很小的txt文件也会占用3G大小,当时我就想到了Docker的镜像分层机制,为什么Docker不会有这个弊端,所以今天撸一下Docker的镜像分层原理。
这里默认你已经知道Docker的镜像分层机制了,所以不会对它是啥做过多的介绍。
层和Storage Drivers
假设有下面一个Dockerfile,第一行的FROM
指令会pullubuntu:22.04
中的层,然后第3、4、5行会分别创建一个新层:
FROM ubuntu:22.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app # new layer
RUN make /app # new layer
RUN rm -r $HOME/.cache # new layer
CMD python /app/app.py
当你基于该镜像启动一个容器时,会创建一个新的读写层,用于承载你的修改,原先的层都是只读的,无论你做什么修改都不会影响到它们,如下图:
只读层使得层可以复用,比如基于Ubuntu再创建一个镜像,它们可以共用Ubuntu的那些层,大大节省了磁盘空间。但是层级的存在使得读取速度变慢,因为每一次读取都需要自顶向下查找每一层,找到第一个具有文件的层。
如何编排这些层,以及如何使用这些层来运行容器就是存储驱动(Storage Driver)要做的,Docker提供了多种存储驱动:
- Btrfs
- Device Mapper
- OverlayFS
- ZFS
- VFS
- AUFS
- ...
虽然说不同的存储驱动编排层的方式不一样,但是它们都遵循上面的层叠镜像模型以及Copy-On-Write策略(后面简称COW)。
Copy-On-Write策略
我们已经知道了一个Docker镜像具有N个只读层,容器运行时会有一个最顶层的读写层,用户的写操作都会留在读写层,容器被删除,这个读写层也被删除。
那么假设/a/b
在某个只读层中,容器运行时,用户期望改动这个文件,它必须将它复制到自己的层中进行修改,这就叫Copy-On-Write(写时复制)。
COW的好处显而易见,容器运行不用拷贝所有底层依赖的镜像层,只需要当有修改时将文件复制到自己的读写层即可,大大提高了空间利用率和创建容器的速度。
COW的坏处也显而易见,每次要修改低层的文件时(包括修改文件元数据),都要将文件复制一份到自己的读写层再操作,所以在写密集的应用中并不建议直接在容器中做IO操作,而是使用卷挂载。
下面做一个COW的实验,我的Dockerfile.1最后一行生成了一个5G的大文件,可以看到生成后的镜像很大:
现在,基于它运行两个镜像,在没进行修改时,这些容器的大小都是5.38G,而实际大小都是0B,因为5.38G是复用镜像的层
我又用这个镜像运行了两个容器,在第四个容器中修改了一下这个大文件:
这一行执行了很久,而在实际的操作系统中应该是瞬间执行完成的,毕竟就是追加一个字节而已,所以可以推断是在执行从原始镜像层复制大文件到容器的读写层的操作,再次通过指令查看这些容器的大小,image4
已经变成了10G多(虚拟大小,读写层有5.37G,加上镜像层的5G)
嘶,为啥不能只复制需要的块,而是整个文件复制?后来知道了这只是
overlay2
的特性,有些存储驱动支持块级别复制,在这种情况下会快很多。
存储驱动介绍
驱动 | 介绍 | 受支持的底层文件系统 |
---|---|---|
overlay2 |
overlay2 是当前所有受支持的Linux发行版的默认存储驱动,不需要额外配置 |
ftype=1下的xfs 、ext4 |
fuse-overlayfs |
fuse-overlayfs 是在运行Rootless Docker的主机上,并且主机不提供rootless的overlay2 支持时的默认选项。在Ubuntu和Debian10上,fuse-overlayfs 驱动不需要被使用,即使是在rootless模式,overlay2 也能工作。细节请看Rootless mode documentation |
任何文件系统 |
btrfs 和zfs |
它俩提供一些高级选项,比如创建快照,但是需要更多的维护和设置,每一个都需要背后的文件系统被正确的配置 | btrfs 和zfs |
vfs |
这个存储驱动用于测试亩地,以及那些没有copy-on-write文件系统可用的情况。它的性能很差,通常不推荐在生产环境使用 | 任何文件系统 |
devicemapper |
该存储驱动在生产环境需要direct-lvm ,因为loopback-lvm 虽然是零配置,但是性能很差。devicemapper 在CentOS和RHEL上曾经是推荐的存储引擎,因为它们的内核版本不支持overlay2 。然而,当前的CentOS和RHEL已经支持overlay2 了。 |
direct-lvm |
每一个存储驱动有自己的性能特征,使得它们或多或少地适合不同的workloads
overlay2
在文件层级工作,而非块层级,这能更高效的利用内存,但在写入比较重的workload下可能让容器的可写层变得非常大。- 块级存储驱动,比如
devicemapper
、btrfs
和zfs
,在重写入workload时性能更好(虽然不如卷) btrfs
和zfs
需要大量内存zfs
在密集的工作负载(如PaaS)下是一个好选择
本节上面的内容都是翻译自官方文档,一些地方看不懂,不用管。
OverlayFS
磁盘存储结构
我的docker没有任何镜像,现在pull一个mysql,它具有11个层:
Docker将文件存储在/var/lib/docker/<storage driver>/
下,进入查看:
除了第一个1
是一个文件以外,剩下的都是一堆名字很古怪的文件夹,不知道里面都是啥,看一看每一个文件夹里都有啥:
yudoge-arch# ls $(ls | tr '\s' '\n')
24c8f8ad45062676747efcee80794e7199f07bbbe3c5eb1cb3e5d7c4602050e9:
committed diff link
29f1a2f52e4307af79565e38b3e16b54bdb13d81e55679cce4f38ecc559b4d50:
committed diff link lower work
2f7e13e6b80f9f19837cbe61578e01141f012a2bbad38efc62aa2d99a095ca2c:
committed diff link lower work
3c554617f26581acebe341ce9242b406dc5b05388d346d1856e2663cdd14e441:
committed diff link lower work
8f72c03dafd2cd46cab06fb10667060cddd86799f857e943da2fbe43d43807a2:
committed diff link lower work
9ebff6298e82baa0337a971e37c9d0e70bb32f4b968eeb018762301edc39505c:
committed diff link lower work
c7c8b73afa6247e87502a016838d12d45ed1f4a024be182040aa88df5f954d7e:
diff link lower work
cdc4dfdc0baa0eef9c6f12097d8e7d40fe332b87b3115282499aba6f514b41b2:
committed diff link lower work
e4adcd02a5079dcfa19f967a21c371dc20b81f048f6f97e037c03b9f44ce12e1:
committed diff link lower work
ff4c13585960bb1b823e3e0e89987905ca638cc212feba4a3b265ff2aa767bca:
committed diff link lower work
l:
2CYSJH5HKCRQYLT3YZ6YJUSWNS MHN435GTE26WH3XQK7GHKLA2WX UE2RISJIAXR2COS55ETHU3M6UU ZI2FF4Z2IIOY2Q5AFJ4KN6COUG
2H3EAOI3ZSQZSC5HDUOD2J72UY OGHUJ26UTM36F4442J67JG6IMT UZCYLVTJUTL2NTUJ2L5HCBG4O3
HJGKKE5R2IYWFOXK3B4VS6P2D6 QPICFLQJ7JGK2RJCMWGHXFOFGK XXP5E2KJH6PJ6XFVB6HBJC4DG2
那些名称像一串哈希值的都是一个层级,它们大都包含committed
、diff
、link
、lower
和work
这些子文件(目录),lower
文件中写明了它的所有父级,diff
文件夹包含该层级自己的文件内容,work
则是OverlayFS内部需要的文件夹。
yudoge-arch# cat 29f1a2f52e4307af79565e38b3e16b54bdb13d81e55679cce4f38ecc559b4d50/lower
l/MHN435GTE26WH3XQK7GHKLA2WX:l/HJGKKE5R2IYWFOXK3B4VS6P2D6:l/ZI2FF4Z2IIOY2Q5AFJ4KN6COUG:l/UE2RISJIAXR2COS55ETHU3M6UU#
yudoge-arch# tree 8f72c03dafd2cd46cab06fb10667060cddd86799f857e943da2fbe43d43807a2/diff
8f72c03dafd2cd46cab06fb10667060cddd86799f857e943da2fbe43d43807a2/diff
└── etc
└── yum.repos.d
└── mysql-community-tools.repo
而l
文件夹中包含一堆符号链接,指向每一个层级文件夹中的diff
:
yudoge-arch# ls -al l
total 56
drwx------ 2 root root 4096 Sep 3 22:31 .
drwx--x--- 15 root root 4096 Sep 3 22:31 ..
lrwxrwxrwx 1 root root 72 Sep 3 22:02 2CYSJH5HKCRQYLT3YZ6YJUSWNS -> ../c7c8b73afa6247e87502a016838d12d45ed1f4a024be182040aa88df5f954d7e/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 2H3EAOI3ZSQZSC5HDUOD2J72UY -> ../29f1a2f52e4307af79565e38b3e16b54bdb13d81e55679cce4f38ecc559b4d50/diff
lrwxrwxrwx 1 root root 77 Sep 3 22:31 4FYPZQFKTYVCP5J56MXWT7Y5PG -> ../de0b45609e14ad7d9f18c283b68f9b5ef2beb9d7d25f3910b490c6abb47f1213-init/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:31 6L3W4MP2S23ZVOZHV467SMV237 -> ../de0b45609e14ad7d9f18c283b68f9b5ef2beb9d7d25f3910b490c6abb47f1213/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 HJGKKE5R2IYWFOXK3B4VS6P2D6 -> ../9ebff6298e82baa0337a971e37c9d0e70bb32f4b968eeb018762301edc39505c/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 MHN435GTE26WH3XQK7GHKLA2WX -> ../3c554617f26581acebe341ce9242b406dc5b05388d346d1856e2663cdd14e441/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 OGHUJ26UTM36F4442J67JG6IMT -> ../ff4c13585960bb1b823e3e0e89987905ca638cc212feba4a3b265ff2aa767bca/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 QPICFLQJ7JGK2RJCMWGHXFOFGK -> ../e4adcd02a5079dcfa19f967a21c371dc20b81f048f6f97e037c03b9f44ce12e1/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 UE2RISJIAXR2COS55ETHU3M6UU -> ../24c8f8ad45062676747efcee80794e7199f07bbbe3c5eb1cb3e5d7c4602050e9/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 UZCYLVTJUTL2NTUJ2L5HCBG4O3 -> ../8f72c03dafd2cd46cab06fb10667060cddd86799f857e943da2fbe43d43807a2/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 XXP5E2KJH6PJ6XFVB6HBJC4DG2 -> ../2f7e13e6b80f9f19837cbe61578e01141f012a2bbad38efc62aa2d99a095ca2c/diff
lrwxrwxrwx 1 root root 72 Sep 3 22:02 ZI2FF4Z2IIOY2Q5AFJ4KN6COUG -> ../cdc4dfdc0baa0eef9c6f12097d8e7d40fe332b87b3115282499aba6f514b41b2/diff
之所以这样编排,好像是因为开启容器的时候运行mount
命令挂载时,mount
的参数长度首页大小限制,最多4096字节,多的会被截断,所以这里有一批简写的链接,然后lower
文件中记录的貌似也是这些链接,层级文件中的link
记录的就是它在l
中的链接文件名。
挂载
容器不能直接利用这些层级,OverlayFS需要通过挂载点提供一个统一的合并后的视图,如果你对此没啥感觉,可以了解下fuse-overlay
。
运行容器后,使用mount
命令可以查看挂载情况,docker将/var/lib/docker/overlay2/dfxxx/merged
作为挂载点(刚刚没这个df什么的文件夹哦),OverlayFS作为该挂载点使用的FS,会将操作系统的文件相关操作转为对于这些upperdir和lowerdir的层级操作:
yudoge-arch# mount | grep overlay
overlay on /var/lib/docker/overlay2/df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/2RWAC6GZLJV64RBOZDSCUCWHJE:/var/lib/docker/overlay2/l/2CYSJH5HKCRQYLT3YZ6YJUSWNS:/var/lib/docker/overlay2/l/QPICFLQJ7JGK2RJCMWGHXFOFGK:/var/lib/docker/overlay2/l/UZCYLVTJUTL2NTUJ2L5HCBG4O3:/var/lib/docker/overlay2/l/OGHUJ26UTM36F4442J67JG6IMT:/var/lib/docker/overlay2/l/XXP5E2KJH6PJ6XFVB6HBJC4DG2:/var/lib/docker/overlay2/l/2H3EAOI3ZSQZSC5HDUOD2J72UY:/var/lib/docker/overlay2/l/MHN435GTE26WH3XQK7GHKLA2WX:/var/lib/docker/overlay2/l/HJGKKE5R2IYWFOXK3B4VS6P2D6:/var/lib/docker/overlay2/l/ZI2FF4Z2IIOY2Q5AFJ4KN6COUG:/var/lib/docker/overlay2/l/UE2RISJIAXR2COS55ETHU3M6UU,upperdir=/var/lib/docker/overlay2/df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260/diff,workdir=/var/lib/docker/overlay2/df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260/work,index=off)
这个/var/lib/docker/overlay2/df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260
就是为容器新创建的文件夹:
yudoge-arch# ls | grep df9a
df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260
df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260-init
查看df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260/merged
文件夹,里面的内容是多个层合并后的统一视图:
yudoge-arch# ls -al df9a7a8984736e75b4348be3234580bbc91e8519bb9a26e07c9c0a3e8cc66260/merged
total 84
drwxr-xr-x 1 root root 4096 Sep 3 22:44 .
drwx--x--- 5 root root 4096 Sep 3 22:44 ..
lrwxrwxrwx 1 root root 7 Oct 9 2021 bin -> usr/bin
dr-xr-xr-x 2 root root 4096 Oct 9 2021 boot
drwxr-xr-x 1 root root 4096 Sep 3 22:44 dev
drwxr-xr-x 2 root root 4096 Aug 11 09:38 docker-entrypoint-initdb.d
-rwxr-xr-x 1 root root 0 Sep 3 22:44 .dockerenv
drwxr-xr-x 1 root root 4096 Sep 3 22:44 etc
drwxr-xr-x 2 root root 4096 Oct 9 2021 home
lrwxrwxrwx 1 root root 7 Oct 9 2021 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Oct 9 2021 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4096 Oct 9 2021 media
drwxr-xr-x 2 root root 4096 Oct 9 2021 mnt
drwxr-xr-x 2 root root 4096 Oct 9 2021 opt
dr-xr-xr-x 2 root root 4096 Aug 10 11:44 proc
dr-xr-x--- 1 root root 4096 Aug 11 09:38 root
drwxr-xr-x 1 root root 4096 Aug 11 09:38 run
lrwxrwxrwx 1 root root 8 Oct 9 2021 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Oct 9 2021 srv
dr-xr-xr-x 2 root root 4096 Aug 10 11:44 sys
drwxrwxrwt 1 root root 4096 Sep 3 22:44 tmp
drwxr-xr-x 1 root root 4096 Aug 10 11:44 usr
drwxr-xr-x 1 root root 4096 Aug 10 11:44 var
OverlayFS将在单一Linux主机上的多个文件夹展现为单一文件夹,这些文件夹被称为层,而将它们归一化的过程被称为联合挂载(union mount),OverlayFS将底层的那些文件夹称为
lowerdir
,顶层的称为upperdir
,最后通过upperdir
下的merged
文件夹暴露出统一视图
限制
删除文件
当对一个在下层的文件写入时,可以将文件拷贝到容器的读写层,但若是要在最后合并的视图中删除一个位于下层的文件呢?
下层的实际文件肯定不能删,又得让合并后的视图中看不见这个文件,OverlayFS的做法是在上层写入一个空的whiteout文件——即一个具有S_IFWHT
stat的文件作为占位文件,它代表在底层已经被删除。
rename dir
当重命名目录时(rename(2)),源和目的必须都存在于顶层,也就是说OverlayFS不允许重命名处于镜像层的目录,这会返回EXDEV
。
open
假设foo
位于底层镜像层,使用O_RDONLY
调用open(2)
得到的是底层的文件,此时再使用O_RDWR
调用open(2)
,发生从底层到容器层的copy_up
,此时第一个和第二个fd引用的不是一个文件。
问题
- 为什么strace了docker start过程,看不到任何有关mount的syscall
要strace守护进程(我蠢了)
- 为什么umount了容器的merged目录,容器还能正常工作,但其它人已经观察不到挂载点中overlayfs的内容了(看起来很像lazy umount)
貌似和namespace相关
- 为什么docker容器运行时还能umount merged目录
貌似和namespace相关
- 为什么在容器的挂载点(merged)上挂了一个自己编写的fuse文件系统,docker容器依然能启动,并叠加一个overlayfs在挂载点上?但反过来先启动docker容器再挂载自己的fs就不行
- 如何骗过docker,让它用自己的fs运行