docker中的联合文件系统

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

$ tree
.
├── A
│  ├── a
│  └── x
└── B
  ├── b
  └── x

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

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

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

$ tree ./C
./C
├── a
├── b
└── x

可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。

在ubuntu上默认使用的是AUFS这个联合文件系统驱动。

AuFS 的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来干脆改名叫作 Advance UnionFS。

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

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

而这个目录的作用,我们不妨通过一个具体例子来看一下。现在,我们启动一个容器,比如:

$ docker run -d ubuntu:latest sleep 3600

这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的是,Docker 镜像使用的 rootfs,往往由多个“层”组成:

$ docker image inspect ubuntu:latest
...
     "RootFS": {
      "Type": "layers",
      "Layers": [
        "sha256:f49017d4d5ce9c0f544c...",
        "sha256:8f2b771487e9d6354080...",
        "sha256:ccd4d61916aaa2159429...",
        "sha256:c01d74f99de40e097c73...",
        "sha256:268a067217b5fe78e000..."
      ]
    }

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

/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e

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

$ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
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):

$ cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0

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

$ cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh

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

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

第一部分,只读层。

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

可以分别查看一下这些层的内容:

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

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

第二部分,可读写层。

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

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

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

所以,最上面这个可读写层的作用,就是专门用来存放修改 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 操作系统供容器使用。

下面的内容翻译自官方文档:描述的是overlay和overlay2相关的一些特性:在有些地方与AUFS驱动提供的AUFS联合文件系统并不相同。

docker的联合文件系统的特性取决于它所使用的存储驱动,不同的存储驱动的性能是不同的。例如:OverlayFS是一个现代化的类似于AUFS的联合文件系统,但是它的性能更快而且底层实现更简单。Docker提供了两种OverlayFS的驱动类型:最初的overlay和最新的最稳定的overlay2。

如果需要使用性能更好的overlay2存储驱动,则我们的Linux系统内核版本必须是4.0+。

使用overylay或overlay2来配置docker

一般情况下推荐使用overlay2驱动而不是overlay驱动。overlay驱动不支持Docker EE版本。

如果需要使用overlay来配置docker,我们的docker主机必须运行在3.18或者更高的内核版本之上。对于overlay2驱动,我们的内核版本必须4.0+。

下面是配置docker存储驱动的步骤:

停止容器

$ sudo systemctl stop docker

/var/lib/docker下面的内容拷贝到临时目录。

$ cp -au /var/lib/docker /var/lib/docker.bk

如果想要使用一个独立的备份文件系统,可以格式化这个文件系统并将它挂载到/var/lib/docker。为了确保永久生效,将该配置记录添加到/etc/fstab中。

编辑/etc/docker/daemon.json文件。如果这个文件不存在,则创建它。添加下面的内容:

{
  "storage-driver": "overlay2"
}

如果该配置文件包含错误的JSON格式,docker不会正常启动。

启动容器。

$ sudo systemctl start docker

使用docker info命令来查看配置的存储驱动是否生效:

$ docker info

Containers: 0
Images: 0
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true
 Native Overlay Diff: true
<output truncated>

Docker现在正在使用overlay2存储驱动并且自动创建了overlay挂载所需要的lowerdir,upperdir, mergedworkdir结构。

overlay2驱动如何工作

OverlayFS在单个Linux主机上分层两个目录,并将它们作为单个目录呈现。这些目录称为层,联合过程称为联合挂载。overlayFSlower目录称为lowerdirupper目录称为upperdir。联合视图通过merged目录暴露出来。

overlay2驱动程序原生支持多达128个lower overlayFS层。此功能为与层相关的Docker命令(如docker build和docker commit)提供了更好的性能,并且在备份文件系统上消耗更少的inodes

overlay驱动如何工作

overlay在单个Linux主机上分层两个目录,并将它们作为单个目录呈现。这些目录称为层并且联合的过程被称为联合挂载。overlay将lower目录称为lowerdir,upper目录称为upperdir。联合视图通过其称为merged的目录暴露出来。

