ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ubuntu 上 ZFS 实战:从安装、调优到快照备份的完整指南

Ubuntu 上 ZFS 实战:从安装、调优到快照备份的完整指南

1. 为什么要在 Ubuntu 上折腾 ZFS

很多人第一次听到 ZFS,反应都是"这不是企业级存储才用的东西吗"。实际上,ZFS 早就不是大型存储阵列的专属了。它是一个集卷管理、软 RAID、快照、校验、压缩、去重于一身的文件系统,最吸引人的地方在于:它把过去需要 LVM + mdadm + 文件系统 + 备份工具四五个组件拼起来才能干的事,全部收进了一个统一的框架里。

我在一台 Ubuntu 工作站上跑 ZFS 已经有一段时间了,最初的目的很简单——受够了传统 ext4 分区一旦划好就不好调整的毛病,也受够了 rsync 做增量备份时那种"心里没底"的感觉。ZFS 的快照是原子级的,几乎瞬间完成,而且不占额外空间(只记录变化块),回滚一条命令搞定。这种体验一旦用过,就很难回去了。

这篇文章面向的是有一定 Linux 基础、想在 Ubuntu 上把 ZFS 真正用起来的人。我会从安装、创建池、调优参数、快照管理、日常维护几个角度,把整个流程讲透,并且把那些官方文档里不会写、但实际用起来一定会遇到的坑一并说清楚。你不需要有存储工程师的背景,但至少要熟悉基本的命令行操作和磁盘设备命名规则。

需要提前说明的是,ZFS 有两种常见形态:一种是 Ubuntu 官方仓库里的zfs-linux(内核模块方式),另一种是用户态的 OpenZFS 源码编译。对绝大多数人来说,直接用 Ubuntu 仓库的版本就够了,省心且和内核更新配合得好。下面所有操作都基于这个前提。

2. 安装前的磁盘规划与设备识别

2.1 先搞清楚你要用哪些盘

ZFS 最忌讳的一件事就是"盘还没认全就急着建池"。我见过有人把系统盘和数据盘搞混,结果一条zpool create下去,系统盘被覆盖,当场重装。所以第一步永远是确认设备名。

用lsblk看块设备拓扑是最直观的:

lsblk -o NAME,SIZE,TYPE,MODEL,MOUNTPOINT

输出里你会看到sda、sdb、nvme0n1这类名字。关键点在于:ZFS 建池时用的是整块盘(whole disk),而不是分区。也就是说,你给的是/dev/sdb而不是/dev/sdb1。ZFS 会自己在盘上写标签和分区结构。

如果你用的是 NVMe 盘,名字会是/dev/nvme0n1、/dev/nvme1n1。这里有个容易踩的坑:NVMe 设备名里的数字含义和 SATA 不一样,nvme0n1表示控制器 0 上的命名空间 1,别把它当成分区号。

提示:建池前务必用lsblk和blkid双重确认目标盘上没有重要数据。ZFS 建池会直接覆盖盘上的分区表,没有"你确定吗"这种二次确认。

2.2 用 by-id 路径避免设备名漂移

/dev/sdb这种名字是内核枚举时动态分配的,重启或者插拔硬盘后可能变成/dev/sdc。如果你在zpool create时写的是/dev/sdb,ZFS 内部其实会记录设备的唯一标识(比如 WWN 或序列号),所以池本身不会因为改名而挂掉。但为了排查问题时心里有数,我习惯在建池时就用/dev/disk/by-id/下的稳定路径:

ls -l /dev/disk/by-id/

你会看到类似ata-WDC_WD40EFRX-68N32N0_WD-WCC7K1234567这样的长名字。建池时用它,好处是无论设备名怎么变,你都能一眼对应到物理盘。这个习惯在多盘环境下尤其重要。

2.3 内存和 ECC 的现实考量

网上关于 ZFS 的讨论里,永远绕不开两个话题:内存要多大、必须用 ECC 内存吗。

先说内存。ZFS 默认会用一部分内存做 ARC(Adaptive Replacement Cache),这是它的读缓存。经验值是每 1TB 存储配 1GB 内存起步,但这只是"舒适线",不是"生死线"。我实测过 8GB 内存带 4TB 池,日常家用完全够用,只是 ARC 命中率低一些。真正吃内存的是**去重(dedup)**功能,那个才是内存杀手,每 TB 去重数据可能要 5GB 以上内存,家用场景我强烈建议别开。

