使用 ZFS 存储驱动程序

ZFS 是一种下一代文件系统,支持许多高级存储技术,例如卷管理、快照、校验和、压缩和重复数据删除、复制等等。

它由 Sun Microsystems(现为 Oracle Corporation)创建,并在 CDDL 许可下开源。由于 CDDL 和 GPL 之间存在许可证不兼容性,因此 ZFS 无法作为 mainline Linux 内核的一部分提供。但是,ZFS On Linux (ZoL) 项目提供了一个树外内核模块和用户空间工具,可以单独安装。

ZFS on Linux (ZoL) 端口健康且正在成熟。但是,目前不建议将 zfs Docker 存储驱动程序用于生产环境,除非您拥有丰富的 ZFS on Linux 使用经验。

注意:Linux 平台上还存在 ZFS 的 FUSE 实现。不推荐这样做。原生 ZFS 驱动程序 (ZoL) 经过更多测试,性能更高,并且使用更广泛。本文档的其余部分将参考原生 ZoL 端口。

先决条件

  • ZFS 需要一个或多个专用块设备,最好是固态硬盘 (SSD)。
  • /var/lib/docker/ 目录必须挂载在 ZFS 格式化的文件系统上。
  • 更改存储驱动程序会使您已创建的所有容器在本地系统上无法访问。使用 docker save 保存容器,并将现有镜像推送到 Docker Hub 或私有仓库,这样您就不必以后重新创建它们。

注意

无需使用 MountFlags=slave,因为 dockerdcontainerd 位于不同的挂载命名空间中。

