坑边闲话:TrueNAS 提供了良好的 ZFS 使用体验,但是频繁使用 Web 界面并非是一个令人舒适的事情。我的原则是,与存储相关的问题尽量在命令行里解决。此外,将 Debian 系统配置为一个理想的存储服务器是很吸引人的,但这依旧需要用户对存储管理的命令行有深入的理解。

本文包括但不限于以下内容;

  • ZFS 打快照、删除快照
  • ZFS 存储吃创建
  • ZFS 文件系统创建与销毁
  • ZFS 文件系统的参数调整

1. 安装 ZFS 相关组件·

Debian 或者 Ubuntu 等 Linux 发行版不具备 TrueNAS 的网页图形化界面,因此操作 ZFS 时颇为繁琐,为此我们做简单介绍。

1.1 常规安装·

在 Debian 12 的 的 stable 上游中,zfs-linux 的版本比较老旧,很多新的特性无法支持。为此,我们需要使用 bookworm-backports 中的 zfs-linux.

首先确保在 apt 配置文件里开启了相关上游,即确保下面的行存在:

1
2
deb http://deb.debian.org/debian bookworm-backports main contrib
deb-src http://deb.debian.org/debian bookworm-backports main contrib

为什么要启用相关 backport 软件源

OpenZFS 的最新版代码只在 Debian 的 testing 分支发布,像 OpenZFS 这种内核扩展一般会被向后移植,使较为保守的发行版,比如 bookworm,也能够安装使用最新版代码。

随后,修改系统 APT 偏好:

1
2
3
4
5
sudo tee /etc/apt/preferences.d/90_zfs > /dev/null << EOF
Package: src:zfs-linux
Pin: release n=bookworm-backports
Pin-Priority: 990
EOF

其中,Pin-Priority 数字越大优先级越高。若 990 依然不够大,可自行调整相对优先级。

最后,安装 ZFS 相关模块即可:

1
2
3
sudo apt update
sudo apt install dpkg-dev linux-headers-generic linux-image-generic
sudo apt install zfs-dkms zfsutils-linux nfs4-acl-tools

图 1. ZFS 与 Linux 内核的开源许可是冲突的,安装时会有提示。

OpenZFS 和 Linux 的许可证不兼容。

OpenZFS 根据共同开发和分发许可证(CDDL)授权,而 Linux 内核则根据通用公共许可证第二版(GPL-2)授权。虽然这两种许可证都是自由开源许可证,但它们是限制性的许可证。这种组合导致了问题,因为它阻止了在同一个二进制文件中使用专门在另一许可证下可用的代码片段。

你将以这样的方式构建 OpenZFS,即它们不会被构建成一个单一的大型二进制文件。请注意,将这两种二进制文件分发在同一媒介(如磁盘映像、虚拟设备等)可能会导致侵权

随后软件会自行注册两个 Systemd 服务:

1
2
Created symlink /etc/systemd/system/zed.service → /lib/systemd/system/zfs-zed.service.
Created symlink /etc/systemd/system/zfs.target.wants/zfs-zed.service → /lib/systemd/system/zfs-zed.service.

1.2 问题排查·

如果遇到 zfs-load-module.service 服务失败时,大概率是 Linux Kernel 升级了但是系统并没有下载对应的 linux-headers 文件,可通过下列命令安装头文件并重新安装 ZFS DKMS 模块。

1
sudo apt install linux-headers-$(uname -r)

2. 存储池相关·

存储池是最关键的部分,因为它是数据集的底层。存储池与 RAID 类型密切相关,一般存储池将会涉及以下内容:

  • 存储冗余度,
  • 虚设备,如 Slog、L2ARC 等。

如果不能保证底层的可靠,用户侧做再多设置都是错的。接下来将介绍与存储池相关的高频操作。

2.1 创建存储池·

对于磁盘列表,可以使用 nvme-cli 等工具查看硬盘列表。

图 2. 使用 nvme-list 命令查看 NVMe 硬盘列表,Node 列就是磁盘的句柄(盘符),可用来执行 ZFS pool 的创建。

对于 SATA 硬盘,可能需要使用 lsblk 等命令进行查看。

