为什么个人站长值得认真考虑 ZFS
很多人一听到 ZFS 就摇头,觉得它是「企业级的重型武器」,个人 VPS 那点内存根本带不动。这个印象在三五年前或许有道理,但今天的 ZFS on Linux(OpenZFS)已经非常成熟:Debian、Ubuntu 都能直接装包,1GB 内存的小机器只要把参数调对,也能稳稳跑起来。数据一旦出问题,你才会明白「写时复制」和「端到端校验」这两个词有多值钱。
ZFS 和 ext4、XFS 这类传统文件系统最大的区别在于:它不是「磁盘上的一个格式化层」,而是一套把卷管理、文件系统、校验、快照、压缩、配额全部打包在一起的数据管理平台。你不再需要先做 LVM 再 mkfs,也不需要额外配 mdadm 做软 RAID——ZFS 把这些活儿全揽了。
本文面向个人站长的真实场景:一台单机 VPS 或家宽小主机,跑着网站、数据库和一堆备份。我会从装包、建池、调 ARC、做快照、send/receive 异地备份一路讲到日常维护,并指出几个新手最容易踩的坑。
第一步:安装 OpenZFS 并确认内核模块
Debian 12 / Ubuntu 22.04 以上都自带 zfs 内核模块包。装之前先更新源,然后一次性装上用户态工具和 DKMS 模块:
apt update
apt install -y linux-headers-$(uname -r) zfsutils-linux
modprobe zfs
zfs version
如果 zfs version 报「module not found」,多半是当前内核版本和已安装的 headers 对不上,或者机器是 OpenVZ/LXC 这类共享内核的容器——ZFS 需要能加载内核模块,纯用户态容器里跑不了。KVM、独立物理机、以及部分允许加载模块的 VPS 才可行。
装好后建议把 zfs 相关服务设为开机启用,避免重启后池没挂上:
systemctl enable zfs-import-cache zfs-mount zfs-zed
systemctl status zfs-import-cache --no-pager
第二步:创建池——单盘、镜像还是 RAIDZ
ZFS 里「池(pool)」相当于 LVM 的卷组,里面可以再切出多个「数据集(dataset)」,数据集更像是一个个可独立设配额、压缩、快照的文件夹。单盘个人站最简建池命令如下:
zpool create -o ashift=12 tank /dev/disk/by-id/ata-XXXXX
zpool status
zfs list
几个关键点必须说清楚:
- 务必用
/dev/disk/by-id/而不是/dev/sdb。设备名在重启后可能漂移,用 by-id 才能保证池每次都找对盘。 ashift=12对应 4K 扇区对齐,几乎所有现代硬盘和 SSD 都应设置。设错会导致写放大,性能明显下降。ashift 一旦建池就无法更改,只能重建。- 单盘池没有冗余,一块盘挂了数据就没了,它只提供校验和快照能力。要冗余就得镜像(mirror)或 RAIDZ。两块盘做镜像:
zpool create -o ashift=12 tank mirror /dev/disk/by-id/ata-DISK1 /dev/disk/by-id/ata-DISK2
RAIDZ1 至少三块盘,RAIDZ2 至少四块,能容忍 1 块或 2 块盘同时损坏。个人站如果只有两块盘,镜像比 RAIDZ 更划算(RAIDZ 在两块盘时无意义)。还要提醒一句:RAIDZ 扩容很不灵活,加盘只能整组加,不能像镜像那样一块一块加。
第三步:数据集规划与常用属性
建好池之后不要把所有东西都堆在池根下,而是按用途切数据集,这样每个数据集都能独立设压缩、配额和快照策略:
zfs create tank/www
zfs create tank/mysql
zfs create tank/backup
zfs set compression=lz4 tank/www
zfs set compression=zstd tank/mysql
zfs set quota=20G tank/backup
zfs set mountpoint=/var/www tank/www
zfs set atime=off tank/www
几个属性值得记住:
- compression:
lz4几乎零 CPU 开销、压缩比也不错,是通用首选;zstd压缩率更高但吃 CPU,适合数据库和日志这类文本多的场景。压缩是「透明」的,对上层程序完全无感。 - atime=off:关闭访问时间更新,能显著减少不必要的写操作。绝大多数网站根本不需要 atime。
- quota / refquota:quota 限制数据集及其子孙的总量,refquota 只限制数据集自身。给备份目录设 quota 能防止备份把池写满。
- recordsize:默认 128K,数据库场景可设
recordsize=16K以减少写放大;纯大文件存储可设1M。
第四步:ARC 缓存——小内存机器的调参重点
ZFS 性能的核心是 ARC(Adaptive Replacement Cache),它用内存做读缓存。问题在于 ZFS 默认会「尽量多吃内存」,小 VPS 上如果不限制,容易和 PHP、MySQL 抢内存,甚至触发 OOM Killer 把数据库杀掉。
限制 ARC 大小的正确做法是设置内核模块参数。Debian/Ubuntu 上写入 /etc/modprobe.d/zfs.conf:
options zfs zfs_arc_max=536870912
options zfs zfs_arc_min=134217728
上面把 ARC 上限设为 512MB、下限 128MB。改完要重建 initramfs 并重启才生效:
update-initramfs -u -k all
reboot
重启后可以实时查看 ARC 的命中情况,判断内存给得够不够:
arc_summary
# 或者看原始计数
cat /proc/spl/kstat/zfs/arcstats | grep -E "hits|misses|size"
经验值:ARC 上限大约取物理内存的一半到三分之二。1GB 内存的机器给 256~384MB 比较稳妥;2GB 给 512MB~1GB。如果 arc_summary 里命中率长期低于 80%,说明缓存太小或者访问模式本身很随机,不必强求。
第五步:快照——ZFS 最实用的功能
ZFS 快照是「瞬间」完成的,因为它基于写时复制(Copy-on-Write):创建时只记录当前状态,不复制任何数据。快照几乎不占空间,只有当原数据被修改时,旧块才被保留下来。这意味着你可以随便打快照,即便数据库正在运行也没关系。
# 手动打一个快照
zfs snapshot tank/mysql@before_upgrade
# 列出快照
zfs list -t snapshot
# 看快照占用的实际空间
zfs list -t snapshot -o name,used,refer
恢复很简单,但要注意 rollback 会丢弃快照之后的所有修改,务必先确认:
# 先销毁快照之后的部分数据(示例:先看差异)
zfs rollback -R tank/mysql@before_upgrade
如果只是想找回某个误删的文件,不必整卷回滚——直接把快照目录当普通文件夹进去拷就行:
ls /tank/mysql/.zfs/snapshot/before_upgrade/
cp /tank/mysql/.zfs/snapshot/before_upgrade/users.sql /tmp/
自动快照可以通过 zfs-auto-snapshot 或顺手写个 cron 完成,例如每小时保留 24 个、每天保留 7 个:
zfs snapshot tank/www@hourly-$(date +%Y%m%d%H)
# 清理七天前的
zfs list -t snapshot -H -o name | grep 'tank/www@hourly-' | head -n -168 | xargs -r -n1 zfs destroy
第六步:send / receive 异地备份
ZFS 原生的 send/receive 是增量异地备份的利器。快照是块级的,传输的也是块级流,比 rsync 逐文件比对更高效,而且天然保持一致性。第一次做全量:
zfs snapshot tank/www@bk1
zfs send tank/www@bk1 | ssh backup-host zfs receive backup/www
之后做增量,只传上一个快照到当前快照之间的差异:
zfs snapshot tank/www@bk2
zfs send -i tank/www@bk1 tank/www@bk2 | ssh backup-host zfs receive backup/www
推荐加上 -c 参数启用压缩传输,省带宽(前提是两端都支持):
zfs send -c -i tank/www@bk1 tank/www@bk2 | ssh backup-host zfs receive backup/www
要恢复就是把流反向传回来,或者直接在备份机上 rollback。注意:做增量 send 时两端的快照必须严格对应,如果目标端缺少 bk1,增量就会失败,这时只能重做全量。所以建议写脚本时记录「上一次成功传输的快照名」。
第七步:数据校验与 scrub 定期检查
ZFS 在写入时对每个数据块计算校验和,读取时校验。一旦发现校验不匹配,并且有冗余(镜像/RAIDZ),它会自动用健康副本修复——这就是「端到端数据完整性」的价值,对静默损坏(silent data corruption)尤其重要。
定期让 ZFS 主动扫描全盘校验数据,称为 scrub:
zpool scrub tank
zpool status tank # 查看 scrub 进度和发现的错误
建议每月跑一次,可以用 cron 定时。scrub 会占用磁盘 I/O,尽量避开业务高峰。如果 zpool status 里出现 CKSUM 大于 0 的行,说明那块盘或那条链路有问题,要认真排查——单盘池上校验错误意味着数据已经无法修复。
第八步:几个新手必踩的坑
- 内存没限 ARC 就上生产:小 VPS 上这是最常见的翻车原因,MySQL 被 OOM 干掉后你会一脸懵。装完 ZFS 第一件事就是设
zfs_arc_max。 - 用 /dev/sdX 建池:重启后盘序变了,池可能 import 失败甚至挂错盘。永远用 by-id。
- 以为单盘池能防数据丢失:单盘 ZFS 只能发现损坏,不能修复。要保护数据必须有多副本、镜像或 RAIDZ。
- 把池用到 100% 满:ZFS 在池写满时性能会断崖式下跌,还会碎片化严重。建议保持在 80% 以下,用 quota 兜底。
- snapshot 当备份用:快照和池在同一块盘上,盘挂了快照一起没。真正的备份必须 send 到另一台机器或另一块物理硬盘。
- dedup 随手开:去重(dedup)非常吃内存,每 TB 数据可能要好几 GB RAM,个人站几乎永远不该开。
小结
ZFS 对个人站长的真正价值,不在于它能跑多快,而在于它给了你一套「数据不会悄悄坏掉」的保障:校验和发现损坏、快照让你随时回退、send/receive 让异地备份变得可靠。这些能力加起来,比省几百兆内存重要得多。
实操建议是:先在测试机或非关键目录上把建池、限 ARC、打快照、send 备份走一遍,确认流程顺畅了再迁移线上数据。迁移时用 rsync 把数据倒进 ZFS 数据集,重建好目录权限,然后规规矩矩地设好自动快照和异地备份。这样一套下来,你的网站就有了真正的数据安全带。