<?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>快照 · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/%E5%BF%AB%E7%85%A7/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/%E5%BF%AB%E7%85%A7/index.xml" rel="self" type="application/rss+xml"/><item><title>LVM 快照在时间上定格一块块设备</title><link>https://www.xbcatm.com/zh/posts/lvm-logical-volume-snapshot/</link><pubDate>Mon, 10 Apr 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/lvm-logical-volume-snapshot/</guid><description>逻辑卷快照保留原始数据块，让你能在不卸载分区的前提下备份或回滚。</description><content:encoded>&lt;p&gt;LVM 快照既不是拷贝，也不是 reflink：它是一个挂在源逻辑卷上的、大小固定的写时复制异常存储。&lt;code&gt;lvcreate -s -L 5G -n snap vg/data&lt;/code&gt; 从卷组里划走 5 GiB，之后 device-mapper 的 &lt;code&gt;snapshot&lt;/code&gt; 目标会在某个源 chunk 第一次被写时，把它的原始内容记录下来。无论源卷多大，创建都是瞬时的，因为创建时什么都没复制。&lt;/p&gt;
&lt;p&gt;这个大小就是你全部的安全余量，也正是最常被写错的参数。按“一周内没什么写入”配的存储，会被一次数据库 checkpoint 或一次 &lt;code&gt;rsync&lt;/code&gt; 在一小时内吃光；而它填满时快照不会降级，而是直接失效并开始返回 I/O 错误。恢复点恰好在你最需要它的那一刻消失，所以存储的剩余空间该进监控面板，而不是躺在 runbook 里。&lt;/p&gt;
&lt;h2 id="异常存储不是副本"&gt;异常存储，不是副本&lt;/h2&gt;
&lt;p&gt;一个经典的 LVM 快照用到两个 device-mapper 目标：源卷被重新映射为 &lt;code&gt;snapshot-origin&lt;/code&gt;，另有一个新的 &lt;code&gt;snapshot&lt;/code&gt; 设备把它与 COW 设备组合起来。读未改动的 chunk 直接落到源卷；某个 chunk 第一次被写时，旧 chunk 被复制进 COW 设备，然后新数据才写下去。所以消耗量跟“发生过改动的不同 chunk 数”成正比，而不是跟写入次数成正比：fixture 往 16 个热 chunk 写了一百万次，只消耗 15 个异常，约 60 KiB。&lt;/p&gt;
&lt;p&gt;用 &lt;code&gt;lvs -a -o lv_name,lv_attr,origin,data_percent,lv_size&lt;/code&gt; 观察它。经典快照的 &lt;code&gt;lv_attr&lt;/code&gt; 是 &lt;code&gt;swi-a-s---&lt;/code&gt;：&lt;code&gt;s&lt;/code&gt; 表示快照、&lt;code&gt;w&lt;/code&gt; 表示可写，而状态位在存储耗尽时会变成 &lt;code&gt;I&lt;/code&gt;。&lt;code&gt;data_percent&lt;/code&gt; 是存储的填充度，这才是该告警的数字，阈值取 70% 或 80%；100% 不是警告，是讣告。&lt;code&gt;--chunksize&lt;/code&gt; 决定异常存储的粒度：粒度越大，块映射表的元数据越省，但每一次改动要拷贝的旧数据也越多。快照还有效时可以用 &lt;code&gt;lvextend&lt;/code&gt; 扩容 COW 设备；&lt;code&gt;lvchange --monitor y&lt;/code&gt; 则让 dmeventd 自动做这件事。&lt;/p&gt;
&lt;h2 id="耗尽即失效"&gt;耗尽即失效&lt;/h2&gt;
&lt;p&gt;内核文档说得很直白：COW 设备往往比源卷小，一旦填满，快照就变得无用并被禁用，开始返回错误。LVM 给出的容量经验是按预期写入量规划——常常取源卷的 20%——并在填满之前扩容。另外，存储里有一小块要用来存放 COW 块映射表，所以可用于数据的容量略小于 &lt;code&gt;-L&lt;/code&gt; 的数值。&lt;/p&gt;
&lt;p&gt;影响范围很窄，但时机很残忍：源卷照常运行，所以在读取该快照的备份任务失败之前，或者在你要用的那次回滚已经不可用之前，看不出任何异常。它没有中间状态：到了 100%，正确的心理模型是这份快照从未存在过。&lt;/p&gt;
&lt;h2 id="合并回滚以及-thin-如何改变失败形态"&gt;合并回滚，以及 thin 如何改变失败形态&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lvconvert --merge vg/snap&lt;/code&gt; 把快照中改动过的 chunk 合并回源卷，合并在后台进行，期间源卷仍然可用——对尚未合并的 chunk 的写入会被推迟。约束在源卷一侧：建立合并时它必须处于非活动状态，所以要么先卸载，要么先安排合并、之后再激活源卷。合并只对持久化快照有效，临时快照走不了这条路；合并会消耗掉这份快照，所以需要的东西先拷出来。&lt;/p&gt;
&lt;p&gt;thin 快照改变的是失败形态，而不是消除失败。当源是 thin LV 且不给大小时，创建出来的就是 thin 快照；它共享 thin 池里的块，而不拥有自己的存储，所以它自身的分岔无法让它失效。此时共享资源变成了池：池一旦耗尽，坏掉的不是一份快照，而是池里所有 thin 卷，而 LVM 明确警告池可能因此损坏、且难以甚至无法修复。&lt;code&gt;thin_pool_autoextend_threshold&lt;/code&gt; 与 dmeventd 的存在，正是因为池耗尽是一次池级事故。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最经典的是“备份悄悄不再是备份”。任务挂载快照开始拷贝，拷贝耗时超过了存储能覆盖的时长，存储在某刻到达 100%；快照随即失效，拷贝失败——更糟的情况是，监控只看任务的退出码，而任务读的早就是一个过期的挂载点。所以要盯每一个快照 LV 的 &lt;code&gt;data_percent&lt;/code&gt;，70% 就告警，并且按保留窗口内最忙的那一小时来定容量，而不是按平均值。&lt;/p&gt;
&lt;p&gt;第二种是“要用的时候合并不了”。&lt;code&gt;lvconvert --merge&lt;/code&gt; 要求源卷处于关闭状态，而团队往往是在事故当中才发现这一点。合并开始后由 &lt;code&gt;snapshot-merge&lt;/code&gt; 目标接管源卷，&lt;code&gt;dmsetup status&lt;/code&gt; 里的已分配扇区数会持续下降，降到只剩元数据就表示合并完成。先在一个一次性卷上演练合并，另留一份不依赖快照的真实备份，并记住：存储已满的快照根本无法合并。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;在信任这份快照之前，先看存储的填充度：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lvs -a -o lv_name,vg_name,lv_attr,origin,data_percent,lv_size
lvs -a -o lv_name,data_percent,metadata_percent vg/thinpool
dmsetup status vg-snap
lvextend -L +2G vg/snap
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;data_percent&lt;/code&gt; 就是 COW 存储的填充度，健康的快照应当远低于 100；&lt;code&gt;lv_attr&lt;/code&gt; 里出现状态位 &lt;code&gt;I&lt;/code&gt; 说明快照已经失效。对 thin 快照而言，该看的是池上的 &lt;code&gt;data_percent&lt;/code&gt; 与 &lt;code&gt;metadata_percent&lt;/code&gt;；&lt;code&gt;dmsetup status&lt;/code&gt; 会给出已分配扇区与总扇区之比，可以据此实时观察一次合并把扇区数排空的过程。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/lvcreate.8.html"&gt;lvcreate(8)&lt;/a&gt;，说明 &lt;code&gt;-s&lt;/code&gt;/&lt;code&gt;--snapshot&lt;/code&gt;、COW 与 thin 快照的区别以及容量建议，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/lvs.8.html"&gt;lvs(8)&lt;/a&gt;，定义 &lt;code&gt;lv_attr&lt;/code&gt; 状态位，含无效快照，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/admin-guide/device-mapper/snapshot.html"&gt;Device-mapper snapshot 支持&lt;/a&gt;，异常存储、失效语义与 &lt;code&gt;lvconvert --merge&lt;/code&gt; 行为，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/lvmthin.7.html"&gt;lvmthin(7)&lt;/a&gt;，thin 快照、池耗尽与自动扩容，已核验 2026-09-16。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><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><item><title>Btrfs 快照是写时复制，不是复制</title><link>https://www.xbcatm.com/zh/posts/btrfs-snapshots-are-copy-on-write/</link><pubDate>Sun, 14 Nov 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/btrfs-snapshots-are-copy-on-write/</guid><description>Btrfs 快照与源共用未发生改动的块，一旦有写入才把那部分单独拷贝，因此创建几乎零成本，之后只有变化的数据占新空间。</description><content:encoded>&lt;p&gt;btrfs 快照不是任何意义上的复制。&lt;code&gt;btrfs subvolume snapshot&lt;/code&gt; 建立的新子卷，其根指向与源完全相同的 extent 树，两边共享的每个 extent 被引用两次、只存一份。创建是一次元数据操作：子卷里装的是 1 GiB 还是 10 TiB，耗时都在毫秒级。&lt;/p&gt;
&lt;p&gt;这让快照成为机器上最便宜的回滚原语，也正是它最常被过度信任的原因。空间随分岔增长，而不随数据量增长；代价来得晚，落在别人的写操作上，而且落在保存原始数据的那块设备上。从不清理的快照是一场慢动作的磁盘写满事故，而同盘快照无论留多少份都不是备份。&lt;/p&gt;
&lt;h2 id="共享-extent而不是复制数据块"&gt;共享 extent，而不是复制数据块&lt;/h2&gt;
&lt;p&gt;快照就是子卷，而子卷是拥有自己根的子树，所以快照机制与让 reflink 生效的是同一套引用计数。用 &lt;code&gt;btrfs subvolume snapshot -r /data /snap/data-2026-09-16&lt;/code&gt; 打一个只读快照；&lt;code&gt;-r&lt;/code&gt; 很关键，因为可写快照在任何一次写入发生的那一刻就开始分岔，而备份目标本来就不该可写。快照与源是平级的子卷，清理用 &lt;code&gt;btrfs subvolume delete /snap/data-2026-09-16&lt;/code&gt;；正在被挂载使用的子卷删不掉，&lt;code&gt;btrfs subvolume list -t&lt;/code&gt; 能把快照按父子关系列成表，按时间做保留时用得上。&lt;/p&gt;
&lt;p&gt;真正花钱的是快照之后的那次写入。任意一侧覆盖一个共享 extent 时，文件系统给写入方分配新 extent，把旧 extent 留给另一侧，所以快照的价格由随后的写入支付，计量单位是这次写入触及的 extent 数，而不是快照命令本身。APFS 的 clonefile 遵循同一规则，fixture 量到的形状是：克隆 64 MiB 花费 0 KiB，普通复制 64 MiB 花费 65536 KiB，覆盖克隆文件前 8 MiB 花费 8192 KiB；而把这 8 MiB 再改写一次花费 0 KiB，因为这些 extent 已经分岔过了。&lt;/p&gt;
&lt;h2 id="空间到底去了哪里"&gt;空间到底去了哪里&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;btrfs filesystem du&lt;/code&gt; 给出诚实的账目：对每个参数打印 Total、Exclusive 与 Set shared，其中 Set shared 是它与命令行上其他子卷共享的空间。刚打完快照时，Exclusive 应接近零，Set shared 接近源的 Total。&lt;code&gt;compsize&lt;/code&gt; 按文件给出同样的划分，开了配额后 &lt;code&gt;btrfs qgroup show -reF&lt;/code&gt; 按 qgroup 追踪 exclusive 与 referenced 字节。&lt;code&gt;df&lt;/code&gt; 在这里帮不上忙：它报的是整个文件系统，无法把空间归属到某个子卷或某一份快照，这正是需要 &lt;code&gt;du&lt;/code&gt; 与 qgroup 的原因。&lt;/p&gt;
&lt;p&gt;删掉源并不会释放共享 extent——它们变成快照的独占数据，空间只是换了主人。这是关于 btrfs 最常见的求助：文件删了，&lt;code&gt;df&lt;/code&gt; 不动。只有当最后一个引用消失，空间才会归还，所以保留策略与快照频率必须一起设计。另外，Set shared 会把多个参数之间重叠的共享 extent 一并算进去，所以同时传源和快照得到的数字比两者逐个相加更小。&lt;/p&gt;
&lt;h2 id="还原点不是副本"&gt;还原点不是副本&lt;/h2&gt;
&lt;p&gt;快照与源在同一个文件系统的同一块设备上，控制器故障、坏盘，或者一次搞砸的 &lt;code&gt;btrfs check&lt;/code&gt;，会一次带走数据以及它的每一份快照。它们只防一类事故——本不该发生的那次写入——并且防得极好：回滚是本地操作、瞬时完成、不需要网络。&lt;/p&gt;
&lt;p&gt;真正的保护意味着独立副本：&lt;code&gt;btrfs send&lt;/code&gt; 遍历快照并输出一条流，&lt;code&gt;btrfs receive&lt;/code&gt; 把它落到另一个文件系统上，&lt;code&gt;-p&lt;/code&gt; 只发送相对父快照的增量。把这条流送到另一台机器或另一个池。要分清的界限是：&lt;code&gt;send&lt;/code&gt; 才是复制机制，快照只是被复制的对象。只读快照同时充当 &lt;code&gt;btrfs send -p&lt;/code&gt; 的父快照，于是增量复制的粒度就由快照频率决定：打得越密，每次送出去的增量越小。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;最经典的是删了却不释放：有人删掉一个 40 GB 的目录，&lt;code&gt;df&lt;/code&gt; 纹丝不动，于是他继续删更多东西来“补偿”。对子卷和它的各个快照跑 &lt;code&gt;btrfs filesystem du -s&lt;/code&gt;，找出 Exclusive 值恰好能解释这个缺口的那一个。如果快照是只读的，解决办法是删快照，而不是删更多文件。&lt;/p&gt;
&lt;p&gt;第二种是文件系统写满，在 btrfs 上比 ext4 更糟。元数据需要自己的空间，而没有空闲空间的写时复制文件系统无法为一次写入分配新 extent，于是一个占用 100% 的文件系统会拒绝在别处本可成功的写入，甚至转为只读。留出余量——5% 不是多疑——给快照保留量设上限，并记住：给一个接近写满的文件系统打快照，等于同时承诺为它的分岔也找到空间。此时 &lt;code&gt;btrfs filesystem usage&lt;/code&gt; 里的 unallocated 会先耗尽，然后才轮到 free；看到 unallocated 接近零就该动手，而不是等写入开始报错。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;确认快照是共享而不是复制，并找出是谁占着空间：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;btrfs subvolume snapshot -r /data /snap/data-$(date +%F)
btrfs filesystem du -s /data /snap/data-2026-09-16
compsize /snap/data-2026-09-16
btrfs filesystem usage /data
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;du&lt;/code&gt; 对每条路径打印 Total、Exclusive、Set shared 三元组：新建快照的 Exclusive 至多是几 MiB 元数据，而 Set shared 应与源的 Total 相当。若 Exclusive 已经等于 Total，说明这不是快照而是完整拷贝。随后 &lt;code&gt;btrfs filesystem usage&lt;/code&gt; 给出设备还能吸收多少分岔。要用 &lt;code&gt;btrfs qgroup show -reF&lt;/code&gt; 的话得先 &lt;code&gt;btrfs quota enable /data&lt;/code&gt;，没开配额时它不会报错，只是给不出数字。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-subvolume.html"&gt;btrfs 子卷文档&lt;/a&gt;，子卷与快照语义，含只读快照，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-filesystem.html"&gt;btrfs filesystem 文档&lt;/a&gt;，定义上文所用的 &lt;code&gt;du&lt;/code&gt; Exclusive 与 Set shared 列，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-qgroup.html"&gt;btrfs qgroup 文档&lt;/a&gt;，按 qgroup 统计 exclusive 与 referenced 字节，已核验 2026-09-16。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://btrfs.readthedocs.io/en/latest/btrfs-send.html"&gt;btrfs send 文档&lt;/a&gt;，把快照送出本机的复制流，已核验 2026-09-16。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>