再说 ECC。ZFS 的设计哲学是"端到端校验",它能发现并修复数据损坏,但如果内存本身出错,ZFS 可能会把正确的数据"修"成错的。所以理论上 ECC 更安全。但现实是,非 ECC 内存跑 ZFS 的人海了去了,出问题的概率远低于硬盘故障本身。我的态度是:有 ECC 更好,没有也不必因此放弃 ZFS。别被那些"没有 ECC 就别用 ZFS"的论调吓住。

3. 在 Ubuntu 上装 ZFS 的完整流程

3.1 安装软件包

Ubuntu 从 16.04 开始就把 ZFS 做进了官方仓库,安装非常直接:

sudo apt update sudo apt install zfsutils-linux

这个包会带上zpool、zfs、zdb等核心工具,以及内核模块zfs.ko。装完之后验证一下:

zfs version

正常会输出类似zfs-2.2.x和zfs-kmod-2.2.x两行。如果只显示用户态版本、内核模块版本为空,说明模块没加载,手动来一下:

sudo modprobe zfs

如果modprobe报错,多半是内核版本和模块不匹配,通常是刚升级完内核还没重启。重启一次基本就好。

3.2 关于 DKMS 和内核更新

这里有个很多人忽略的细节:ZFS 是内核模块,每次内核升级后都需要重新编译或加载对应版本的模块。Ubuntu 的zfsutils-linux依赖zfs-dkms(Dynamic Kernel Module Support),DKMS 会在内核更新时自动帮你重新编译模块。

你可以确认一下 DKMS 状态:

dkms status

应该能看到zfs相关的条目,状态是installed。如果某次内核升级后 ZFS 突然不能用了,第一件事就是看 DKMS 有没有编译失败。常见原因是缺少内核头文件:

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

装完头文件再sudo dkms autoinstall重新编译即可。这个坑我在一次自动升级后遇到过,池没挂,但zpool命令报"模块未加载",折腾了半小时才反应过来是 DKMS 没跟上。

3.3 创建第一个存储池

假设你有两块盘/dev/sdb和/dev/sdc,想做一个镜像池(类似 RAID1),命令是:

sudo zpool create -f tank mirror /dev/sdb /dev/sdc

这里tank是池名,你可以随便起,但建议用有意义的名字。mirror表示镜像模式,两块盘互为副本。执行完,ZFS 会自动把池挂载到/tank。

如果你想要更细的控制,比如指定挂载点、关闭自动挂载,可以加参数:

sudo zpool create -f \ -O mountpoint=/data \ -O compression=lz4 \ -O atime=off \ tank mirror /dev/sdb /dev/sdc

逐条解释一下这几个参数为什么这么设:

  • -O mountpoint=/data:把池的挂载点定在/data,而不是默认的/tank,方便你按用途组织目录。
  • -O compression=lz4:开启 LZ4 压缩。这是 ZFS 上最值得开的一个选项,压缩几乎不损失性能,反而因为写入数据变少而提升吞吐。LZ4 的压缩比在文本、日志类数据上通常有 2 到 3 倍。
  • -O atime=off:关闭访问时间更新。每次读文件都写一次元数据是纯浪费,关掉能明显减少写入放大。

3.4 池的健康检查

建完池,第一件事是看状态:

zpool status tank

输出里会显示池的整体健康度(ONLINE)、每块盘的状态、以及是否有校验错误。刚建好的池应该是全绿。如果看到DEGRADED或者某块盘FAULTED,说明盘有问题,别急着往里存数据。

再看一眼容量和属性:

zpool list tank zfs get all tank

zpool list显示的是池的物理容量,注意 ZFS 报告的可用空间和传统 RAID 算法不太一样,镜像池里两块 4TB 盘,可用空间是 4TB 而不是 8TB,这是正常的。

4. 数据集、压缩与那些影响性能的关键参数

4.1 数据集不是文件夹,是独立的管理单元

ZFS 里有个核心概念叫数据集(dataset),它看起来像目录,但本质是一个可以独立设置属性的文件系统。你可以这样创建:

sudo zfs create tank/media sudo zfs create tank/backup sudo zfs create tank/vm

每个数据集都能单独设压缩、配额、快照策略。比如给虚拟机镜像单独设recordsize,给备份数据单独设配额,互不干扰。这是 ZFS 比传统文件系统灵活的地方——属性跟着数据集走,而不是跟着整个分区走。

4.2 recordsize 的选择逻辑

