ZFS 文件系统实战:单盘池建池、ARC 内存限制、快照回滚与 send/receive 异地备份

为什么个人站长值得认真考虑 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 数据集,重建好目录权限,然后规规矩矩地设好自动快照和异地备份。这样一套下来,你的网站就有了真正的数据安全带。

Last modification:October 3rd, 2026 at 09:24 pm

Leave a Comment