获得了磁盘列表,并成功地将物理磁盘与磁盘句柄(盘符)进行配对之后,即可进行存储池创建:

1
2
$POOL_NAME=tank
sudo zpool create ${POOL_NAME} raidz sda sdb sdc -f

最佳实践

可以看到,创建存储池是一个与硬件相关的命令。为了保证后期的辨识便利性,笔者建议按照如下关键字级联给存储池命名:

  • 磁盘制造商,如
    • DapuStor
    • Intel
    • Kioxia
    • Memblaze
    • Micron
    • Samsung
    • Solidigm
    • WD
  • 磁盘型号,如
    • R5100
    • P5520
    • PM1733
    • SN750
  • RAIDZ 级别,如
    • Mirror
    • Stripe
    • RAID-Z1
    • RAID-Z2
    • RAID-Z3

DapuStor_R5100_RAID-Z1 等。如此一来,一眼便知存储器类型及冗余级别。

2.2 对存储池进行导入和导出·

在创建存储池之后,如果需要将硬盘拔除,则需要先把存储池下线,即 export 导出。

1
2
$POOL_NAME=tank
zpool export ${POOL_NAME}

如果需要恢复存储池,则使用相反的命令 import 导入。

1
2
$POOL_NAME=tank
sudo zpool import ${POOL_NAME}

如果需要对存储池进行重命名,则需要先导出再导入且在导入的时候使用新的名字,同时应该注意,并没有 zpool-rename 这种命令。

1
2
3
4
5
# Rename a zfs pool
$OLD_NAME=old_name
$NEW_NAME=new_name
zpool export ${OLD_NAME}
zpool import ${OLD_NAME} ${NEW_NAME}

此外,不加参数地使用 zfs import 可以列出所有可以被导入的 ZFS 存储池。

2.3 设置存储池相关属性·

2.3.1 autotrim·

TRIM 是一种用于固态硬盘的操作,用来通知 SSD 固件哪些数据块已经不再被文件系统使用,可以被标记为空闲。这有助于 SSD 执行垃圾回收从而提高性能和寿命。

  • 文件系统的角色:
    • 文件系统知道哪些数据块被删除或释放(例如文件删除或文件系统调整时)。
    • TRIM 命令由文件系统发出,通知存储设备这些块可以清除。
  • SSD 固件的角色:
    • SSD 固件接收 TRIM 命令后,将对应的块标记为空闲。
    • 固件会根据内部策略在适当时间对这些块执行垃圾回收或优化操作。

延迟 TRIM 操作可能提高效率,但会带来短期性能下降的风险,因为 SSD 的写入性能与剩余可用块的数量密切相关。

通过 zpool trim 命令,可以手动进行 TRIM. 反之,通过设置存储池的自动 trim 参数,也可激活自动 trim 行为。

1
sudo zpool set autotrimi=on ${POOL_NAME}

该参数的引入可能会导致存储池频繁进行垃圾回收,进而降低性能。

2.3.2 bclone·

通过执行下列命令,可开启 ZFS 内核模块的相关特性。

1
2
3
bash -c '
sudo echo 1 > /sys/module/zfs/parameters/zfs_bclone_enabled
'

开启 bclone 特性之后,在同一个存储池内部拷贝数据,将直接触发 ZFS 内部函数,通过创建新的 dentryinode 并指向原有的数据块,可快速拷贝新文件同时不占用双倍空间。不过需要注意,在进行 ZFS Replication 时,zfs send 会把所有节约的空间重新展开,如果目标池没有足够的空间,将无法完成 replication 工作。

参考

在 Linux 系统中,/sys/module/ 目录是 sysfs 文件系统的一部分,用于显示已加载的内核模块的相关信息。这个目录提供了一个结构化的接口,允许用户和工具查看和操作内核模块的状态和参数。

3. 数据集相关·

在完成了底层存储池的构建之后,我们所关心的主要是用户侧的内容,如存储池和文件共享等。

存储池已经

  • 通过 data vdev 提供了基本的存储能力,
  • 通过 Slog 和 L2ARC 提供了读写强化(可选),
  • 通过 Hot spare 盘提供了一定程度的故障自动转移(可选)。

