ZFS 存储驱动程序

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

它由 Sun Microsystems(现为 Oracle Corporation)创建,并在 CDDL 许可证下开源。由于 CDDL 与 GPL 之间的许可证不兼容性,ZFS 无法作为主线 Linux 内核的一部分发布。然而,ZFS On Linux (ZoL) 项目提供了一个非主线内核模块和用户空间工具,可以单独安装。

Linux 上的 ZFS (ZoL) 移植版本运行良好且日益成熟。然而,目前不建议在生产环境中使用 zfs Docker 存储驱动程序,除非您对 Linux 上的 ZFS 有丰富的经验。

注意

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 按需分配。
  • 快照:只读的、节省空间的文件系统时间点副本。
  • clones:快照的读写副本。用于存储与上一层的差异。

创建克隆的过程:

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

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

磁盘上的镜像和容器层

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

镜像分层和共享

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

下面的图表展示了这是如何基于一个双层镜像的运行容器组合在一起的。

ZFS pool for Docker container

当您启动一个容器时,会按顺序发生以下步骤:

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

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

    在图中,“第1层”是通过获取基础层的 ZFS 快照,然后从该快照创建一个克隆来添加的。克隆是可写的,并按需从 zpool 消耗空间。快照是只读的,将基础层保持为不可变对象。

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

    在图中,容器的读写层是通过创建镜像顶层(Layer 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这样的写时复制(copy-on-write)文件系统的自然产物。ZFS通过使用128k的小块大小来缓解这一问题。ZFS意图日志(ZIL)和写入合并(延迟写入)也有助于减少碎片化。您可以使用zpool status来监控碎片化。但是,如果不重新格式化和恢复文件系统,就无法对ZFS进行碎片整理。

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

性能最佳实践

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

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