一、简介

  1. 联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。联合文件系统是实现Docker镜像的技术基础。
  2. Docker镜像可以通过分层来进行继承。例如,用户基于基础镜像来制作各种不同的应用镜像。这些镜像共享同一个基础镜像层,提高了存储效率。此外,当用户改变了一个Docker镜像(比如升级程序到新的版本),则会创建一个新的层(layer)。因此,用户不用替换整个原镜像或者重新建立,只需要添加新层即可。用户分发镜像的时候,也只需要分发被改动的新层内容(增量部分)。这让Docker的镜像管理变得十分轻量和快速。

    二、Docker存储原理

  3. Docker目前通过插件化方式支持多种文件系统后端。Debian/Ubuntu上成熟的AUFS,就是一种联合文件系统实现。AUFS支持为每一个成员目录(类似Git的分支)设定只读(readonly)、读写(readwrite)或写出(whiteout-able)权限,同时AUFS里有一个类似分层的概念,对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。

  4. Docker镜像自身就是由多个文件层组成,每一层有基于内容的唯一的编号(层ID)。对于Docker镜像来说,这些层的内容都是不可修改的、只读的。而当Docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂载一个新的可读写的层给容器。容器中的内容更新将会发生在可读写层。当所操作对象位于较深的某层时,需要先复制到最上层的可读写层。当数据对象较大时,往往意味着较差的IO性能。因此,对于IO敏感型应用,一般推荐将容器修改的数据通过volume方式挂载,而不是直接修改镜像内数据。

    三、Docker存储结构

  5. 所有的镜像和容器都存储都在Docker指定的存储目录下,以Ubuntu宿主系统为例,默认路径是/var/lib/docker。在这个目录下面,存储由Docker镜像和容器运行相关的文件和目录。

  6. 其中,如果使用AUFS存储后端,则最关键的就是aufs目录,保存Docker镜像和容器相关数据和信息。包括layers、diff和mnt三个子目录。
  • layers子目录包含层属性文件,用来保存各个镜像层的元数据:某镜像的某层下面包括哪些层。
  • diff子目录包含层内容子目录,用来保存所有镜像层的内容数据
  • mnt子目录下面的子目录是各个容器最终的挂载点,所有相关的AUFS层在这里挂载到一起,形成最终效果。一个运行中容器的根文件系统就挂载在这下面的子目录上

    四、分析

  1. 我们启动一个容器,比如:$ docker run -d ubuntu:latest sleep 3600
  • 这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的是,Docker 镜像使用的 rootfs,往往由多个“层”组成:

    1. $ docker image inspect ubuntu:latest
    2. ...
    3. "RootFS": {
    4. "Type": "layers",
    5. "Layers": [
    6. "sha256:f49017d4d5ce9c0f544c...", "sha256:8f2b771487e9d6354080...", "sha256:ccd4d61916aaa2159429...", "sha256:c01d74f99de40e097c73...", "sha256:268a067217b5fe78e000..."
    7. ]
    8. }
  • 可以看到,这个 Ubuntu 镜像,实际上由五个层组成。这五个层就是五个增量 rootfs,每一层都是 Ubuntu 操作系统文件与目录的一部分;而在使用镜像时,Docker 会把这些增量联合挂载在一个统一的挂载点上。这个挂载点就是 /var/lib/docker/aufs/mnt/,比如:/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e

  • 不出意外的,这个目录里面正是一个完整的 Ubuntu 操作系统:

    1. $ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
    2. bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
  • 那么,前面提到的五个镜像层,又是如何被联合挂载成这样一个完整的 Ubuntu 文件系统的呢?

这个信息记录在 AuFS 的系统目录 /sys/fs/aufs 下面。

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

    1. $ cat /proc/mounts| grep aufsnone /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0
    2. 即,si=972c6d361e6b32ba
  • 然后使用这个 ID,你就可以在 /sys/fs/aufs 下查看被联合挂载在一起的各个层的信息:

    1. $ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
    2. /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
    3. /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
    4. /var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
    5. /var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
    6. /var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
    7. /var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
    8. /var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh
  • 从这些信息里,我们可以看到,镜像的层都放置在 /var/lib/docker/aufs/diff 目录下,然后被联合挂载在 /var/lib,而且,从这个结构可以看出来,这个容器的 rootfs 由如下图所示的三部分组成:

image.png

1. 第一部分,只读层。

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的。
这时,我们可以分别查看一下这些层的内容:

  1. $ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0
  2. etc sbin usr var
  3. $ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2
  4. run
  5. $ ls /var/lib/docker/aufs/diff/a524a729adadedb900
  6. bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

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

2.第二部分,可读写层。

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。
可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?
为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。
比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。
所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。

3.第三部分,Init层。

它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。
可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。
所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。
最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。