数据集就是建立在存储池之上的抽象的文件系统,也是用户真正可读写的最基本单元。存储池使用 zpool 系列命令进行配置,数据集通过 zfs 命令进行配置。从命令名也可以看出,数据集就是真正的 ZFS 文件系统。

3.1 创建数据集·

3.1.1 创建 ZFS 文件系统·

文件系统,指的是可以在里面创建目录、文件的 ZFS 对象。执行下列命令,可以直接创建一个最基本的文件系统。

1
2
3
$POOL_NAME=tank
$DATASET_NAME=data
sudo zfs create ${POOL_NAME}/${DATASET_NAME}

3.1.2 创建 ZVol 块设备·

ZVol 是一种基于 ZFS 的块设备,可以类比为一块虚拟磁盘,可以用来格式化、创建其他类型的文件系统。一般来说,在 Linux 系统上都有 ZFS 模块,因此没有使用块设备的必要性,但是如果要通过 iSCSI 给 Windows 系统共享块存储,ZVol 就是一个很明智的选择。

1
2
3
4
5
6
7
$POOL_NAME=tank
$ZVOL_NAME=Intel_750_RAID-Z1/openwrt-compile
sudo zfs create -V \
128G \
-o compression=lz4 \
-o volblocksize=16K \
${POOL_NAME}/${ZVOL_NAME}

由此,可以创建一个 ZVol,它具有以下性质:

  • 容量:128 GiB
  • 底层块压缩方式:LZ4 算法
  • volume 块大小:16 KiB
  • 名字:Intel_750_RAID-Z1/openwrt-compile

3.2 设置数据集的相关属性·

熟悉 C# 编程的朋友应该清楚,通过设置一对 getset 函数,可以用来获取、修改 class 的属性。ZFS 的数据集属性访问与 C# 的语法非常相似。

  • 获取属性使用 zfs get
  • 设置属性使用 zfs set

3.2.1 设置压缩方式·

1
sudo zfs set compression=lz4 ${POOL_NAME}/${DATASET_NAME}

3.2.2 设置权限类型·

1
sudo zfs set acltype=nfsv4 ${POOL_NAME}/${DATASET_NAME}

3.2.3 设置挂载入口·

这是唯一一个特殊的命令,它使用 zfs 控制存储池。在存储池创建之后,原则上需要一个文件系统入口才可被用户访问,mountpoint 即该入口。

1
sudo zfs set mountpoint=${/PATH/OF/MOUNT_POINT}] ${POOL_NAME}

设置了存储池的入口之后,存储池下的数据集也将以对应目录结构被自动挂载。

3.2.4 重命名数据集·

使用 zfs rename 命令可对数据集进行重命名,如

1
2
3
$OLD_DATASET_NAME
$NEW_DATASET_NAME
sudo zfs rename $POOL_NAME/$OLD_DATASET_NAME $POOL_NAME/$NEW_DATASET_NAME

4. 快照相关·

4.1 创建快照·

打快照建议使用标准化命名方式,随意取名字可能会导致后期管理紊乱。

1
sudo zfs snapshot ${POOL_NAME}/${DATASET_NAME}@$(date +"manual-%Y-%m-%d_%H-%M")

4.2 移除快照·

过于老旧的快照会占用很多不必要的空间,及时清理不再需要的快照是个好习惯。

1
sudo zfs destroy ${POOL_NAME}/${DATASET_NAME}@${SNAPSHOT_NAME}

注意

zfs destroy 命令,不仅仅可以用来删除快照,还可以用来移除 dataset,这是一个很危险的操作,一定要谨慎无比地使用!

4.3 创建 systemd 快照任务·

用户可根据自身需求,定期创建快照。快照遵循严格的命名规范,后期可根据快照名进行自动化备份。

4.3.1 创建快照生成文件·

1
sudo vim /usr/local/bin/zfs_snapshot.sh

填入以下内容:

1
2
3
#!/bin/bash

/sbin/zfs snapshot -r zroot@$(/usr/bin/date +auto-%Y-%m-%d_%H-%M)

随后赋予该文件可执行权限:

1
sudo chmod +x /usr/local/bin/zfs_snapshot.sh

