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 tankzpool 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 snapshot5.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 tankscrub 期间性能会下降,最好安排在低峰期。如果 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/yyyxattr=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 命令集体报错,先确认模块加载状态,再怀疑池本身。这个顺序能帮你省下大量不必要的恐慌。