跳转至

ZFS

本文初稿编写中

ZFS(Zettabyte File System)虽然名叫“FS”,但是集成了一系列存储管理功能,包括文件系统、卷管理、快照、数据完整性检查和修复等,常被称作“单机最强存储方案”。

虽然 ZFS 没有特殊的系统要求,但是我们推荐在具有较好配置的服务器上使用 ZFS,以获得更好的性能和稳定性。

  • 固态硬盘,或者多块规格相同的大容量机械硬盘(推荐 4 块或更多),尽量避免用单块机械硬盘。
  • 如果预期需要承载较重的读写负载,推荐使用大容量内存用于缓存(ZFS 官方推荐每 1 TB 存储容量配置 1 GB 内存)。
    • 如果打算启用 ZFS 的去重(deduplication)功能,推荐为每 TB 存储容量配备至少 5 GB 内存(但是官方推荐的比例是 30 GB)。
  • 如果预期的热数据量超出了内存容量,推荐使用 SSD 作为 L2ARC 缓存,但用于缓存的 SSD 容量不宜超过内存的 10 倍。
  • 多核心 CPU,以便处理 ZFS 的数据完整性检查和透明压缩等任务。

如果只是为了将 ZFS 的高级功能用于个人存储,如 NAS 等,那么你大可忽略以上所有推荐,在 Intel J3455 和 4 GB 内存的小主机上就可以轻松运行 ZFS,例如 QNAP 的个人 NAS 设备就已经默认采用 ZFS 了。

内核模块

尽管 OpenZFS 也是一个开源项目,但是由于开源协议不兼容(OpenZFS 采用 CDDL,Linux 内核采用 GPL),因此 OpenZFS 无法直接集成到 Linux 内核中。

  • Debian 将 ZFS 的代码以 DKMS 模块的形式打包(包名 zfs-dkms),以便在更新内核之后自动编译和安装。

    Debian backports

    受限于 Debian 的稳定性政策,stable 的软件源中的 ZFS 版本可能较老。如果需要使用较新的 ZFS 版本,可以考虑使用 Debian 的 backports 仓库(例如在 Debian Bookworm 中,对应使用 backports 的命令是 apt install -t bookworm-backports zfs-dkms)。

  • Ubuntu 自 20.04 LTS 起提供预编译好的 zfs.ko(软件包 linux-modules-$(uname -r)),因此无需再安装 zfs-dkms。Ubuntu 18.04 LTS 及之前的版本仍然需要安装 zfs-dkms

不论是 Debian 还是 Ubuntu,都需要安装 zfsutils-linux 软件包,以便使用 ZFS 的命令行工具。

基础概念

与 LVM 和其他卷管理工具类似,ZFS 也将存储设备组织成多级结构:

  • vdev(Virtual Device)是 ZFS 对“硬盘组”的抽象。一个 vdev 可以是单块硬盘、mirror(类似 RAID 1 硬盘组)或 RAID-Z、RAID-Z2、RAID-Z3(类似 RAID 5、6、7 硬盘组)等。
  • pool 是 ZFS 的存储池,由一个或多个 vdev 组成,类似 LVM 的 VG(Volume Group)概念。
  • ZFS 的文件系统和 Zvol(ZFS Volume)都是在 pool 上创建的,类似 LVM 的 LV(Logical Volume)概念。

    与 LVM 略有区别的地方是,ZFS 的文件系统是直接创建在 pool 上的,而无需像 LVM 一样先创建 LV,再将其格式化为某种文件系统。ZFS 的文件系统就是 ZFS。

创建 pool

同样与 LVM 不同的是,ZFS 推荐使用整块硬盘或尽可能整块的硬盘来创建 pool,以保证稳定的性能。作为一项适配操作,如果将整块硬盘用于创建 zpool,ZFS 会自动对其进行分区,以便在硬盘故障时能够更好地识别和处理。

以下假设 disk[1-3].img 是三块大小相同的硬盘。

$ truncate -s 1G disk1.img disk2.img disk3.img
$ sudo losetup -f --show disk1.img
/dev/loop0
$ sudo losetup -f --show disk2.img
/dev/loop1
$ sudo losetup -f --show disk3.img
/dev/loop2
$ sudo zpool create tank /dev/loop0 /dev/loop1 /dev/loop2
$

关于 zpool