下图展示了Docker镜像和Docker容器的分层方式。镜像层为lowerdir,容器层为upperdir。联合视图通过称为merged的目录暴露给用户,该目录实际上是容器的挂载点。下图显示了Docker构建如何映射到OverlayFS构造。

镜像层和容器层如果包含相同名称的文件,容器层会获得优选,而镜像层中相同的文件会被遮蔽起来。

overlay驱动只作用于两层。这意味着多层镜像无法实现多个OverlayFS层。相反,每一个镜像层作为自己的目录在/var/lib/docker/overlay目录下面实现。它使用硬链接的方式来引用lower层共享的数据。硬链接的使用会导致inodes使用溢出。这是overlady存储驱动的一个性能限制。

要创建一个容器,overlay驱动程序将镜像的顶层目录与容器的新目录相结合。镜像的顶层是overlay中的lowerdir,是只读的。容器的新目录是upperdir,是可写的。

磁盘上的镜像层和容器层

当我们使用docker pull ubuntu下载一个五层镜像以后,我们可以在/var/lib/docker/overlay2下面看到6个目录:

$ ls -l /var/lib/docker/overlay2

total 24
drwx------ 5 root root 4096 Jun 20 07:36 223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
drwx------ 3 root root 4096 Jun 20 07:36 3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b
drwx------ 5 root root 4096 Jun 20 07:36 4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1
drwx------ 5 root root 4096 Jun 20 07:36 e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5
drwx------ 5 root root 4096 Jun 20 07:36 eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed
drwx------ 2 root root 4096 Jun 20 07:36 l

新的l层(小写的L)目录包含了作为符号链接的缩短的层标识符。这些缩短的标识符用来避免触发mount命令的参数的长度限制。

$ ls -l /var/lib/docker/overlay2/l

total 20
lrwxrwxrwx 1 root root 72 Jun 20 07:36 6Y5IM2XC7TSNIJZZFLJCS6I4I4 -> ../3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 B3WWEFKBG3PLLV737KZFIASSW7 -> ../4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 JEYMODZYFCZFYSDABYXD5MF6YO -> ../eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 NFYKDW6APBCCUCTOUSYDH4DXAT -> ../223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 UL2MW33MSE3Q5VYIKBRN4ZAGQP -> ../e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5/diff

最低层包含一个名为link的文件,文件中包含缩短的标识符的名称,以及一个名为diff的目录,其中包含该层的内容。

$ ls /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/

diff  link

$ cat /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/link

6Y5IM2XC7TSNIJZZFLJCS6I4I4

$ ls  /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff

bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

次低层和每个较高层包含一个名为lower的文件,该文件表示其父级,以及一个称为diff的目录,其中包含其内容。它还包含一个merged目录,其中包含其父层和自身的联合内容,以及一个由OverlayFS在内部使用的work目录。

$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7

diff  link  lower  merged  work

$ cat /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/lower

l/6Y5IM2XC7TSNIJZZFLJCS6I4I4

$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff/

etc  sbin  usr  var

当我们使用dockeroverlay存储驱动时,我们可以在容器内部通过mount命令查看存在哪些挂载。

$ mount | grep overlay

overlay on /var/lib/docker/overlay2/9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/merged
type overlay (rw,relatime,
lowerdir=l/DJA75GUWHWG7EWICFYX54FIOVT:l/B3WWEFKBG3PLLV737KZFIASSW7:l/JEYMODZYFCZFYSDABYXD5MF6YO:l/UL2MW33MSE3Q5VYIKBRN4ZAGQP:l/NFYKDW6APBCCUCTOUSYDH4DXAT:l/6Y5IM2XC7TSNIJZZFLJCS6I4I4,
upperdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/diff,
workdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/work)

第二行表明overlay挂载是读写的。

容器如何通过overlay或overlay2进行读写

读取文件