recordsize是 ZFS 里最容易被忽视、但对性能影响巨大的参数。它定义了一个数据块的最大尺寸,默认是 128K。

  • 存大文件(视频、镜像、备份归档):保持 128K 甚至调到 1M,减少元数据开销。
  • 存数据库或小文件:调到 8K 或 16K,减少读放大。
  • 存虚拟机磁盘:通常建议 16K 到 64K,具体看虚拟化平台。

改法是:

sudo zfs set recordsize=16k tank/vm

注意recordsize只对新写入的数据生效,已经写进去的块不会重排。所以最好在数据集还是空的时候就设好。

4.3 压缩算法的取舍

ZFS 支持好几种压缩算法,常用的有 LZ4、ZSTD、GZIP。我的建议很明确:

算法压缩比CPU 开销适用场景
LZ4中极低默认首选,几乎无脑开
ZSTD高中归档、日志、可接受一点 CPU
GZIP最高高极少用,除非存储极度紧张

LZ4 之所以是默认推荐,是因为它的解压速度比内存带宽还快,等于"免费"压缩。ZSTD 在较新版本的 OpenZFS 里支持,压缩比更好,适合对空间敏感的场景。GZIP 现在基本被 ZSTD 取代了,除非你有特殊需求。

查看压缩效果:

zfs get compressratio tank

这个值会随着数据写入慢慢稳定下来。我自己的媒体库用 LZ4,压缩比大概 1.15;日志数据集用 ZSTD,能到 3 以上。

4.4 别轻易碰去重

dedup这个功能听起来很美——相同的数据只存一份。但它的代价是一张巨大的去重表常驻内存,每 TB 数据可能要 5GB 以上内存。家用环境开去重,轻则内存吃紧,重则系统卡死。我见过有人为了省几百 GB 空间开了去重,结果 16GB 内存被吃光,机器直接失去响应。

如果你确实需要去重,先算清楚内存够不够,或者考虑用zstd压缩来替代——压缩在很多场景下能达到类似的空间节省,却没有去重表的内存负担。

5. 快照、回滚与日常数据保护

5.1 快照为什么是 ZFS 的杀手锏

传统备份要么全量复制(慢、占空间),要么增量同步(依赖工具、容易出错)。ZFS 快照是**写时复制(Copy-on-Write)**的产物:创建快照的瞬间只是记录当前的数据块指针,几乎不耗时、不占空间。之后只有数据被修改时,旧块才保留下来。

创建快照:

sudo zfs snapshot tank/media@20240101

命名建议带上日期或用途,方便管理。查看快照:

zfs list -t snapshot

5.2 回滚与克隆

回滚到某个快照:

sudo zfs rollback tank/media@20240101

注意,回滚会丢弃快照之后的所有修改,而且如果有更新的快照,需要先删掉它们或者加-r递归删除。这个操作不可逆,执行前想清楚。

如果不想破坏当前数据,可以从快照克隆出一个新数据集:

sudo zfs clone tank/media@20240101 tank/media-restore

克隆出来的数据集是独立的,可以随便改,不影响原数据。这个功能在"误删文件想找回"的场景下特别好用——克隆一份,把文件拷出来,再删掉克隆。

5.3 用 cron 做自动快照

手动打快照容易忘,用 cron 自动化最省心。写个简单脚本:

#!/bin/bash DATE=$(date +%Y%m%d-%H%M) zfs snapshot tank/media@auto-$DATE # 只保留最近 30 个自动快照 zfs list -t snapshot -o name -s creation | grep "tank/media@auto-" | head -n -30 | xargs -r -n1 zfs destroy

放进 crontab 每小时跑一次:

0 * * * * /usr/local/bin/zfs-snapshot.sh

这个脚本的逻辑是:先打快照,再列出所有自动快照按创建时间排序,删掉最旧的、只留 30 个。这样既保证了有回滚点,又不会让快照无限堆积。

提示:快照本身不占空间,但被快照引用的数据块无法释放。如果你删了文件但快照还在,空间不会回收。定期清理旧快照是必须的。

5.4 发送到远程做异地备份

ZFS 的send/receive是它最强大的功能之一,能把快照流式传输到另一台机器:

sudo zfs send tank/media@20240101 | ssh user@remote zfs receive backup/media

增量发送更省带宽:

sudo zfs send -i tank/media@20240101 tank/media@20240102 | ssh user@remote zfs receive backup/media

这个机制的好处是传输的是块级差异,不依赖文件系统层,比 rsync 更可靠,而且接收端的数据是完整可用的 ZFS 数据集,随时能挂载。

6. 那些官方文档不会告诉你的坑