ashift 参数是 ZFS 对“磁盘扇区大小”的理解,且在创建 pool 后无法更改。 为了确保最佳的性能,ashift 参数应当与硬盘的真实扇区大小相匹配,既不宜过大也不宜过小。 默认情况下,在创建 zpool 的时候,ZFS 会自动检测硬盘的扇区大小(ioctl(BLKPBSZGET))并设置合适的 ashift 参数。 对于固态硬盘,建议查阅硬盘的规格,然后手动指定 zpool create -o ashift=...

在生产环境中,请不要将 ZFS 用于任何虚拟硬盘或软件/硬件 RAID 阵列上。 只有当 ZFS 直接管理每块硬盘时,才能获得最佳的性能和 ZFS 提供的数据完整性保证。 如果你的阵列卡不支持直通,可以考虑为每块盘建立一个单盘阵列。

在实际应用中,如果你使用 sdanvme0n1 等设备名来创建 pool,ZFS 会自动对其进行分区,然后使用 sda1nvme0n1p1 等分区名来创建 pool,并在每个盘的末尾创建一个 8 MB 的分区(sda9nvme0n1p9)。分区的目的可能出于某些历史原因,目前无从考证,且这个编号为 9 的分区是没有任何用途的。

在创建好 pool 之后,可以使用 zpool statuszpool list 查看 pool 的状态和使用情况。

$ zpool status
  pool: tank
 state: ONLINE
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          loop0     ONLINE       0     0     0
          loop1     ONLINE       0     0     0
          loop2     ONLINE       0     0     0

errors: No known data errors
$ zpool list
NAME    SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
tank   2.81G   112K  2.81G        -         -     0%     0%  1.00x    ONLINE  -

Tip

在 ZFS 中,绝大多数诸如查询状态等只读的命令都不需要 sudo

在创建好 zpool tank 后,ZFS 也自动创建了一个文件系统 tank 并挂载在了 /tank 目录,可以直接使用。

参数调节

对于新建的 ZFS pool,我们推荐调整一些参数以获得最佳的性能。具体参见下面的参数调节

参数调节

Zpool

Zpool 层面的参数可以通过 zpool set 命令进行调整,以下是一些推荐修改的参数:

  • autotrim=on:如果你使用硬盘是 SSD,启用此选项后 ZFS 会自动为已删除的数据块向硬盘发送 TRIM 指令。例如:

    zpool set autotrim=on tank
    

ZFS 文件系统

ZFS(文件系统)层面的参数可以通过 zfs set 命令进行调整,语法与 zpool set 类似。以下是一些推荐修改的参数:

  • xattr=sa:将文件的扩展属性(如 POSIX ACL 和 SELinux 标签等)存储在 dnode 中(类似其他文件系统的 inode),而不是独立的“文件”中。对于经常使用扩展属性的应用场景(如 Samba),使用 xattr=sa 可以减少磁盘 I/O,提高性能。

    如果你的使用场景不需要扩展属性(如镜像站),可以使用 xattr=off 关闭扩展属性功能,进一步减少磁盘 I/O。

    该选项的默认值为 xattr=on,即扩展属性存储在额外的数据块中。这是为了保持与 FreeBSD / Solaris 等系统中的 ZFS 实现的兼容性。除非你预计需要将 ZFS pool 搬到这些系统上使用,否则我们推荐使用 xattr=saxattr=off

  • compression=oncompression=zstd:启用 ZFS 的透明压缩功能。对于大多数数据,压缩后的数据量会显著减小,从而减少磁盘 I/O。

    一般建议启用透明压缩功能,除非你的 CPU 性能较差(例如 10 年前的服务器)或者预期的数据量不会因压缩而减小(例如归档存储已经压缩过的数据)。

    压缩算法

    截至 2024 年,ZFS 默认使用 LZ4 算法进行压缩,这是一种速度较快的单线程算法。如果你的 CPU 不是上古级别的,可以考虑使用 Zstd,这是一种更加现代化的压缩算法,支持多线程和更高的压缩比。

ZFS 内核模块参数

ZFS 的内核模块具有非常多的可调节参数,其中大部分参数可以通过读写 /sys/module/zfs/parameters 目录下的文件进行调节。ZFS 的内核模块参数从生效时间上可以分为三类:

  • 仅加载时生效:这类参数在加载模块时就已经确定,无法在运行时修改。如果需要使用非默认值的话,需要在加载模块的时候就指定。一般通过在 /etc/modprobe.d 中创建 .conf 文件来指定。
  • import 时生效:这类参数可以在运行时通过读写 sysfs 进行调节,但新的值只有在下次导入 pool 时才会生效。如果需要对使用中的 pool 修改这些参数,需要先 zpool exportzpool import
  • 立即生效:这类参数可以在运行时通过读写 sysfs 进行调节,且立即生效。