4.3.2 创建 systemd 任务·

编辑 systemd 服务文件:

1
sudo vim /etc/systemd/system/zfs_snapshot.service

填写以下内容:

1
2
3
4
5
6
7
[Unit]
Description=Take ZFS snapshot

[Service]
Type=oneshot
WorkingDirectory=/
ExecStart=/usr/local/bin/zfs_snapshot.sh

随后,创建定时器文件进行时间调度。编辑定时器文件:

1
sudo vim /etc/systemd/system/zfs_snapshot.timer

填写以下内容:

1
2
3
4
5
6
7
8
9
[Unit]
Description=Run ZFS snapshot of zroot daily at every hour

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

最后,开启 systemd 相应定时器:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable zfs_snapshot.timer
sudo systemctl start zfs_snapshot.timer

此后,系统将会周期性地对目标数据集执行快照。

建议

如果数据集比较复杂,可使用 Python 文件重构 /usr/local/bin/zfs_snapshot.sh,代码控制力会更强一些。

5. 数据集迁移与备份·

5.1 数据集迁移·

ZFS 数据集迁移比较优雅,极具 Unix I/O 流的特点。

  • ZFS 数据集可以被序列化为 io_stream,因此可以把数据集保存为一个本地的文件;
  • zfs send 生成流,zfs receive 接受流,本地某文件也可以接受流;
  • ZFS 快照之间的增量部分也可以被重定向为流。

由此可见,ZFS 数据集迁移非常灵活。一个常见的模式如下:

1
sudo zfs send ${POOL_NAME}/${DATASET_NAME}@${SNAPSHOT_NAME} | sudo zfs receive ${TARGET_POOL}/${TARGET_DATASET}

如果接受数据流的是另一台机器,则可通过网络使用 SSH 传输:

1
sudo zfs send ${POOL_NAME}/${DATASET_NAME}@${SNAPSHOT_NAME} | ssh -p ${PORT} ${USERNAME}@${HOSTNAME} sudo zfs receive ${TARGET_POOL}/${TARGET_DATASET}

在 send 和 receive 之间添加 pv -tea 步骤,可以看到当前管道的传输速率。命令格式如下:

1
zfs send ... | pv -tea | zfs receive ...

此外,若要指定接收端新建数据集的特性,可使用 -o 参数,如:

1
sudo zfs send ${POOL_NAME}/${DATASET_NAME}@${SNAPSHOT_NAME} | pv -tea | sudo zfs receive -o compression=lz4 -o recordsize=128k Toshiba_MG06S_RAID-Z1/Downloads

其中,

  • compression=lz4 将会设置接收端的压缩算法;
  • recordsize=128k 将会设置接收端的 ZFS 记录值。

5.2 查看快照与数据集之间的差异·

1
sudo zfs diff -F ${POOL_NAME}/${DATASET_NAME}@${OLD_SNAPSHOT} ${POOL_NAME}/${DATASET_NAME}@${NEW_SNAPSHOT} | while read -r line; do echo -e "$line"; done

最佳实践

由于 ZFS 的 zfs diff 命令输出中文字符、日文字符时,会使用八进制编码(Ocatl Encoding),所以打印到屏幕上是 \dddd 的样子。通过上述命令可以逐行转移为可识别的中日文字。

SuperUser 参考

5.2 TrueNAS 数据迁移要诀·

使用 SSH+netcat 进行 replication 时需要注意两边至少有一方安装了 py-libzfs.

1
sudo apt install python3-libzfs

总结·

ZFS 因为优秀的特性使得它在一众文件系统中独领风骚很多年,至今它都是最优秀的文件系统之一,可以说在开源方案中,ZFS 是最优秀的之一。然而,因为 Linux Kernel 和 OpenZFS 的开源许可冲突,导致两者至今不能合并,这或许也是一个开源届的损失。但是我认为,这依旧是社会契约的一种合理表现。或许在未来的某一天我们能够看到 OpenZFS 正式融入 Linux Kernel.

除此之外,本文不涉及 boot from ZFS,毕竟这还不是一个非常完善的操作。使用 ZFS 进行系统引导,可参考 ZFSBootMenu 工程项目。