6.1 池满了会怎样

ZFS 在池使用率超过 80% 之后性能会明显下降,超过 95% 可能直接拒绝写入。这不是 bug,是设计使然——ZFS 需要空闲块来做写时复制和碎片整理。所以永远给池留至少 20% 的余量。

监控使用率:

zpool list -o name,capacity tank

如果快满了,优先删旧快照,其次考虑加盘扩容。

6.2 加盘扩容的正确姿势

镜像池扩容有个限制:不能直接往镜像里加一块盘变成三副本(其实可以,但意义不大)。常见做法是加一组新的镜像 vdev:

sudo zpool add tank mirror /dev/sdd /dev/sde

这样池的容量翻倍,但注意——ZFS 不会自动重新平衡已有数据。新写入的数据会优先落到新 vdev,旧数据还留在老 vdev 上。想要平衡,得手动迁移数据或者用zfs send/receive重建。

6.3 scrub 要定期跑

scrub是 ZFS 的校验扫描,它会读取所有数据块,用校验和验证完整性,发现坏块就用副本修复。建议每月跑一次:

sudo zpool scrub tank

查看进度:

zpool status tank

scrub 期间性能会下降,最好安排在低峰期。如果 scrub 报出校验错误,别慌,先看zpool status -v定位是哪个文件,再决定是修复还是从备份恢复。

6.4 别在 ZFS 上叠 RAID 控制器

如果你用的是硬件 RAID 卡,一定要把它设成 HBA/JBOD 模式,让 ZFS 直接看到物理盘。硬件 RAID 会隐藏磁盘的真实状态,ZFS 拿到的是一块"逻辑盘",校验和修复能力就废了一半。这是 ZFS 部署里最常见的错误之一。

6.5 系统盘能不能用 ZFS

可以,Ubuntu 的安装器甚至支持 ZFS 根文件系统(ZFS on Root)。但要注意,ZFS 根需要单独的/boot分区(通常是 ext4),因为 GRUB 对 ZFS 的支持有限。如果你只是想体验 ZFS,建议先拿数据盘练手,别一上来就动系统盘。

7. 我自己的调优参数清单

折腾久了,我总结出一套自己常用的参数组合,直接贴出来供参考:

# 建池时 sudo zpool create -f \ -O mountpoint=/data \ -O compression=lz4 \ -O atime=off \ -O xattr=sa \ -O acltype=posixacl \ tank mirror /dev/disk/by-id/xxx /dev/disk/by-id/yyy
  • xattr=sa:把扩展属性存在 inode 里而不是隐藏目录,性能更好,Samba/NFS 场景尤其明显。
  • acltype=posixacl:启用 POSIX ACL,兼容 Linux 权限模型。

数据集层面:

sudo zfs set recordsize=1M tank/media # 大文件 sudo zfs set recordsize=16k tank/vm # 虚拟机 sudo zfs set compression=zstd tank/backup # 归档 sudo zfs set quota=2T tank/backup # 配额限制

这些参数不是教条,你可以根据自己的负载调整。核心原则是:大文件大块、小文件小块、能压缩就压缩、别开去重。

8. 出问题时的排查思路

ZFS 出问题,第一步永远是看zpool status。它会告诉你池的健康度、哪块盘有问题、有没有校验错误。如果池是ONLINE但性能异常,看zpool iostat:

zpool iostat -v tank 5

这个命令每 5 秒刷新一次,显示每个 vdev 的读写吞吐和 IOPS。如果某块盘明显拖后腿,可能是盘要坏了。

如果池变成DEGRADED,说明有盘掉线。先确认是物理故障还是连接问题:

sudo zpool status -v tank

如果是临时掉线,zpool online tank /dev/sdb能重新上线并触发 resilver(重建)。resilver 期间性能会下降,别在这时候做重负载操作。

如果池彻底无法导入,试试:

sudo zpool import sudo zpool import -f tank

-f是强制导入,用于池被标记为"上次未正常导出"的情况。如果还不行,可能需要-F或-X做更激进的事务回滚,但这两个参数有数据丢失风险,用之前一定确认清楚。

最后分享一个我踩过的坑:有次系统升级后 ZFS 模块没加载,zpool命令全部报错,我第一反应是池坏了,差点去动zpool import。后来冷静下来一看,是 DKMS 编译失败导致模块缺失。遇到 ZFS 命令集体报错,先确认模块加载状态,再怀疑池本身。这个顺序能帮你省下大量不必要的恐慌。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进