最常调节的 ZFS 模块参数其实只有一个,那就是 zfs_arc_max,即 ZFS 使用系统内存作为 ARC 的最大值,详情请见下面的章节。

关于 ARC

ZFS ARC 的全称是 Adaptive Replacement Cache,是 ZFS 用于缓存磁盘数据的一级缓存。ZFS 的缓存算法非常智能,会将可用的缓存容量分为 MFU(Most Frequently Used)和 MRU(Most Recently Used)两部分,并根据负载情况自动调整两部分的大小。

由于 ZFS 的多级 metadata 等复杂的设计,ZFS 需要使用一定的内存作为 ARC 才能保证磁盘的正常读写操作,而不像其他文件系统(如 ext4,XFS 等)仅需在内存中维护少量的 metadata 即可正常运转。ZFS ARC 允许使用的最大内存量可以通过 zfs_arc_max 参数调节。默认情况下,ZFS 会使用系统内存的一半作为 ARC,但是如果你的服务器是专用于存储和文件服务的,可以考虑将这个值调大一些。例如:

# Set ARC memory limit to 4 GiB
echo 4294967296 > /sys/module/zfs/parameters/zfs_arc_max

如果你的系统中有其他大量占用内存的程序,我们推荐你同时设置一个合适的 zfs_arc_min 参数(其默认值为零),以保证 ZFS 能够维持一定的性能。

在 Linux 下,受限于 kernel 的设计 (1),ARC 占用的内存会在 htop / free 等程序中显示为 used 而不是 cached,但是其行为和 cached 是一致的,即在系统内存压力升高时会自动释放,以供其他程序使用。

  1. 在 FreeBSD 中,ZFS ARC 占用的内存会正确地显示为 cached。

强制释放 ARC

与 cached 内存一样,在 echo 3 > /proc/sys/vm/drop_caches 时,ZFS ARC 会一同释放。注意这会短暂地增加磁盘的读取压力。

ARC 的统计信息(如内存使用量、MRU / MFU 配比、命中率等)可以通过 /proc/spl/kstat/zfs/arcstats 查看,并且 ZFS 也提供了 arc_summary 命令将该接口的数据以更易读的方式输出。由于 arc_summary 输出量较大,建议使用 less 等分页工具查看。例如:

arc_summary | less

调试

ZFS 提供了调试工具 zdb,可以用于查看 pool 和文件系统的内部结构。 在遇到无法解释的问题时,使用 zdb 可能可以帮助调试问题。

需要注意的是:

  • zdb 不关心 pool 或者文件系统是否挂载,它都会直接访问块设备。因此在正在使用的 pool 或者文件系统上使用 zdb 可能会得到不一致的结果;
  • zdb 的输出格式没有文档说明,因为其假设使用者了解 ZFS 的内部结构;
  • zdb 支持写入内容,但是在不了解 ZFS 内部结构的情况下,建议仅使用 zdb 读取 pool 和文件系统的结构内容。

以下提供了一个使用 zdb 调试出生产环境「未解之谜」的例子:

案例:使用 zdb 帮助找出文件系统使用空间异常的原因

一台使用 ZFS 的服务器将 /var/log 挂载在了 ZFS 文件系统中:

NAME                                 USED  AVAIL  REFER  MOUNTPOINT
pool1/log                           2.88G   181G  2.88G  /var/log

但是系统管理员发现 /var/log 的使用空间会异常增大,直到大部分空间都被占用:

NAME                                 USED  AVAIL     REFER  MOUNTPOINT
pool1/log                            173G  3.43G      173G  /var/log

但是实际的 log 大小只有不到 3G:

$ sudo du -sh .
2.9G  .

同时没有快照,通过 lsof 检查也没有进程占用在 /var/log 下已经被删除的文件。 重启后文件系统的使用空间又恢复到了正常的大小。没有人能够解释原因。

在时隔半年又一次因此重启后,系统管理员决定使用 zdb 来查看文件系统的内部结构:

$ sudo zdb -dddd pool1/log > zdb-log.txt

检查输出,发现一个特别大的文件:

