深入 Docker 存储引擎

1,955 阅读5分钟

前言

Docker 存储引擎对于普通开发人员来说可能并不关心,主要是性能和稳定性上的综合考虑,不过 Docker 存储引擎的设计思想还是非常值得学习的。

设计思想

Docker 存储引擎的核心思想是“层”的概念,理解了这个层,就基本可以理解它的设计思路。

当我们拉取一个 Docker 镜像的时候,往往看到如下界面。

一个镜像被分成许多的“层”,每“层”包含了若干的文件,而一层层堆叠起来就组成了我们的一个完整的镜像。我们镜像中的文件就是所有“层”文件的并集。

我们构建 Docker 镜像一般采用 Dockerfile 的方式,而 Dockerfile 的每行命令,其实就会生成一个“层”,即使什么文件都没有添加。

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

Docker 的镜像(image)是静态的,所以当镜像构建完成后,所有的层都是只读的,并会赋予一个唯一的 ID。而容器(container)是动态的,当容器启动后,Docker 会给这个容器创建一个可读写“层”,位于所有镜像“层”的最上面。我们对容器的所有操作也就是在这个“层”里完成,当我们执行 docker commit 将容器生成镜像的时候,就是把这个“层”给拍了个快照,添加了一个新的只读层。

文件的创建是在读写层增加文件,那修改和删除呢?

这就要提一下 Docker 设计的 copy-on-write (CoW) 策略。

当我们试图读取一个文件时,Docker 会从上到下一层层去找这个文件,找到的第一个就是我们的文件。所以下面层相同的文件就被“覆盖”了。而修改就是当我们找到这个文件时,将它“复制”到读写层并修改,这样读写层的文件就是我们修改后的文件,并且“覆盖”了镜像中的文件了。而删除就是创建了一个特殊的 whiteout 文件,这个 whiteout 文件覆盖的文件即表示删除了。

这样的设计有什么好处吗?

显而易见的就是减少了存储空间,由于镜像被分成了多个层,而各个层是静态只读的,是可以共享的。当你从一个镜像构建另一个镜像时,只需要添加新的层,原有的层不会被复制。

我们可以用 docker history 命令查看我们创建的镜像,相同的层将共享且只保存一份。

我们可以在系统的 /var/lib/docker/<存储驱动>/ 下看到我们所有的层。

第二个好处是启动容器就变得非常轻量和快速。因为我们的容器只是添加了一个“空”的读写层,其他的都是复用的只读层,需要用时才会去搜索。

因此,服务器上存储了上百个镜像,启动了上千个容器,一点也不费力。

存储驱动

Docker 的存储引擎设计思路是这样,但是针对不同的文件系统,是由不同的存储驱动去实现的。下面我们来聊聊 Docker 的存储驱动。

Docker 主要有一下几类存储驱动:

  • overlay2:是当前版本推荐的存储驱动,无需额外的依赖和配置即可发挥绝佳的性能。在 18.09 版本之后替换了 overlay 存储驱动。支持 xfs,ext4 文件系统。
  • aufs:Docker 早期使用的存储驱动,是 Docker 18.06 版本之前,Ubuntu 14.04 版本前推荐的。支持 xfs,ext4 文件系统。
  • devicemapper:是较早版本的 CentOS 和 RHEL 系统推荐的存储驱动,因为它们不支持 overlay2,需要 direct-lvm 的支持。
  • btrfs:仅用于 btrfs 文件系统。
  • zfs:仅用于 zfs 文件系统。
  • vfs:不依赖于文件系统,但是性能奇差,主要用来测试。

需要注意的是,overlay2,overlay,aufs 的层是基于文件的,当单文件的写并发较高时需要大内存的支持,且读写层可能因为单个文件而变得很大。devicemapper,btrfs,zfs 的层是基于块存储的,因此对于单个文件的高并发影响不大。但是 btrfs 和 zfs 非常消耗内存。

有条件的情况下,我们还是建议选择 overlay2 的存储驱动。

配置存储驱动

配置 Docker 存储驱动非常简单,只需要修改配置文件即可。

注意,如果你原先有不同存储驱动的层数据,更换存储驱动后将不可用,建议备份镜像并清除 /var/lib/docker 下所有数据。

备份镜像可以用 docker save 导出镜像,之后用 docker load 导入镜像。

创建或修改文件 /etc/docker/daemon.json 并添加

{
  "storage-driver": "overlay2"
}

然后重启 Docker

systemctl restart docker

OverlayFS

下面我们重点讲讲 overlayFS(overlay2 和 overlay)。

overlayFS 是从 aufs 之上改进和简化而来的,比 aufs 和 devicemapper 有更好的性能,大部分情况下也比 btrfs 好。

它将文件简化为上、下两层,上面的称为 upperdir,可读写,下面的称为 lowerdir,只读,统一后暴露的视图称为 merged

它有如下特性:

页缓存:overlayFS 支持页缓存分享,多个容器如果读取相同层的同一个文件,可以共享页缓存,有效利用内存,使得它对于高并发读场景十分高效。

层查找:由于第一次修改只读层文件时需要复制到读写层,所以对于大文件会有一些延迟。但是 overlayFS 还是比 aufs 更快,因为在搜索和缓存方面做了不少优化。

重命名:overlayFS 不支持不同层文件的重命名操作,需要修改为复制然后删除。

最佳实践

  • 尽可能使用 SSD 等高性能存储
  • 对于大量写场景,建议使用外挂盘(Volume)的方式

参考