容器镜像 rootfs(根文件系统)

挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
一个最常见的 rootfs,或者说容器镜像,会包括如下所示的一些目录和文件,比如 /bin,/etc,/proc 等等:

  1. $ ls /
  2. bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var

进入容器之后执行的 /bin/bash,就是 /bin 目录下的可执行文件,与宿主机的 /bin/bash 完全不同。
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  • 启用 Linux Namespace 配置;
  • 设置指定的 Cgroups 参数;
  • 切换进程的根目录(Change Root)。

这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别。

rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。

共享内核

同一台机器上的所有容器,都共享宿主机操作系统的内核。
如果应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,需要注意:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

容器运行环境的一致性

由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。并不是局限在编程语言层面。

容器镜像“打包操作系统”的能力保证了依赖也就是运行环境的一致性。

layer和联合文件系统UnionFS

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

  1. $ tree
  2. .
  3. ├── A
  4. ├── a
  5. └── x
  6. └── B
  7. ├── b
  8. └── x

使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

  1. $ mkdir C
  2. $ mount -t aufs -o dirs=./A:./B none ./C

再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

  1. $ tree ./C
  2. ./C
  3. ├── a
  4. ├── b
  5. └── x

如果文件名相同,前面的文件会覆盖后面的文件,这里A中的x会覆盖B中的x。

AuFS 的全称是 Another UnionFS —> Alternative UnionFS —> Advance UnionFS

AUFS

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

  1. /var/lib/docker/aufs/diff/<layer_id>

busybox的layer

  1. $ docker image inspect busybox
  2. ...
  3. "RootFS": {
  4. "Type": "layers",
  5. "Layers": [
  6. "sha256:1be74353c3d0fd55fb5638a52953e6f1bc441e5b1710921db9ec2aa202725569"
  7. ]
  8. }

增量 rootfs 每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上(等价于前面例子里的“/C”目录)。
挂载点就是 /var/lib/docker/aufs/mnt/。

通过查看 AuFS 的挂载信息,我们可以找到这个目录对应的 AuFS 的内部 ID(也叫:si):

  1. $ cat /proc/mounts| grep aufs
  2. none /root/clang/C aufs rw,relatime,si=9c6a0973fc494d5e 0 0
  3. none /var/lib/docker/aufs/mnt/0aae6be2cc861738f09beb41292bb344b73180592ef19cf9701d686ef37bb142 aufs rw,relatime,si=9c6a0973c851fd5e,dio,dirperm1 0 0

然后使用这个 ID,就可以在 /sys/fs/aufs 下查看被联合挂载在一起的各个层的信息:

  1. cat /sys/fs/aufs/si_9c6a0973c851fd5e/*
  2. /var/lib/docker/aufs/diff/0aae6be2cc861738f09beb41292bb344b73180592ef19cf9701d686ef37bb142=rw
  3. /var/lib/docker/aufs/diff/0aae6be2cc861738f09beb41292bb344b73180592ef19cf9701d686ef37bb142-init=ro+wh
  4. /var/lib/docker/aufs/diff/e97b0a36bd2f0073cbcdde06659e9a19d7537cfdc564cfa56ddf08a4c8ce9674=ro+wh
  5. 64
  6. 65
  7. 66
  8. /dev/shm/aufs.xino

镜像的层都放置在 /var/lib/docker/aufs/diff 目录下,然后被联合挂载在 /var/lib/docker/aufs/mnt 里面.

分层介绍

只读层

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的。简单理解,里面是除了内核外的文件系统。

读写层

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

为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来。
最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。

init层

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。

所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

其他

  1. https://coolshell.cn/articles/17010.html LINUX NAMESPACE
  2. 在容器中修改文件时,Docker会从上到下依次在各镜像层中查找比文件。找到后,会把此文件复制到容器层(可读写层),然后修改。这就是 Copy on Write.
  3. linuxkit https://www.cnblogs.com/allcloud/p/7095065.html
  4. run 容器的时候 使用—dns 来指定dns 服务器覆盖init层的/etc/resolv.conf
  5. 现在默认用 overlay2 作为 storage driver 主要是性能更好。而且 OverlayFS 已经进入 Linux 内核主线,