<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zfs · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/zfs/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/zfs/index.xml" rel="self" type="application/rss+xml"/><item><title>ZFS 快照给文件系统一个回退键</title><link>https://www.xbcatm.com/zh/posts/zfs-snapshots-multiversion-safety/</link><pubDate>Tue, 21 Mar 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/zfs-snapshots-multiversion-safety/</guid><description>ZFS 快照在某个瞬间记录一份一致视图，数据变化前几乎零成本，且可随时回滚。</description><content:encoded>&lt;p&gt;ZFS 快照就是给某个数据集在某个 transaction group 上的块树起的一个名字。它不复制任何东西、不冻结任何东西，代价只是几个元数据块：&lt;code&gt;zfs snapshot tank/data@before-upgrade&lt;/code&gt; 会在 shell 提示符重画完之前就返回。由于 ZFS 从不原地覆盖仍在使用的块，旧块树在磁盘上一直有效，直到最后一个引用消失——而快照就是这个引用。&lt;/p&gt;
&lt;p&gt;正是这一点让“一墙的每日历史”变得可行，也正是本文标题最危险的地方。快照给你数据的许多个版本，却不给你“池不会丢”这件事的保障。&lt;code&gt;tank/data&lt;/code&gt; 的每一份快照都住在 &lt;code&gt;tank&lt;/code&gt; 里，所以带走池的那次故障，会在同一瞬间带走数据集和它的全部历史。多版本与多版本安全是两种不同的性质，后者只能由复制来提供。&lt;/p&gt;
&lt;h2 id="为一个一致的树命名"&gt;为一个一致的树命名&lt;/h2&gt;
&lt;p&gt;快照建立在 transaction group 边界上，所以它捕获的树内部自洽——数据库的文件与元数据冻结在同一瞬间，而不是相差几微秒。&lt;code&gt;-r&lt;/code&gt; 会连同所有后代数据集一起打快照，凡是有子数据集的地方都该这么用；否则一个递归布局会在不同层级留下时间不同的快照，层与层之间的增量复制就变得别扭。&lt;/p&gt;
&lt;p&gt;名字由你决定，而它是保留策略唯一的抓手：&lt;code&gt;tank/data@daily-2026-09-16&lt;/code&gt; 可以按模式清理，自动生成的名字则需要额外记账。&lt;code&gt;zfs list -t snapshot -o name,used,refer,creation -s creation&lt;/code&gt; 列出整个快照集以及每一份钉住的空间。&lt;/p&gt;
&lt;h2 id="空间在哪里zfs-list--o-space"&gt;空间在哪里：zfs list -o space&lt;/h2&gt;
&lt;p&gt;ZFS 用一组列来回答唯一重要的问题：这是谁的数据？&lt;code&gt;zfs list -o space tank/data&lt;/code&gt; 会给出 USED、USEDSNAP、USEDDS、USEDREFRESERV、USEDCHILD、REFER 与 AVAIL。REFER 是数据集自身需要的量，USEDSNAP 是快照钉住的量，USED 是二者之和。USEDSNAP 持续上涨而 REFER 保持不变，说明吃掉池子的是快照，不是数据。具体到某一份快照，&lt;code&gt;used&lt;/code&gt; 是它单独钉住的块，&lt;code&gt;refer&lt;/code&gt; 是它能引用到的全部块，两者之差正是与其他版本共用的那部分。&lt;/p&gt;
&lt;p&gt;fixture 在 64 MiB 的数据集上搭了一墙 30 份每日快照，每天翻动 2 MiB。每一次创建都花费 0 KiB——代价落在紧随其后的那次写入上，重写 2 MiB 就是 2048 KiB——整墙历史的代价是 60 MiB，而 30 份完整拷贝需要 1920 MiB。这个比例就是快照存在的全部理由，也解释了为什么池子直到快满才有人注意到。反过来，当池子告急时，该做的第一件事是按 &lt;code&gt;zfs list -t snapshot -o name,used&lt;/code&gt; 倒序找出钉住空间最多的那一份。&lt;/p&gt;
&lt;h2 id="holddiff-与版本对比"&gt;hold、diff 与版本对比&lt;/h2&gt;
&lt;p&gt;hold 是一个具名引用，它让销毁以 EBUSY 失败：&lt;code&gt;zfs hold keep tank/data@daily-2026-09-16&lt;/code&gt; 保护一份快照，&lt;code&gt;zfs holds&lt;/code&gt; 列出持有者，&lt;code&gt;zfs release keep&lt;/code&gt; 解除。备份软件会给正在发送的那份快照加 hold，免得保留脚本把它删掉；代价是保留任务会一直以“快照忙”失败，直到 hold 被释放，而一个卡住的 hold 能让一份快照被钉住好几个月。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zfs diff tank/data@daily-2026-09-15 tank/data@daily-2026-09-16&lt;/code&gt; 按路径报告变化——&lt;code&gt;+&lt;/code&gt; 新建、&lt;code&gt;-&lt;/code&gt; 删除、&lt;code&gt;M&lt;/code&gt; 修改、&lt;code&gt;R&lt;/code&gt; 重命名——并且两份快照都必须仍然存在，所以对比昨天很容易，对比上个月未必。要看快照里的内容，走数据集下隐藏的 &lt;code&gt;.zfs/snapshot/&amp;lt;名字&amp;gt;&lt;/code&gt; 目录；快照没有单独的挂载点，而 &lt;code&gt;snapdir=visible&lt;/code&gt; 能让这个目录被列出来。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;真正要紧的失败，恰恰是标题诱导你去相信的那种：池丢了。控制器故障、背板故障或者池级损坏，会原子地删掉数据集以及它的每一份快照，没有部分幸存这回事。唯一的防线是池外的那份拷贝：&lt;code&gt;zfs send -i tank/data@yesterday tank/data@today | zfs receive backup/data&lt;/code&gt; 会把一条独立、带校验和的流写进第二个池，只有它能挺过第一个池的死亡。一墙快照却没有副本，是换了个好看界面的单点故障。池内的冗余（镜像、RAIDZ）与 scrub 解决的是另一类问题——坏盘与静默损坏——它们替代不了池外副本，因为控制器或背板故障会把同一个池里的冗余一起带走。&lt;/p&gt;
&lt;p&gt;第二种是空间悄悄爬升、保留任务却跑不动。USEDSNAP 上涨而 REFER 不动，池越过 80%，随后 ZFS 开始以“空间不足”拒绝写入，尽管从数据集上看一切正常——池的 slop 与预留量在文件系统内部根本看不见。如果某份快照还被某个 clone 钉住，销毁会失败；而要回滚到 clone 的源之前，就必须连带递归销毁那个 clone。长寿命的 hold 会把保留策略变成一张依赖图，所以 hold 的存活时间要短、要可审计。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;看清快照的代价，以及它们能不能被删掉：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每份快照的 &lt;code&gt;used&lt;/code&gt; 是它单独钉住的空间，刚创建的快照应接近零；&lt;code&gt;space&lt;/code&gt; 那组列则给出数据集上的 USEDSNAP 与 REFER，USEDSNAP 上涨而 REFER 不变就是该清理的信号。若 &lt;code&gt;zfs destroy&lt;/code&gt; 报快照忙，&lt;code&gt;zfs holds&lt;/code&gt; 会告诉你要释放哪个 tag。最后一条是唯一能防住池丢失的操作，应该定时执行。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-snapshot.8.html"&gt;zfs-snapshot(8)&lt;/a&gt;，快照创建语义与递归快照，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-list.8.html"&gt;zfs-list(8)&lt;/a&gt;，&lt;code&gt;space&lt;/code&gt; 列集，含 USEDSNAP、USEDDS 与 REFER，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-hold.8.html"&gt;zfs-hold(8)&lt;/a&gt;，hold、&lt;code&gt;zfs holds&lt;/code&gt; 以及销毁返回的 EBUSY，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-send.8.html"&gt;zfs-send(8)&lt;/a&gt;，把数据送出本池的增量复制流，已核验 2026-09-16。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>