使用 zfs 存储驱动程序配置 Docker

  1. 停止 Docker。

  2. /var/lib/docker/ 的内容复制到 /var/lib/docker.bk,并删除 /var/lib/docker/ 的内容。

    $ sudo cp -au /var/lib/docker /var/lib/docker.bk
    
    $ sudo rm -rf /var/lib/docker/*
    
  3. 在您的专用块设备或设备上创建一个新的 zpool,并将其挂载到 /var/lib/docker/ 中。请确保您指定了正确的设备,因为这是一个破坏性操作。此示例将两个设备添加到池中。

    $ sudo zpool create -f zpool-docker -m /var/lib/docker /dev/xvdf /dev/xvdg
    

    该命令创建 zpool 并将其命名为 zpool-docker。该名称仅用于显示目的,您可以使用其他名称。使用 zfs list 检查池是否已成功创建并挂载。

    $ sudo zfs list
    
    NAME           USED  AVAIL  REFER  MOUNTPOINT
    zpool-docker    55K  96.4G    19K  /var/lib/docker
    
  4. 配置 Docker 以使用 zfs。编辑 /etc/docker/daemon.json 并将 storage-driver 设置为 zfs。如果该文件之前为空,现在应该看起来像这样

    {
      "storage-driver": "zfs"
    }

    保存并关闭文件。

  5. 启动 Docker。使用 docker info 验证存储驱动程序是否为 zfs

    $ sudo docker info
      Containers: 0
       Running: 0
       Paused: 0
       Stopped: 0
      Images: 0
      Server Version: 17.03.1-ce
      Storage Driver: zfs
       Zpool: zpool-docker
       Zpool Health: ONLINE
       Parent Dataset: zpool-docker
       Space Used By Parent: 249856
       Space Available: 103498395648
       Parent Quota: no
       Compression: off
    <...>
    

管理 zfs

增加正在运行设备的容量

要增加 zpool 的大小,您需要向 Docker 主机添加一个专用块设备,然后使用 zpool add 命令将其添加到 zpool

$ sudo zpool add zpool-docker /dev/xvdh

限制容器的可写存储配额

如果要实施基于每个镜像/数据集的配额,可以将 size 存储选项设置为限制单个容器可用于其可写层的空间量。

编辑 /etc/docker/daemon.json 并添加以下内容

{
  "storage-driver": "zfs",
  "storage-opts": ["size=256M"]
}

请参阅 守护程序参考文档 中每个存储驱动程序的所有存储选项

保存并关闭文件,然后重新启动 Docker。

zfs 存储驱动程序的工作原理

ZFS 使用以下对象

  • 文件系统:精简配置,根据需要从 zpool 分配空间。
  • 快照:文件系统的空间高效的只读时间点副本。
  • 克隆:快照的可写副本。用于存储与上一层的差异。

创建克隆的过程

ZFS snapshots and clones
  1. 从文件系统创建一个只读快照。
  2. 从快照创建一个可写克隆。它包含与父层的任何差异。

文件系统、快照和克隆都从底层 zpool 分配空间。

磁盘上的镜像和容器层

每个正在运行的容器的统一文件系统都挂载在 /var/lib/docker/zfs/graph/ 中的挂载点上。请继续阅读以了解统一文件系统的组成方式。

镜像分层和共享

镜像的基础层是一个 ZFS 文件系统。每个子层都是基于其下方层的 ZFS 快照的 ZFS 克隆。容器是从它创建的镜像的顶层创建的 ZFS 快照的 ZFS 克隆。

下图显示了如何将基于两层镜像的正在运行的容器组合在一起。

ZFS pool for Docker container

启动容器时,以下步骤按顺序发生

  1. 镜像的基础层在 Docker 主机上作为 ZFS 文件系统存在。

  2. 其他镜像层是托管直接下方镜像层的数据集的克隆。

    在图中,“层 1”是通过对基础层进行 ZFS 快照然后从该快照创建克隆而添加的。该克隆是可写的,并根据需要从 zpool 占用空间。该快照是只读的,将基础层保持为不可变对象。

  3. 启动容器时,会在镜像之上添加一个可写层。

    在图中,容器的可写层是通过对镜像的顶层(层 1)进行快照然后从该快照创建克隆而创建的。

  4. 容器修改其可写层的内容时,会为更改的块分配空间。默认情况下,这些块为 128k。

zfs 如何处理容器读写操作

读取文件

每个容器的可写层都是一个 ZFS 克隆,它与创建它的数据集(其父层快照)共享所有数据。即使要读取的数据来自较深的层,读取操作也是快速的。此图说明了块共享的工作原理

ZFS block sharing

写入文件

写入新文件:根据需要从底层 zpool 分配空间,并将块直接写入容器的可写层。

修改现有文件:仅为更改的块分配空间,并使用写时复制 (CoW) 策略将这些块写入容器的可写层。这可以最大程度地减小层的尺寸并提高写入性能。

删除文件或目录:

  • 删除位于较低层的文件或目录时,ZFS 驱动程序会屏蔽容器的可写层中文件或目录的存在,即使文件或目录仍然存在于较低的只读层中。
  • 如果在容器的可写层中创建然后删除文件或目录,则块将被 zpool 回收。

ZFS 和 Docker 性能

有几个因素会影响使用 zfs 存储驱动程序的 Docker 的性能。

  • 内存:内存对 ZFS 性能有很大影响。ZFS 最初是为具有大量内存的大型企业级服务器设计的。

  • ZFS 功能:ZFS 包含一个重复数据删除功能。使用此功能可以节省磁盘空间,但会使用大量内存。建议您为与 Docker 一起使用的 zpool 禁用此功能,除非您使用 SAN、NAS 或其他硬件 RAID 技术。

  • ZFS 缓存: ZFS 将磁盘块缓存在一个名为自适应替换缓存 (ARC) 的内存结构中。ZFS 的 *单拷贝 ARC* 功能允许将缓存块的单一副本共享给同一数据集的多个克隆。使用此功能,多个运行的容器可以共享单个缓存块的副本。此功能使 ZFS 成为 PaaS 和其他高密度用例的理想选择。

  • 碎片化: 碎片化是像 ZFS 这样的写时复制文件系统的一个自然副产品。ZFS 通过使用 128k 的小块大小来缓解这个问题。ZFS 意图日志 (ZIL) 和写操作合并(延迟写)也有助于减少碎片化。您可以使用 `zpool status` 命令监控碎片化。但是,除了重新格式化和恢复文件系统之外,没有其他方法可以对 ZFS 进行碎片整理。

  • 使用 Linux 的原生 ZFS 驱动: 不建议使用 ZFS FUSE 实现,因为它性能很差。

性能最佳实践

  • 使用快速存储: 固态硬盘 (SSD) 比旋转磁盘提供更快的读写速度。

  • 对写密集型工作负载使用卷: 卷为写密集型工作负载提供了最佳和最可预测的性能。这是因为它们绕过了存储驱动程序,不会产生由精简配置和写时复制引入的任何潜在开销。卷还具有其他优点,例如允许您在容器之间共享数据,并且即使没有运行的容器使用它们,它们也能持久化。