关于容器技术的原理,我在很早之前翻译过命名空间相关的文章,但这还远远不够,需要切入的还有cgroup、文件系统和网络相关方面的细节。
到了招聘季,稍微有点时间整理这方面的资料,索性先从文件系统入手,本文的目标仅仅是“知其然”。
0x00 对于分层的需求
在LiveCD的场景下,有这么一种需求:要求在发行版启动后,在逻辑上得到一个统一的文件系统,用户能够对其进行读写,而该文件系统中的基础部分来自一个只读文件系统,无法对其进行写操作。
容器镜像的需求也是类似,用户需要能够根据基础镜像构建新镜像,出于共享基础镜像的目的,整个过程并不能影响基础镜像本身,也就是说最终得到的是一个逻辑意义上的新镜像。
通俗而言,就是要求文件系统提供“层次”的概念,目前可选的方案有AUFS,OverlayFS,DeviceMapper等。
作为UnionFS之一的OverlayFS就是这么一种文件系统,它于2014年被合并到3.18内核,顾名思义其主要特性就是“覆盖”,可以结合下图进行理解(图非原创)。
0x01 特性介绍
正如上图所示,在OverlayFS中,存在Lower和Upper的概念,指定的Lower和Upper文件系统共同组成了新的文件系统。
其效果就如同上图所示,我们从Upper的正上方向下观察,存在文件和目录的位置是“不透明”的,因此最终得到的俯视图应该就如同Overlay那一层所示。
对于文件系统而言,“不透明”的另一种表达方式即为覆盖。也就是说Upper中存在的文件会覆盖Lower中的同名文件,但这对于目录而言稍有不同,同名文件会被覆盖,而不同名的则是合并。
对于文件/目录的修改,处理策略如下:
- 若指定文件存在于Upper,则直接修改该文件。
- 若指定文件仅存在于Lower,则会先从Lower拷贝该文件到Upper(copy_up操作),然后进行修改。
对于文件/目录的创建,效果等同于在Upper层直接进行创建。
对于文件的删除,处理策略如下:
- 若指定文件仅存在于Upper,则直接删除该文件。
- 若指定文件存在于Lower,则在Upper层创建同名的字符设备。
对于目录的删除,处理策略如下:
- 若指定目录仅存在于Upper,则直接删除该目录。
- 若指定目录存在于Lower,则在Upper层创建同名的字符设备。
- 若在删除后再次创建同名目录,且同名目录存在于Lower,此时新的目录成为opaque目录,通过将xattr的“trusted.overlay.opaque”设置为“y”实现标记,此时Upper层的该目录就会完全覆盖Lower中的目录,而非原先的合并。
通过上述策略,使得在OverlayFS中,Lower可以是只读的,而Upper则需要是可写的文件系统。
0x02 copy_up操作
在上一节,我们已经提及了copy_up操作,简单阐述,就是指当涉及需要修改Lower中的数据时,将对应数据从Lower拷贝到Upper的操作。
触发copy_up操作有2个条件:
- 涉及到对文件的写操作,无论是meta信息还是文件内容
- 文件仅存在于Lower文件系统中
创建硬链接也会导致copy_up操作,而符号链接则不会。
copy_up操作在解决一些问题的同事,也带来了一些问题,毕竟它不是万能的。
如果你在copy_up前,对该文件加了文件锁,copy_up后,文件锁的目标并不会改变。对于拥有多个硬链接的文件,亦是如此。
0x03 实战
接下来我们通过实际操作来感受一下OverlayFS的功能。具体的命令如下:
sudo mount -t overlay overlay -olowerdir=LLL,upperdir=UUU,workdir=WWW MMM
这里允许存在多个lowerdir,使用“:”分隔即可,upperdir和workerdir是可选的,且它们必须位于同一个文件系统上,workdir似乎是用来存储一些操作的中间产物的。
LLL,UUU,WWW,MMM均为目录,且WWW必须为空。
假设我们有如下目录:
|- lower
| |- a
| |- b
| |- dir/
| | |- c
| | |- d
| |- test/
|- upper/
| |- e
| |- f
| |- dir/
| |- g
|- work/
|- merged/
我们可以通过如下命令将lower和upper“合并”,并挂载到merged目录。
sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged
通过查看merged目录和merged/dir目录,可以得到如下结果:
[codesun@lucode overlay]$ ls -l merged/
总用量 8
-rw-r--r-- 1 codesun users 0 8月 1 00:01 a
-rw-r--r-- 1 codesun users 0 8月 1 00:01 b
drwxr-xr-x 1 codesun users 4096 8月 1 00:02 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 e
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
drwxr-xr-x 2 codesun users 4096 8月 1 00:01 test
[codesun@lucode overlay]$ ls -l merged/dir
总用量 0
-rw-r--r-- 1 codesun users 0 8月 1 00:01 c
-rw-r--r-- 1 codesun users 0 8月 1 00:01 d
-rw-r--r-- 1 codesun users 0 8月 1 00:02 g
尝试添加一个新文件h,然后查看upper目录:
[codesun@lucode overlay]$ touch merged/h
[codesun@lucode overlay]$ ls -l upper/
总用量 4
drwxr-xr-x 2 codesun users 4096 8月 1 00:02 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 e
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
-rw-r--r-- 1 codesun users 0 8月 1 00:05 h
可以看到,h文件被创建到了upper目录,这个就证明了文件创建的规则。
接下来,我们来测试一下copy_up操作,分为两步:
- 修改文件a的meta信息
- 修改文件b的内容
然后,再次查看uppder目录:
[codesun@lucode overlay]$ touch merged/a
[codesun@lucode overlay]$ echo test >> merged/b
[codesun@lucode overlay]$ ls -l upper/
总用量 8
-rw-r--r-- 1 codesun users 0 8月 1 00:08 a
-rw-r--r-- 1 codesun users 5 8月 1 00:08 b
drwxr-xr-x 2 codesun users 4096 8月 1 00:02 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 e
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
-rw-r--r-- 1 codesun users 0 8月 1 00:05 h
可以看到,touch操作修改了文件a的atime信息,所以被copy_up了,文件b由于内容被修改,亦被copy_up。
接下来,我们验证一下文件的删除和文件夹的删除,分为如下几步:
- 删除upper独有的文件e
- 删除test目录
- 删除共有的文件a,测试字符设备机制
[codesun@lucode overlay]$ rm merged/e
[codesun@lucode overlay]$ rmdir merged/test
[codesun@lucode overlay]$ rm merged/a
[codesun@lucode overlay]$ ls -l upper/
总用量 8
c--------- 1 root root 0, 0 8月 1 00:12 a
-rw-r--r-- 1 codesun users 5 8月 1 00:08 b
drwxr-xr-x 2 codesun users 4096 8月 1 00:02 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
-rw-r--r-- 1 codesun users 0 8月 1 00:05 h
c--------- 1 root root 0, 0 8月 1 00:12 test
[codesun@lucode overlay]$ ls -l lower/
总用量 8
-rw-r--r-- 1 codesun users 0 8月 1 00:01 a
-rw-r--r-- 1 codesun users 0 8月 1 00:01 b
drwxr-xr-x 2 codesun users 4096 8月 1 00:01 dir
drwxr-xr-x 2 codesun users 4096 8月 1 00:01 test
可以看到文件e删除后,没有任何字符设备生成,因为它是upper目录独有的,文件a和目录test被删除后,则是生成了字符设备,以表示文件已被删除,而lower目录中的文件a和目录test,并没有任何变化。
接下来,我们来测试opaque目录机制,具体的步骤是删除共有目录dir,然后再创建同名目录。
[codesun@lucode overlay]$ rm -r merged/dir
[codesun@lucode overlay]$ ls -l upper/
总用量 4
c--------- 1 root root 0, 0 8月 1 00:12 a
-rw-r--r-- 1 codesun users 5 8月 1 00:08 b
c--------- 1 root root 0, 0 8月 1 00:15 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
-rw-r--r-- 1 codesun users 0 8月 1 00:05 h
c--------- 1 root root 0, 0 8月 1 00:12 test
[codesun@lucode overlay]$ ls -l lower/
总用量 8
-rw-r--r-- 1 codesun users 0 8月 1 00:01 a
-rw-r--r-- 1 codesun users 0 8月 1 00:01 b
drwxr-xr-x 2 codesun users 4096 8月 1 00:01 dir
drwxr-xr-x 2 codesun users 4096 8月 1 00:01 test
删除dir目录后,可以看到和目录test被删除后一样的现象,lower中的dir目录则并无变化,那么在重新创建dir目录呢?
[codesun@lucode overlay]$ mkdir merged/dir
[codesun@lucode overlay]$ ls -l upper/
总用量 8
c--------- 1 root root 0, 0 8月 1 00:12 a
-rw-r--r-- 1 codesun users 5 8月 1 00:08 b
drwxr-xr-x 2 codesun users 4096 8月 1 00:17 dir
-rw-r--r-- 1 codesun users 0 8月 1 00:02 f
-rw-r--r-- 1 codesun users 0 8月 1 00:05 h
c--------- 1 root root 0, 0 8月 1 00:12 test
[codesun@lucode overlay]$ ls -l merged/dir
总用量 0
可以看到,目录创建后,upper中的同名字符设备不见了,而其内部并没有文件c和文件d,也就是说该目录已经是一个opaque目录了。
需要注意的是,在挂载OverlayFS后,如果修改其Lower和Upper目录中的内容,其行为对于合并后的文件系统是未定义的,所以尽量不要这么做。
0xFF 总结
总体而言,OverlayFS的使用还是很方便的,并且其概念也不难理解,在使用Docker时,我们可以通过配置使daemon使用该驱动,据说相交DeviceMapper更适合生产环境。
从目录结构来看,目前我的系统中,Docker默认使用的似乎是DeviceMapper,所以近期还会摸索一下DeviceMapper,尽请期待。