> For the complete documentation index, see [llms.txt](https://camelgemonion.gitbook.io/docker/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://camelgemonion.gitbook.io/docker/docker-overview/docker-zhong-de-lian-he-wen-jian-xi-tong.md).

# 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 里面。

![](https://3635233293-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MVjOb1Mk_y7lcQcI_LJ%2F-MXQmqRxgJ__yf7RfFsy%2F-MXQtcd4ARNk8K_IPz7J%2F%E6%88%AA%E5%B1%8F2021-04-04%20%E4%B8%8B%E5%8D%884.01.17.png?alt=media\&token=ce13b89b-121e-4119-92f4-d80227c7f5b4)

**第一部分，只读层。**

它是这个容器的 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。

{% hint style="info" %}
如果需要使用性能更好的overlay2存储驱动，则我们的Linux系统内核版本必须是4.0+。
{% endhint %}

### 使用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， merged`和`workdir`结构。

### overlay2驱动如何工作

`OverlayFS`在单个Linux主机上分层两个目录，并将它们作为单个目录呈现。这些目录称为层，联合过程称为联合挂载。`overlayFS`将`lower`目录称为`lowerdir`，`upper`目录称为`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`构造。

![](https://3635233293-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MVjOb1Mk_y7lcQcI_LJ%2F-MWJs35hYcGjWS7vbuHm%2F-MWJuU42uX53L2ozuDGp%2F%E6%88%AA%E5%B1%8F2021-03-21%20%E4%B8%8B%E5%8D%889.11.57.png?alt=media\&token=35d1a17b-8ad5-42d6-addc-92ec2ede66d8)

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

`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
```

当我们使用`docker`的`overlay`存储驱动时，我们可以在容器内部通过`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`进行读取访问。

* &#x20;**在容器层，文件不存在：**&#x5982;果一个容器需要打开一个文件进行访问而文件并不存在于容器层中`(upperdir)`，那么它会从镜像层进行读取`(lowerdir)`。这会产生非常小性能开销。
* **文件只存在于容器层:**  如果一个容器需要打开一个文件进行访问且该文件存在于容器层中`(upperdir)`，但并不存在于镜像层`(lowerdir)`，那么它会直接从容器层中进行读取。
* **文件既存在于容器层也存在于读写层：**&#x5982;果一个容器需要打开一个文件进行访问且该文件既存在于容器层中`(upperdir)`，也存在于镜像层中`(lowerdir)`，那么会读取容器层中的文件。容器层`(upperdir)`中的文件会遮蔽镜像层中的同名文件。

### 修改文件或目录

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

* **第一次写入一个文件:**  容器第一次对一个已经存在的文件进行写入时，文件并不在容器层`(uppdir)`中。`overlay/overlay2`驱动会执行一个`copy_up`的操作来将镜像层`(lowerdir)`中的文件拷贝`(upperdir)`中。容器会将一些修改写入到新拷贝的文件中。然而，`OverlayFS`作用于文件层而不是文件块层。这意味所有`OverlayFS`的`copy_up`会执行整个文件的拷贝，即使件非常的大并且只修改了文件的一小部分。这会很明显的影响容器写的性能。然而，有两件事值得注意: &#x20;
  * `copy_up`操作只会在第一次对给定文件写入时发生。后续对同一文件的写入操作会作用于新拷贝的文件。
  * `OverlayFS`只工作于两层。这意味着性能应该优于`AUFS`，`AUFS`在搜索具有许多层的镜像中的文件时可能会遭受明显的延迟。此优势应用于`overlayfs`和`overlay2`驱动程序。一开始读取文件时，`overlay2`的性能略低于`overlay`，因为它必须查看更多的层，但它会缓存结果，因此这只是一个小的损失。
* **删除文件和目录：**
  * 当一个文件在容器内被删除时，在容器`(upperdir)`中会创建一个whiteout文件。镜像层中的文件`(lowerdir)`不会被删除(因为`lowerdir`是只读的)。但是，whiteout文件会阻止它用于容器。
  * 当一个目录在容器内被删除时，在容器`(upperdir)`中会创建一个opaque目录。它与whiteout文件的工作方式相同，会有效的阻止目录被访问，即使它依然存在于镜像层中`(lowerdir)`。
* **重命名目录：**&#x4EC5;当源和目标路径都位于顶层时，才允许为目录调用`rename(2)`。否则，它会返回 `EXDEV`错误("不允许交叉设备链接")。我们的应用程序需要设计为能够处理`EXDEV`，并回退到"copy和unlink"策略。

### OverlayFS和Docker性能

* **Page Caching:** `OverlayFS`支持页面缓存共享。访问同一文件的多个容器共享该文件的单个页面缓存条目。这使得`overlay`和`overlay2`驱动程序可以高效地使用内存，并且是PaaS等高密度使用案例的一个不错的选择。
* **copy\_up*****:*** 与`AUFS`一样，每当容器首次写入文件时，`overlay`都会执行复制操作。这对于写入操作会增加一定的延迟，尤其是对于大文件。但是，一旦文件被复制，所有后续写入该文件都发生在容器层，无需进一步复制操作。`OverlayFS`的`copy_up`操作比`AUFS`同样的操作更快，因为`AUFS`支持比`overlay`更多的层，如果通过许多`AUFS`层进行搜索，可能会产生更大的延迟。`overlay2`也支持多层，但它通过缓存来提高性能。
* **Inode limits:** 使用老旧的overlay存储驱动会导致`inode`消耗溢出。这在拥有大量镜像和容器的docker主机尤为明显。增加inodes可用数量的唯一方式是重新格式化文件系统。为了避免这种情况发生，强烈推荐使用`overlay2`存储驱动。

#### 关于性能的最佳实践

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

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