btrfs 快照不是任何意义上的复制。btrfs subvolume snapshot 建立的新子卷,其根指向与源完全相同的 extent 树,两边共享的每个 extent 被引用两次、只存一份。创建是一次元数据操作:子卷里装的是 1 GiB 还是 10 TiB,耗时都在毫秒级。

这让快照成为机器上最便宜的回滚原语,也正是它最常被过度信任的原因。空间随分岔增长,而不随数据量增长;代价来得晚,落在别人的写操作上,而且落在保存原始数据的那块设备上。从不清理的快照是一场慢动作的磁盘写满事故,而同盘快照无论留多少份都不是备份。

共享 extent,而不是复制数据块

快照就是子卷,而子卷是拥有自己根的子树,所以快照机制与让 reflink 生效的是同一套引用计数。用 btrfs subvolume snapshot -r /data /snap/data-2026-09-16 打一个只读快照;-r 很关键,因为可写快照在任何一次写入发生的那一刻就开始分岔,而备份目标本来就不该可写。快照与源是平级的子卷,清理用 btrfs subvolume delete /snap/data-2026-09-16;正在被挂载使用的子卷删不掉,btrfs subvolume list -t 能把快照按父子关系列成表,按时间做保留时用得上。

真正花钱的是快照之后的那次写入。任意一侧覆盖一个共享 extent 时,文件系统给写入方分配新 extent,把旧 extent 留给另一侧,所以快照的价格由随后的写入支付,计量单位是这次写入触及的 extent 数,而不是快照命令本身。APFS 的 clonefile 遵循同一规则,fixture 量到的形状是:克隆 64 MiB 花费 0 KiB,普通复制 64 MiB 花费 65536 KiB,覆盖克隆文件前 8 MiB 花费 8192 KiB;而把这 8 MiB 再改写一次花费 0 KiB,因为这些 extent 已经分岔过了。

空间到底去了哪里

btrfs filesystem du 给出诚实的账目:对每个参数打印 Total、Exclusive 与 Set shared,其中 Set shared 是它与命令行上其他子卷共享的空间。刚打完快照时,Exclusive 应接近零,Set shared 接近源的 Total。compsize 按文件给出同样的划分,开了配额后 btrfs qgroup show -reF 按 qgroup 追踪 exclusive 与 referenced 字节。df 在这里帮不上忙:它报的是整个文件系统,无法把空间归属到某个子卷或某一份快照,这正是需要 du 与 qgroup 的原因。

删掉源并不会释放共享 extent——它们变成快照的独占数据,空间只是换了主人。这是关于 btrfs 最常见的求助:文件删了,df 不动。只有当最后一个引用消失,空间才会归还,所以保留策略与快照频率必须一起设计。另外,Set shared 会把多个参数之间重叠的共享 extent 一并算进去,所以同时传源和快照得到的数字比两者逐个相加更小。

还原点不是副本

快照与源在同一个文件系统的同一块设备上,控制器故障、坏盘,或者一次搞砸的 btrfs check,会一次带走数据以及它的每一份快照。它们只防一类事故——本不该发生的那次写入——并且防得极好:回滚是本地操作、瞬时完成、不需要网络。

真正的保护意味着独立副本:btrfs send 遍历快照并输出一条流,btrfs receive 把它落到另一个文件系统上,-p 只发送相对父快照的增量。把这条流送到另一台机器或另一个池。要分清的界限是:send 才是复制机制,快照只是被复制的对象。只读快照同时充当 btrfs send -p 的父快照,于是增量复制的粒度就由快照频率决定:打得越密,每次送出去的增量越小。

坏掉的样子

最经典的是删了却不释放:有人删掉一个 40 GB 的目录,df 纹丝不动,于是他继续删更多东西来“补偿”。对子卷和它的各个快照跑 btrfs filesystem du -s,找出 Exclusive 值恰好能解释这个缺口的那一个。如果快照是只读的,解决办法是删快照,而不是删更多文件。

第二种是文件系统写满,在 btrfs 上比 ext4 更糟。元数据需要自己的空间,而没有空闲空间的写时复制文件系统无法为一次写入分配新 extent,于是一个占用 100% 的文件系统会拒绝在别处本可成功的写入,甚至转为只读。留出余量——5% 不是多疑——给快照保留量设上限,并记住:给一个接近写满的文件系统打快照,等于同时承诺为它的分岔也找到空间。此时 btrfs filesystem usage 里的 unallocated 会先耗尽,然后才轮到 free;看到 unallocated 接近零就该动手,而不是等写入开始报错。

快速验证

确认快照是共享而不是复制,并找出是谁占着空间:

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

du 对每条路径打印 Total、Exclusive、Set shared 三元组:新建快照的 Exclusive 至多是几 MiB 元数据,而 Set shared 应与源的 Total 相当。若 Exclusive 已经等于 Total,说明这不是快照而是完整拷贝。随后 btrfs filesystem usage 给出设备还能吸收多少分岔。要用 btrfs qgroup show -reF 的话得先 btrfs quota enable /data,没开配额时它不会报错,只是给不出数字。

来源