把玩overlay文件系统

关于容器技术的原理,我在很早之前翻译过命名空间相关的文章,但这还远远不够,需要切入的还有cgroup、文件系统和网络相关方面的细节。

到了招聘季,稍微有点时间整理这方面的资料,索性先从文件系统入手,本文的目标仅仅是“知其然”。

0x00 对于分层的需求


在LiveCD的场景下,有这么一种需求:要求在发行版启动后,在逻辑上得到一个统一的文件系统,用户能够对其进行读写,而该文件系统中的基础部分来自一个只读文件系统,无法对其进行写操作。

容器镜像的需求也是类似,用户需要能够根据基础镜像构建新镜像,出于共享基础镜像的目的,整个过程并不能影响基础镜像本身,也就是说最终得到的是一个逻辑意义上的新镜像。

通俗而言,就是要求文件系统提供“层次”的概念,目前可选的方案有AUFS,OverlayFS,DeviceMapper等。

作为UnionFS之一的OverlayFS就是这么一种文件系统,它于2014年被合并到3.18内核,顾名思义其主要特性就是“覆盖”,可以结合下图进行理解(图非原创)。

overlay-arch.png

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个条件:

  1. 涉及到对文件的写操作,无论是meta信息还是文件内容
  2. 文件仅存在于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操作,分为两步:

  1. 修改文件a的meta信息
  2. 修改文件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。

接下来,我们验证一下文件的删除和文件夹的删除,分为如下几步:

  1. 删除upper独有的文件e
  2. 删除test目录
  3. 删除共有的文件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,尽请期待。

说两句: