docker进阶教程三:rootfs之联合文件系统

一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等。Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。 用到的技术就是联合文件系统(Union File System),也叫 UnionFS ,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。

在 docker 架构中,当 docker daemon 为 docker 容器挂载 rootfs 时,沿用了 Linux 内核启动时的做法,即将 rootfs 设为只读模式。在挂载完毕之后,利用联合挂载(union mount)技术在已有的只读 rootfs 上再挂载一个读写层。这样,可读写的层处于 docker 容器文件系统的最顶层,其下可能联合挂载了多个只读的层,只有在 docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的旧版本文件

目前docker在使用的文件系统包括但不限于以下这几种:aufs, device mapper, btrfs, overlayfs, vfs, zfs。aufs是ubuntu 常用的,device mapper 是 centos,btrfs 是 SUSE,overlayfs ubuntu 和 centos 都会使用,vfs 和 zfs 常用在 solaris 系统。现在最新的 docker 版本中默认两个系统都是使用的 overlay2。之前centos有一段时间使用了device mapper,但其性能不佳,所以很多文章不要在centos上面使用docker,现在看来,还是可以使用的。

rootfs实现原理:chroot

chroot(change root file system),即改变进程的根目录到你指定的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@linjx ~]# mkdir -p $HOME/test
[root@linjx ~]# mkdir -p $HOME/test/{bin,lib64,lib}
[root@linjx ~]# cp -v /bin/{bash,ls} $HOME/test/bin
[root@linjx ~]# T=$HOME/test
[root@linjx ~]# echo $T
/root/test
[root@linjx ~]# list="$(ldd /bin/bash | egrep -o '/lib.*\.[0-9]')"
[root@linjx ~]# for i in $list; do cp -v "$i" "${T}${i}"; done
‘/lib64/libselinux.so.1’ -> ‘/root/test/lib64/libselinux.so.1’
‘/lib64/libcap.so.2’ -> ‘/root/test/lib64/libcap.so.2’
‘/lib64/libacl.so.1’ -> ‘/root/test/lib64/libacl.so.1’
‘/lib64/libc.so.6’ -> ‘/root/test/lib64/libc.so.6’
‘/lib64/libpcre.so.1’ -> ‘/root/test/lib64/libpcre.so.1’
‘/lib64/libdl.so.2’ -> ‘/root/test/lib64/libdl.so.2’
‘/lib64/ld-linux-x86-64.so.2’ -> ‘/root/test/lib64/ld-linux-x86-64.so.2’
‘/lib64/libattr.so.1’ -> ‘/root/test/lib64/libattr.so.1’
‘/lib64/libpthread.so.0’ -> ‘/root/test/lib64/libpthread.so.0’

这样就可以使用chroot来切换根目录,单用户模式就是使用这个原理来实现的。

1
2
3
4
5
6
7
[root@linjx ~]# chroot $HOME/test
bash-4.2# ls
bash: ls: command not found
bash-4.2# cd
bin/ lib/ lib64/
bash-4.2#
bash-4.2# exit

实际上,Mount Namespace正是基于对chroot的不断改良才被发明出来的,它也是Linux操作系统里的第一个Namespace。

当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如Ubuntu16.04的ISO。这样,在容器启动之后,我们在容器里通过执行”ls /“查看根目录下的内容,就是Ubuntu 16.04的所有目录和文件。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在Linux操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。正是由于rootfs的存在,容器才有了一个被反复宣传至今的重要特性:一致性。由于rootfs里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了

这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。

联合文件系统

UnionFS

Union File System也叫UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录A和B,它们分别有两个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@fdm:~# mkdir {a,b,c}
root@fdm:~# touch a/{a,x}
root@fdm:~# touch b/{b,x}
root@fdm:~# echo aaa >a/x
root@fdm:~# echo bbb >b/x
root@fdm:~# mount -t aufs -o dirs=./a:./b none ./c
root@fdm:~# df -h |grep '/root/c'
none 20G 4.4G 15G 24% /root/c
root@fdm:~# cat /proc/mounts |grep 'aufs'
none /root/c aufs rw,relatime,si=122df04eea36a4a8 0 0
root@fdm:~# cat /sys/fs/aufs/si_122df04eea36a4a8/br[0-9]
/root/a=rw
/root/b=ro

可以看到/root/a是RW,/root/b是RO,只读状态

1
2
3
4
5
6
7
root@fdm:~# cat /root/c/x
aaa
root@fdm:~# echo ccc >/root/c/x
root@fdm:~# cat a/x
ccc
root@fdm:~# cat b/x
bbb

可以看到a/x的值有发生变化,但是b/x值还是原来的。

1
2
3
4
5
6
7
root@fdm:~# rm -f /root/c/x
root@fdm:~# ls -a /root/c/
. .. a b
root@fdm:~# ls -a a/
. .. a .wh..wh.aufs .wh..wh.orph .wh..wh.plnk .wh.x
root@fdm:~# ls -a b/
. .. b x

我们把/roo/c/x这个文件删除掉,可以看到a的目录下面多出来了.wh.x,但b的目录没有发生变化,而/root/c的目录下面完全看不到x的文件了。

如果遇到了mount: unknown filesystem type 'aufs'这个问题,可以使用apt-get -y install aufs-tools安装。

Aufs文件系统

简介

AuFS的全称是Another UnionFS,后改名为Alternative UnionFS,再后来干脆改名叫作Advance UnionFS。它是对Linux原生UnionFS的重写和改进;但是Linus Torvalds(Linux之父)一直不让AuFS进入Linux内核主干的缘故,所以我们只能在Ubuntu和Debian这些发行版上使用它。

对于AuFS来说,它最关键的目录结构在/var/lib/docker路径下的diff目录:/var/lib/docker/aufs/diff/

实例

运行docker run -d ubuntu:latest sleep 3600就可以自动下载ubuntu的镜像,这个所谓的“镜像”,实际上就是一个Ubuntu操作系统的rootfs,它的内容是Ubuntu操作系统的所有文件和目录。不过,与之前我们讲述的rootfs稍微不同的是,Docker镜像使用的rootfs,往往由多个“层”组成:

1
2
3
4
5
6
7
8
9
root@fdm:~# docker image inspect ubuntu:latest |sed -n '/Layers/,/]/p'
"Layers": [
"sha256:2fb7bfc6145d0ad40334f1802707c2e2390bdcfc16ca636d9ed8a56c1101f5b9",
"sha256:c8dbbe73b68c96e3252f8191226b700d4f4b284154624fa40a2e6a0c42712a0d",
"sha256:1f6b6c7dc482cab1c16d3af058c5fa1782e231cac9aab4d9e06b3f7d77bb1a58",
"sha256:2c77720cf318a4c7eaee757162e6bfc364c3ed83a96a525bc20c548e0f75f1af"
]
root@fdm:~# docker inspect busybox:latest -f '{{json .RootFS.Layers}}'
["sha256:683f499823be212bf04cb9540407d8353803c25d0d9eb5f2fdb62786d8b95ead"]

这个Ubuntu镜像,实际上由四个层组成。这四个层就是四个增量rootfs,每一层都是Ubuntu操作系统文件与目录的一部分;而在使用镜像时,Docker会把这些增量联合挂载在一个统一的挂载点上。而busybox就只有一个层。

以下测试是在busybox镜像上面做的测试。

那这些文件到底存放在哪个目录呢?这里可以查看/proc/mounts,可以看到是存放在/var/lib/docker/aufs/mnt/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1

1
2
3
4
root@fdm:~# cat /proc/mounts| grep 'aufs/mnt'
none on /var/lib/docker/aufs/mnt/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1 type aufs (rw,relatime,si=9c85c7461d7bafc4,dio,dirperm1)
root@fdm:~# ls /var/lib/docker/aufs/mnt/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

可以看到这个目录下,就是一个类似完整的操作系统的目录。我们可以使用si这个ID,你就可以在/sys/fs/aufs下查看被联合挂载在一起的各个层的信息:

1
2
3
4
5
6
7
8
9
root@fdm:~# ll /sys/fs/aufs/si_9c85c7461d7bafc4/br[0-9]
-r--r--r-- 1 root root 4096 Oct 14 09:38 /sys/fs/aufs/si_9c85c7461d7bafc4/br0
-r--r--r-- 1 root root 4096 Oct 14 09:38 /sys/fs/aufs/si_9c85c7461d7bafc4/br1
-r--r--r-- 1 root root 4096 Oct 14 09:38 /sys/fs/aufs/si_9c85c7461d7bafc4/br2
root@fdm:~#
root@fdm:~# cat /sys/fs/aufs/si_9c85c7461d7bafc4/br[0-9]
/var/lib/docker/aufs/diff/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1=rw
/var/lib/docker/aufs/diff/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1-init=ro+wh
/var/lib/docker/aufs/diff/8c6d77459b5ddf3bfaee756737581c4b94b985c51ed3935b1c18ce575f7f4118=ro+wh

我们可以看到,镜像的层都放置在/var/lib/docker/aufs/diff目录下,然后被联合挂载在/var/lib/docker/aufs/mnt里面。

先删除/etc/group这个文件,

1
2
3
4
5
6
7
8
9
### 进入busybox
root@fdm:~# docker exec -it busybox sh
/ # rm -f /etc/group
/ # ls /etc/group
ls: /etc/group: No such file or directory

### 查看 rw 可写层,会发现group变成了.wh.group文件。
root@fdm:~# ls /var/lib/docker/aufs/diff/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1/etc/.wh.group
f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1/etc/.wh.group

注意,之前/var/lib/docker/aufs/diff/f16fcd089ac75b5319eb85a0cb2caf541893d770ad1adf343790698c5624bcd1/etc是没有group这个文件的,只不少在删除的时候,docker会把这个文件从只读层复制至可写层之后,再重命名为.wh.group

分层结构

readonly只读层

它是这个容器的rootfs最下面的四层,对应的正是ubuntu:latest镜像的四层。可以看到,它们的挂载方式都是只读的。

1
2
3
4
5
6
7
8
9
10
root@fdm:~# ls /var/lib/docker/aufs/mnt/0a13bbcb1f33003f70c78d2b0f2fec0f8510dbf7a03f856f6d828b69efb7115a
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@fdm:~# ls /var/lib/docker/aufs/diff/22a356d0aeb66df4cff65bc5f7bcadef55837f604014254d149756d67595eac8
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@fdm:~# ls /var/lib/docker/aufs/diff/b5350f6671f4d17d9e28e2b23a23d2719d0db81fd96a8d4eccaf578da97af857
etc sbin usr var
root@fdm:~# ls /var/lib/docker/aufs/diff/c0c0cf27aa01ad4c246847b2dda10d46ade51001e305943543896ecb9c73453a
var
root@fdm:~# ls /var/lib/docker/aufs/diff/a2eb74802cdbf12bc3d0d2dc92325b25f9263ed10b1087547666cb81407f23ef
run

从最底层,一层一层的查看,可以看出,这些层都以增量的方式分别包含了Ubuntu操作系统的一部分。

可读可写层rw

它是这个容器的rootfs最上面的一层,它的挂载方式为:rw,即read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。

最上面这个可读写层的作用,就是专门用来存放你修改rootfs后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用docker commit和push指令,保存这个被修改过的可读写层,并上传到Docker Hub上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量rootfs的好处。

Init层

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init层是Docker项目单独生成的一个内部层,专门用来存放/etc/hosts、/etc/resolv.conf等信息。

需要这样一层的原因是,这些文件本来属于只读的Ubuntu镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如hostname,所以就需要在可读写层对它们进行修改。

可是,这些修改往往只对当前的容器有效,我们并不希望执行docker commit时,把这些信息连同可读写层一起提交掉。

所以,Docker做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行docker commit只会提交可读写层,所以是不包含这些内容的。

whiteout层

为了实现这样的删除操作,AuFS会在可读写层创建一个whiteout文件,把只读层里的文件“遮挡”起来。

比如,你要删除只读层里一个名叫foo的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo的文件。这样,当这两个层被联合挂载之后,foo文件就会被.wh.foo文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读+whiteout的含义。我喜欢把whiteout形象地翻译为:“白障”。

1
2
root@fdm:~# ls -a /var/lib/docker/aufs/diff/0a13bbcb1f33003f70c78d2b0f2fec0f8510dbf7a03f856f6d828b69efb7115a
. .. .wh..wh.aufs .wh..wh.orph .wh..wh.plnk

在隐藏低层档的情况下,whiteout的名字是.wh.<filename>;在阻止readdir的情况下,名字是.wh..wh..opq 或者 .wh.__dir_opaque

copy-on-write

上面的读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层。知道这一点,就不难理解镜像文件的修改,比如修改一个文件的时候,首先会从上到下查找有没有这个文件,找到,就复制到容器层中,修改,修改的结果就会作用到下层的文件,这种方式也被称为copy-on-write

overlayfs

简介

Overlayfs是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如ext4fs和xfs等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。因此对于用户来说,它所见到的overlay文件系统根目录下的内容就来自挂载时所指定的不同目录的“合集”。

关于原理这一内容,请直接阅读:深入理解overlayfs(一):初识

早期内核中的overlayfs并不支持多lower layer,在Linux-4.0以后的内核版本中才陆续支持完善。而容器中可能存在多层镜像,所以出现了两种overlayfs的挂载方式,早期的overlay不使用多lower layer的方式挂载而overlay2则使用该方式挂载

实例

挂载文件系统的基本命令如下:

1
mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged

其中"lower1:lower2:lower3"表示不同的lower层目录,不同的目录使用”:”分隔,层次关系依次为lower1 > lower2 > lower3(注:多lower层功能支持在Linux-4.0合入,Linux-3.18版本只能指定一个lower dir);然后upper和work目录分别表示upper层目录和文件系统挂载后用于存放临时和间接文件的工作基目录(work base dir),最后的merged目录就是最终的挂载点目录。若一切顺利,在执行以上命令后,overlayfs就成功挂载到merged目录下了。

挂载选项支持(即”-o”参数):

  • 1)lowerdir=xxx:指定用户需要挂载的lower层目录(支持多lower,最大支持500层);
  • 2)upperdir=xxx:指定用户需要挂载的upper层目录;
  • 3)workdir=xxx:指定文件系统的工作基础目录,挂载后内容会被清空,且在使用过程中其内容用户不可见;
  • 4)default_permissions:功能未使用;
  • 5)redirect_dir=on/off:开启或关闭redirect directory特性,开启后可支持merged目录和纯lower层目录的rename/renameat系统调用;
  • 6)index=on/off:开启或关闭index特性,开启后可避免hardlink copyup broken问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@linjx ~]# cd /tmp
[root@linjx tmp]# mkdir -p lower{1..2}/dir upper/dir worker merge
[root@linjx tmp]# for i in 1 2 ;do touch lower$i/foo$i;done
[root@linjx tmp]#
[root@linjx tmp]# touch upper/foo3
[root@linjx tmp]# echo "from lower1" >lower1/dir/aa
[root@linjx tmp]# echo "from lower2" >lower2/dir/aa
[root@linjx tmp]# echo "from lower1 bb" >lower1/dir/bb
[root@linjx tmp]# echo "from upper" >upper/dir/bb
[root@linjx tmp]# mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=worker merge/
[root@linjx tmp]# df -h |grep '/tmp/merge'
overlay 11G 5.2G 4.7G 53% /tmp/merge
[root@linjx tmp]# ll /tmp/merge/
total 4
drwxr-xr-x 1 root root 4096 Jan 19 06:45 dir
-rw-r--r-- 1 root root 0 Jan 19 06:44 foo1
-rw-r--r-- 1 root root 0 Jan 19 06:44 foo2
-rw-r--r-- 1 root root 0 Jan 19 06:44 foo3
[root@linjx tmp]# ll /tmp/merge/dir/
aa bb
[root@linjx tmp]# cat /tmp/merge/dir/aa
from lower1
[root@linjx tmp]# cat /tmp/merge/dir/bb
from upper

以上操作如下图所示:

image

删除文件

跟aufs文件系统一样,也有whiteout文件,但跟aufs不同的是,whiteout文件并非普通文件,而是主次设备号都为0的字符设备(可以通过mknod <name> c 0 0命令手动创建),当用户在merge层通过ls命令(将通过readddir系统调用)检查父目录的目录项时,overlayfs会自动过过滤掉和whiteout文件自身以及和它同名的lower层文件和目录,达到了隐藏文件的目的,让用户以为文件已经被删除了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[root@localhost overlay]# tree
.
├── lower1
│   ├── dir
│   │   ├── aa
│   │   └── bb
│   └── foo1
├── lower2
│   ├── dir
│   │   └── aa
│   └── foo2
├── merge
│   ├── dir
│   │   ├── aa
│   │   └── bb
│   ├── foo1
│   ├── foo2
│   └── foo3
├── upper
│   ├── dir
│   │   └── bb
│   └── foo3
└── worker
└── work
[root@localhost overlay]# rm -f merge/foo1
#删除之后,upper层多了一个foo1文件,但不是普通文件了
[root@localhost overlay]# tree
.
├── lower1
│   ├── dir
│   │   ├── aa
│   │   └── bb
│   └── foo1
├── lower2
│   ├── dir
│   │   └── aa
│   └── foo2
├── merge
│   ├── dir
│   │   ├── aa
│   │   └── bb
│   ├── foo2
│   └── foo3
├── upper
│   ├── dir
│   │   └── bb
│   ├── foo1
│   └── foo3
└── worker
└── work
[root@localhost overlay]# ll upper/foo1
c--------- 1 root root 0, 0 Oct 19 21:29 upper/foo1

overlay合并的原则是upper层会覆盖lower的目录和文件,即当upper dir和lower dir两个目录存在同名文件时,lower dir的文件将会被隐藏,用户只能看见来自upper dir的文件,然后各个lower dir也存在相同的层次关系,较上层屏蔽叫下层的同名文件,放在第一层的优先级最高。对应在docker中,upperdir层为container layer,lowerdir层为Image layer。所以lowerdir则是只读的,当用户想要往来自lower层的文件添加或修改内容时,overlayfs首先会的拷贝一份lower dir中的文件副本到upper dir中,后续的写入和修改操作将会在upper dir下的copy-up的副本文件中进行,lower dir原文件被隐藏。

其他实例,可以详细阅读好文:深入理解overlayfs(二):使用与原理分析

参考资料

  • 本文作者: wumingx
  • 本文链接: https://www.wumingx.com/k8s/docker-rootfs.html
  • 本文主题: docker进阶教程三:rootfs之联合文件系统
  • 版权声明: 本博客所有文章除特别声明外,转载请注明出处!如有侵权,请联系我删除。
0%