Object  lvl   iblk   dblk  dsize  dnsize  lsize   %full  type
  6426    4   128K   128K   170G     512  1.02T  100.00  ZFS plain file
                                           168   bonus  System attributes
dnode flags: USED_BYTES USERUSED_ACCOUNTED USEROBJUSED_ACCOUNTED 
dnode maxblkid: 8554489
uid     0
gid     4
atime   Thu Aug 17 19:22:48 2023
mtime   Sun Feb 18 16:05:29 2024
ctime   Sun Feb 18 16:05:29 2024
crtime  Thu Aug 17 06:25:01 2023
gen 30893491
mode    100640
size    1121254014464
parent  7279
links   0
pflags  40800000004

"6426" 这个对象也出现在了 ZFS delete queue 中:

Object  lvl   iblk   dblk  dsize  dnsize  lsize   %full  type
     3    1   128K     6K      0     512     6K  100.00  ZFS delete queue
dnode flags: USED_BYTES USERUSED_ACCOUNTED USEROBJUSED_ACCOUNTED 
dnode maxblkid: 0
microzap: 6144 bytes, 1 entries

    191a = 6426 

看起来是这个文件不停增大,但是 ZFS 没有删除。检查 6426 的 parent 7279:

Object  lvl   iblk   dblk  dsize  dnsize  lsize   %full  type
  7279    1   128K  2.50K     8K     512  2.50K  100.00  ZFS directory
                                           168   bonus  System attributes
dnode flags: USED_BYTES USERUSED_ACCOUNTED USEROBJUSED_ACCOUNTED 
dnode maxblkid: 0
uid     0
gid     0
atime   Mon Jun 24 01:32:06 2019
mtime   Fri Mar  8 06:25:02 2024
ctime   Fri Mar  8 06:25:02 2024
crtime  Tue Feb 27 21:11:06 2018
gen 4369970
mode    40755
size    33
parent  4
links   2
pflags  40800000144
microzap: 2560 bytes, 31 entries

    pacct.6.gz = 3908 (type: Regular File)
    pacct.17.gz = 1994 (type: Regular File)
    pacct.16.gz = 275 (type: Regular File)
    pacct.7.gz = 3518 (type: Regular File)
    pacct.5.gz = 473 (type: Regular File)
    pacct.14.gz = 1554 (type: Regular File)
    pacct.15.gz = 651 (type: Regular File)
    pacct.4.gz = 109 (type: Regular File)
    pacct.29.gz = 468 (type: Regular File)
    pacct.11.gz = 1863 (type: Regular File)
    pacct.10.gz = 2129 (type: Regular File)
    pacct.1.gz = 1294 (type: Regular File)
    pacct.28.gz = 3648 (type: Regular File)
    pacct.3.gz = 1864 (type: Regular File)
    pacct.12.gz = 3516 (type: Regular File)
    pacct.13.gz = 2128 (type: Regular File)
    pacct.2.gz = 2955 (type: Regular File)
    pacct.22.gz = 649 (type: Regular File)
    pacct.23.gz = 3649 (type: Regular File)
    pacct.8.gz = 3400 (type: Regular File)
    pacct.19.gz = 535 (type: Regular File)
    pacct = 796 (type: Regular File)
    pacct.21.gz = 534 (type: Regular File)
    pacct.0 = 904 (type: Regular File)
    pacct.20.gz = 3725 (type: Regular File)
    pacct.18.gz = 3515 (type: Regular File)
    pacct.9.gz = 1293 (type: Regular File)
    pacct.24.gz = 3905 (type: Regular File)
    pacct.25.gz = 903 (type: Regular File)
    pacct.27.gz = 1552 (type: Regular File)
    pacct.26.gz = 1176 (type: Regular File)

发现该目录为 /var/log/account,调查后发现其中的文件在启用 process accounting 后会由内核写入。 因此解释了为什么 lsof 没有显示任何进程占用对应文件。在关闭 process accounting 后,delete queue 清空了。

该问题已经尝试向 ZFS 反馈:https://github.com/openzfs/zfs/issues/15998。 在收到 issue 回复之后检查了 Debian 的 acct 的 cron daily 脚本,发现其使用了 invoke-rc.d 重启服务。 但是 /usr/sbin/policy-rc.d 在多年前被设置为 exit 101,因此 invoke-rc.d 无法启动服务, 导致了 process accounting 服务一直未被重启,内核仍然在写入文件。