考虑三种情况,其中容器打开一个文件,以便使用overlay进行读取访问。

  • 在容器层,文件不存在:如果一个容器需要打开一个文件进行访问而文件并不存在于容器层中(upperdir),那么它会从镜像层进行读取(lowerdir)。这会产生非常小性能开销。

  • 文件只存在于容器层: 如果一个容器需要打开一个文件进行访问且该文件存在于容器层中(upperdir),但并不存在于镜像层(lowerdir),那么它会直接从容器层中进行读取。

  • 文件既存在于容器层也存在于读写层:如果一个容器需要打开一个文件进行访问且该文件既存在于容器层中(upperdir),也存在于镜像层中(lowerdir),那么会读取容器层中的文件。容器层(upperdir)中的文件会遮蔽镜像层中的同名文件。

修改文件或目录

思考修改容器中文件的场景。

  • 第一次写入一个文件: 容器第一次对一个已经存在的文件进行写入时,文件并不在容器层(uppdir)中。overlay/overlay2驱动会执行一个copy_up的操作来将镜像层(lowerdir)中的文件拷贝(upperdir)中。容器会将一些修改写入到新拷贝的文件中。然而,OverlayFS作用于文件层而不是文件块层。这意味所有OverlayFScopy_up会执行整个文件的拷贝,即使件非常的大并且只修改了文件的一小部分。这会很明显的影响容器写的性能。然而,有两件事值得注意:

    • copy_up操作只会在第一次对给定文件写入时发生。后续对同一文件的写入操作会作用于新拷贝的文件。

    • OverlayFS只工作于两层。这意味着性能应该优于AUFSAUFS在搜索具有许多层的镜像中的文件时可能会遭受明显的延迟。此优势应用于overlayfsoverlay2驱动程序。一开始读取文件时,overlay2的性能略低于overlay,因为它必须查看更多的层,但它会缓存结果,因此这只是一个小的损失。

  • 删除文件和目录:

    • 当一个文件在容器内被删除时,在容器(upperdir)中会创建一个whiteout文件。镜像层中的文件(lowerdir)不会被删除(因为lowerdir是只读的)。但是,whiteout文件会阻止它用于容器。

    • 当一个目录在容器内被删除时,在容器(upperdir)中会创建一个opaque目录。它与whiteout文件的工作方式相同,会有效的阻止目录被访问,即使它依然存在于镜像层中(lowerdir)

  • 重命名目录:仅当源和目标路径都位于顶层时,才允许为目录调用rename(2)。否则,它会返回 EXDEV错误("不允许交叉设备链接")。我们的应用程序需要设计为能够处理EXDEV,并回退到"copy和unlink"策略。

OverlayFS和Docker性能

  • Page Caching: OverlayFS支持页面缓存共享。访问同一文件的多个容器共享该文件的单个页面缓存条目。这使得overlayoverlay2驱动程序可以高效地使用内存,并且是PaaS等高密度使用案例的一个不错的选择。

  • copy_up: AUFS一样,每当容器首次写入文件时,overlay都会执行复制操作。这对于写入操作会增加一定的延迟,尤其是对于大文件。但是,一旦文件被复制,所有后续写入该文件都发生在容器层,无需进一步复制操作。OverlayFScopy_up操作比AUFS同样的操作更快,因为AUFS支持比overlay更多的层,如果通过许多AUFS层进行搜索,可能会产生更大的延迟。overlay2也支持多层,但它通过缓存来提高性能。

  • Inode limits: 使用老旧的overlay存储驱动会导致inode消耗溢出。这在拥有大量镜像和容器的docker主机尤为明显。增加inodes可用数量的唯一方式是重新格式化文件系统。为了避免这种情况发生,强烈推荐使用overlay2存储驱动。

关于性能的最佳实践

以下通用性能最佳实践也适用于OverlayFS:

  • 使用快速存储: 使用SSD来提供共快速的读写性能。

  • 使用卷来应对写入比较频繁的工作负载:卷为写入频繁的工作负载提供最佳和可预测的性能。这是因为它们绕过了存储驱动程序,并且不会产生任何由thin provisioningcow引入的潜在开销。除了这些,卷还有其他好处,例如允许我们在容器之间共享数据,即使没有容器在使用数据,也可以持久性的保存数据。

Last updated