ZFS 快照就是给某个数据集在某个 transaction group 上的块树起的一个名字。它不复制任何东西、不冻结任何东西,代价只是几个元数据块:zfs snapshot tank/data@before-upgrade 会在 shell 提示符重画完之前就返回。由于 ZFS 从不原地覆盖仍在使用的块,旧块树在磁盘上一直有效,直到最后一个引用消失——而快照就是这个引用。

正是这一点让“一墙的每日历史”变得可行,也正是本文标题最危险的地方。快照给你数据的许多个版本,却不给你“池不会丢”这件事的保障。tank/data 的每一份快照都住在 tank 里,所以带走池的那次故障,会在同一瞬间带走数据集和它的全部历史。多版本与多版本安全是两种不同的性质,后者只能由复制来提供。

为一个一致的树命名

快照建立在 transaction group 边界上,所以它捕获的树内部自洽——数据库的文件与元数据冻结在同一瞬间,而不是相差几微秒。-r 会连同所有后代数据集一起打快照,凡是有子数据集的地方都该这么用;否则一个递归布局会在不同层级留下时间不同的快照,层与层之间的增量复制就变得别扭。

名字由你决定,而它是保留策略唯一的抓手:tank/data@daily-2026-09-16 可以按模式清理,自动生成的名字则需要额外记账。zfs list -t snapshot -o name,used,refer,creation -s creation 列出整个快照集以及每一份钉住的空间。

空间在哪里:zfs list -o space

ZFS 用一组列来回答唯一重要的问题:这是谁的数据?zfs list -o space tank/data 会给出 USED、USEDSNAP、USEDDS、USEDREFRESERV、USEDCHILD、REFER 与 AVAIL。REFER 是数据集自身需要的量,USEDSNAP 是快照钉住的量,USED 是二者之和。USEDSNAP 持续上涨而 REFER 保持不变,说明吃掉池子的是快照,不是数据。具体到某一份快照,used 是它单独钉住的块,refer 是它能引用到的全部块,两者之差正是与其他版本共用的那部分。

fixture 在 64 MiB 的数据集上搭了一墙 30 份每日快照,每天翻动 2 MiB。每一次创建都花费 0 KiB——代价落在紧随其后的那次写入上,重写 2 MiB 就是 2048 KiB——整墙历史的代价是 60 MiB,而 30 份完整拷贝需要 1920 MiB。这个比例就是快照存在的全部理由,也解释了为什么池子直到快满才有人注意到。反过来,当池子告急时,该做的第一件事是按 zfs list -t snapshot -o name,used 倒序找出钉住空间最多的那一份。

hold、diff 与版本对比

hold 是一个具名引用,它让销毁以 EBUSY 失败:zfs hold keep tank/data@daily-2026-09-16 保护一份快照,zfs holds 列出持有者,zfs release keep 解除。备份软件会给正在发送的那份快照加 hold,免得保留脚本把它删掉;代价是保留任务会一直以“快照忙”失败,直到 hold 被释放,而一个卡住的 hold 能让一份快照被钉住好几个月。

zfs diff tank/data@daily-2026-09-15 tank/data@daily-2026-09-16 按路径报告变化——+ 新建、- 删除、M 修改、R 重命名——并且两份快照都必须仍然存在,所以对比昨天很容易,对比上个月未必。要看快照里的内容,走数据集下隐藏的 .zfs/snapshot/<名字> 目录;快照没有单独的挂载点,而 snapdir=visible 能让这个目录被列出来。

坏掉的样子

真正要紧的失败,恰恰是标题诱导你去相信的那种:池丢了。控制器故障、背板故障或者池级损坏,会原子地删掉数据集以及它的每一份快照,没有部分幸存这回事。唯一的防线是池外的那份拷贝:zfs send -i tank/data@yesterday tank/data@today | zfs receive backup/data 会把一条独立、带校验和的流写进第二个池,只有它能挺过第一个池的死亡。一墙快照却没有副本,是换了个好看界面的单点故障。池内的冗余(镜像、RAIDZ)与 scrub 解决的是另一类问题——坏盘与静默损坏——它们替代不了池外副本,因为控制器或背板故障会把同一个池里的冗余一起带走。

第二种是空间悄悄爬升、保留任务却跑不动。USEDSNAP 上涨而 REFER 不动,池越过 80%,随后 ZFS 开始以“空间不足”拒绝写入,尽管从数据集上看一切正常——池的 slop 与预留量在文件系统内部根本看不见。如果某份快照还被某个 clone 钉住,销毁会失败;而要回滚到 clone 的源之前,就必须连带递归销毁那个 clone。长寿命的 hold 会把保留策略变成一张依赖图,所以 hold 的存活时间要短、要可审计。

快速验证

看清快照的代价,以及它们能不能被删掉:

zfs list -t snapshot -o name,used,refer,creation -s creation tank/data
zfs list -o space tank/data
zfs holds tank/data@daily-2026-09-16
zfs diff tank/data@daily-2026-09-15 tank/data@daily-2026-09-16
zfs send -i tank/data@daily-2026-09-15 tank/data@daily-2026-09-16 | zfs receive backup/data

每份快照的 used 是它单独钉住的空间,刚创建的快照应接近零;space 那组列则给出数据集上的 USEDSNAP 与 REFER,USEDSNAP 上涨而 REFER 不变就是该清理的信号。若 zfs destroy 报快照忙,zfs holds 会告诉你要释放哪个 tag。最后一条是唯一能防住池丢失的操作,应该定时执行。

来源

  • zfs-snapshot(8),快照创建语义与递归快照,已核验 2026-09-16。
  • zfs-list(8),space 列集,含 USEDSNAP、USEDDS 与 REFER,已核验 2026-09-16。
  • zfs-hold(8),hold、zfs holds 以及销毁返回的 EBUSY,已核验 2026-09-16。
  • zfs-send(8),把数据送出本池的增量复制流,已核验 2026-09-16。