同一套 Btrfs 代码,在三种机器上是三种命运。这篇文章不讲「怎么装 Btrfs」,只讲它那些高级特性到底在做什么、代价是什么、什么情况下不该开,最后回到一台 UGOS NAS 上,解释为什么它「支持 Btrfs」你却几乎什么开关都动不了。


0. 为什么「支持 Btrfs」≠「能用 Btrfs」——同一个文件系统,三种命运

先看三台机器,它们的 mkfs.btrfs 是同一份代码,mount 之后的能力却完全不同。

A 机:自建的 Linux 服务器。 两块 NVMe 直接做 mkfs.btrfs -d raid1 -m raid1c3,/etc/fstab 自己写,btrfs 命令全在 PATH 里。快照、scrub、send/receive、qgroup、balance 想用哪个用哪个,唯一的约束是「我敢不敢」。

B 机:成品 NAS(群晖、绿联 UGOS、威联通等)。 系统确实建了 Btrfs,也确实在用快照和 scrub 这些能力——但那是厂商在替你做。你打开 UI,看到的是「快照计划」「空间分析」「数据清理」几个开关,底下对应的 btrfs subvolume snapshot、btrfs scrub start、btrfs balance start 你都碰不到。SSH 进去,btrfs 可能压根没装,或者装了但被策略挡住。

C 机:一台装了 Btrfs 就当 ZFS 用的机器。 能建快照、能 scrub、能 send/receive,但从来没跑过 scrub,从来没验证过快照能还原,也从没看过 btrfs device stats。它「有」Btrfs 的所有能力,等于一个都没「用」上。

把这三台机器按两个维度切开,就是一张四象限:

厂商对外暴露了 CLI/API厂商把 Btrfs 藏在内部
厂商自己在用这些特性能力齐全,风险自担(A 机)能用,但只能按厂商的剧本用(B 机,本文重点)
厂商只把它当个普通文件系统名义支持,实际没人维护(少见)最糟:既没有特性,也没有人懂它(C 机)

所以「支持 Btrfs」这句话至少漏掉了三个问题:

  1. 支持到哪一层? 是内核编进了 Btrfs 模块,还是连 btrfs-progs 都齐?内核支持只决定「能挂载」,用户态工具才决定「能运维」。
  2. 谁能操作? 是厂商替你跑,还是把命令交给你?前者意味着你买到的是「结果」,不是「能力」。
  3. 底下垫了什么? Btrfs 是直接坐在裸盘上,还是坐在 LVM、md RAID、bcache、dm-crypt 上面?这一层决定了Btrfs 的校验和还能不能修数据——第 5 节会专门算这笔账。

后文所有内容,都是围绕这三个问题展开的。先把底层三个概念讲透,因为后面 9 个特性没有一个是凭空长出来的。


1. 底层三概念:COW、双份校验和、子卷

Btrfs 的高级特性看起来五花八门,其实全部是三个机制的排列组合。理解这三个,剩下的都是推论。

1.1 COW(写时复制):改数据不改原地

传统文件系统改一个字节,就是把这个字节所在的块原地改写。Btrfs 不这么干:它把被修改的整个数据块复制一份新的,写在新位置,然后改指针指过去,旧块原封不动留着。

一次写入在 Btrfs 里的实际流程是这样的:

用户写 4K
  → 分配新 extent(新物理位置),写入数据
  → 计算 csum,写进 csum tree(校验收据)
  → 生成新的 extent item,指向新位置(旧 extent 的引用计数 -1)
  → 自底向上重建这条路径上的 metadata 块(同样写新位置)
  → 最后原子地切换 tree root 指针,提交事务

这个顺序里藏着两个关键性质:

  • 旧数据永远没被覆盖,所以「拍快照」只需要复制一份 tree root 指针,不需要复制任何数据块,耗时是常数级。
  • 提交是原子的,所以掉电最多丢掉最后一个未提交的事务,不会出现「改了一半」的中间态——这也是 Btrfs 不需要 fsck 善后、只需要 btrfs check 体检的原因(它是「体检」不是「修车」)。

代价也很直接:碎片化和写放大。改 1 字节要付 4K 的新数据 + 一路 metadata 的成本。顺序写还好,SSD 上层不在乎、HDD 上也还能靠 extent 连续分配兜住;真正难受的是「随机小写入」——数据库、虚拟机镜像这类负载在 COW 下会迅速碎成一地,每次都写到新位置、每次都要更新 csum 和 metadata,性能掉得很难看。nodatacow(第 2.8 节)就是为这类负载开的后门,代价是放弃校验和与快照。

1.2 双份校验和:Btrfs 最值钱的东西,也是最容易被误解的东西

Btrfs 对每个数据块和每个 metadata 块都算校验和,默认算法是 CRC32c(也可以 mkfs 时选 xxhash 或 blake2b),4K 一块,存在独立的 csum tree 里。注意这里说的「双份」有两层含义,两层都要讲清楚:

第一层:数据和它的校验和是分开存的。 硬盘静默翻转一个 bit,数据变了、csum 没变,一读就发现对不上。这不是「备份」,而是完整性证据。

第二层:metadata 默认存两份。 单盘上 metadata 默认 profile 就是 DUP(同一块盘上两个物理位置各一份),多盘上 -m raid1/raid1c3 才是跨盘。所以哪怕单盘 Btrfs,metadata 损坏也常常能自愈;但data 默认是 single,一份就是一份。

这里必须澄清一个流传很广的误解:

校验和能「发现」损坏,不等于能「修复」损坏。

修复的前提是「还有第二份好的、且 Btrfs 自己看得见」。Btrfs 修一个坏块的逻辑是:读 A 副本 → csum 不符 → 找同逻辑地址的 B 副本 → 读出来验 csum → 对得上就用 B 覆盖 A。如果 Btrfs 只认识一块设备,它连「B 副本在哪」都不知道,只能返回 EIO。 第 5 节会看到,这正是你这台机器的处境。

还有两个容易踩的边界:

  • nodatasum(或 nodatacow 隐含的 nodatasum)的文件不算 csum。省了一点 CPU 和写放大,代价是这个文件从此对静默损坏完全失明。
  • csum 只能证明「这块数据不是当初写进去的那块」,不能证明「这个文件是完整的」。文件被截断、目录项丢了,csum 都管不着——那是 metadata 和文件系统结构的事。

1.3 子卷(subvolume):文件系统里的「独立小文件系统」

子卷是 Btrfs 内部的独立命名空间,有自己的 inode 编号空间(从 256 起)、自己的 tree root、自己的 UUID。它可以单独挂载、单独设配额、单独做快照、单独 send 出去。

文件系统(一个 Btrfs,一个 UUID)
├── 5  顶层子卷(永远存在,不能删)
├── 256  @           ← 系统根
├── 257  @home       ← /home
├── 258  @snapshots  ← 快照存放处
└── 259  @data       ← 数据盘

几个必须知道的事实:

  • 子卷不是目录。/mnt/btrfs/@home 和 /mnt/btrfs/@data 在同一个文件系统里,跨子卷 mv 是复制+删除(同子卷内才是 rename,瞬间完成)。
  • 快照是子卷的一种,默认创建出来的可写快照和普通子卷没有区别;-r 才是只读快照,只读快照才能 btrfs send。
  • 子卷可以嵌套,但要记住它和普通目录不是一类东西:嵌套子卷在父目录里只是一个挂载点,挂载之前那个位置是空的;btrfs subvolume list 列的是子卷清单(-a 显示绝对路径,-p 显示父 subvolume id),不是目录树。别拿它的输出当 ls 的替代品。
  • qgroup 是按子卷编号记账的(第 2.6 节),所以「给某个共享目录限 500G」在 Btrfs 里的正确姿势是「给它一个独立子卷 + qgroup limit」。

这三个概念一旦立住,下面 9 个特性基本就是推论:

派生自特性
COW快照、send/receive、reflink 去重、nodatacow(反向)
校验和scrub、数据自愈、btrfs device stats 的错误计数
子卷快照的载体、qgroup 的记账单位、send/receive 的传输单位
extent/chunk 布局压缩、balance、defrag、原生 RAID

2. 逐条拆解 9 个特性

每条都按同一个模板走:它解决什么问题 → 机制一句话 → 怎么用 → 代价与坑。赶时间的话可以直接跳到第 4 节的速查表,但坑都在正文里。

2.1 快照(snapshot):最便宜的时间机器,也是最贵的错觉

解决什么:升级前留退路、误删后找回、给备份提供一致性时间点。

机制:新建一个子卷,它的 tree root 指向当前的文件树,所有 extent 引用计数 +1。不复制数据,常数时间完成,1TB 和 1GB 的快照一样快。

# 只读快照(备份/归档用,能 send)
btrfs subvolume snapshot -r /mnt/data /mnt/snapshots/data-$(date +%F)

# 可写快照(当"还原点"用,可直接切回去)
btrfs subvolume snapshot /mnt/data /mnt/snapshots/data-writable

# 看快照与父卷的关系
btrfs subvolume show /mnt/snapshots/data-2026-10-20
btrfs subvolume list -p /mnt

代价与坑:

  • 快照不占空间是「初始状态」。之后每改一块数据,旧块被保留、新块被写入,快照就开始「长大」——增长量等于「快照之后被改写的量」,不是「整个卷的大小」。一个天天被写满的数据库卷,快照一天就能涨到几十上百 GB。
  • 删快照不等于立刻释放空间。删除是异步的(cleaner 线程慢慢摘引用),而且只有引用计数真正归零的 extent 才会被回收。df 看到空间没回来,先别慌,等几分钟再看。
  • 共享 extent 让 du 彻底失真。du -sh 会把共享块在每个文件里各算一遍,看快照占用要用 btrfs filesystem du -s(它分 exclusive / set-shared / total 三列)。
  • 快照和源卷必须同一个文件系统。跨盘、跨设备做不了快照,只能 send/receive。
  • 快照存在同一个文件系统里,盘坏了快照跟着一起没。这是快照和备份的分界线,第 3 节第一条就讲这个。

2.2 scrub:把每个字节读一遍,对一遍账

解决什么:提前发现静默损坏(bit rot)、坏扇区、以及「写进去了但读出来不对」的数据。

机制:后台顺序读全盘的数据块和 metadata 块,逐块重算 csum 与 csum tree 比对。对不上就尝试从冗余副本修复;修不了就记账、报错。它不改数据布局、不搬块,只读+校验(修复时才写)。

btrfs scrub start -Bd /mnt/data        # -B 不交互(直接跑完),-d 每 5 秒打印进度
btrfs scrub status -d /mnt/data        # 看进度、已校验字节数、错误数
btrfs scrub cancel /mnt/data
btrfs scrub resume /mnt/data

# scrub 结果 + 设备级错误计数(两个都要看)
btrfs device stats /mnt/data

btrfs scrub status 正常结束长这样:

UUID:             xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Scrub started:    Sun Oct 19 02:00:01 2026
Status:           finished
Duration:         3:12:44
Total to scrub:   1.82TiB
Rate:             165.00MiB/s
Error summary:    csum=0
  Corrected:      0
  Uncorrectable:  0
  Unverified:     0

三个错误字段要分清:Corrected 是「发现了、用另一份修好了」;Uncorrectable 是「发现了、修不了」;Unverified 是「没有 csum 可对」(nodatasum 的文件),属于信息性提示。

代价与坑:

  • I/O 很重。全盘顺序读,机械盘上跑满一整天很正常,会明显拖慢同时段的业务。建议夜间跑,配 -c 3(idle 优先级)或 ionice。
  • 只能发现,修不修得看有没有第二份。单盘 -d single、或者下层只暴露一块设备的场景,Uncorrectable 会实打实变成 EIO。第 5 节会详细推这个结论。
  • btrfs device stats 的计数不会因为 scrub 自动清零,修好之后要手动 btrfs device stats -z,否则报警会一直响。
  • scrub 不是 RAID 校验。它不校验 RAID5/6 的奇偶校验一致性(那需要读整个 stripe),所以 btrfs raid56 的坑它兜不住。

2.3 压缩(compression):省空间是真省,省得值不值另说

解决什么:日志、文本、容器镜像、备份归档这类可压缩数据省空间,顺带减少写放大(写得少,擦除也少)。

机制:写入时按 extent(最长 128K)压缩再落盘,读取时解压;csum 算在压缩后的数据上。支持 zlib、lzo、zstd(现代内核还多了 lz4)。

# 文件系统级默认(推荐 zstd,1–15 级,3 是性价比拐点)
mount -o compress=zstd:3 /dev/sdX /mnt/data

# 只给某个目录/文件开,不用重挂
btrfs property set -ts /mnt/data/logs compression zstd
btrfs property get -ts /mnt/data/logs

# 看压缩效果
compsize /mnt/data          # 需要 btrfs-progs 里的 compsize
btrfs filesystem du -s /mnt/data

两个关键细节:

  • compress= 有 15% 阈值(内核 5.15 起)。压完省不到 15% 就直接存原数据,避免为了 3% 的收益付解压 CPU。所以「开了压缩但 compsize 显示很多没压」是正常行为,不是没生效。
  • compress-force= 强制压,只建议给确定可压的数据用(比如纯文本归档),否则白白增加 CPU 和碎片。

代价与坑:

  • 只影响新写入。历史数据要重新压缩,得 btrfs filesystem defrag -r -czstd 或者 cp --reflink=never 重写一遍——而 defrag 又踩第 3 节的坑,所以老数据要么接受不压,要么在维护窗口里小范围处理。
  • 压缩和 nodatacow 互斥。nodatacow 的数据不压缩(压缩必然要「写新块」,那就是 COW)。
  • 随机小写入会变慢。压缩/解压有 CPU 成本,SSD 上写入快、CPU 弱的话,压缩可能反而是负收益——在低功耗 NAS 的 Atom/ARM 上尤其明显。
  • 别拿压缩率当指标卷。zstd:3 在混合数据上一般 1.3–2 倍,视频/图片/已压缩包基本 1.0 倍,压不动是数据本身的问题。

2.4 去重(dedupe):内核没有「打开开关」这回事

解决什么:多份相同内容只存一份。虚拟机镜像、容器层、重复的备份文件是典型场景。

先把误解掰正:Btrfs 没有在线去重。它只有两样东西:

  1. reflink(内置能力):cp --reflink=always 复制文件时不复制数据,两个文件共享同一批 extent。这是内核原生支持的,但需要显式的 cp 动作。
  2. 离线去重工具:duperemove、bees 这类用户态程序,自己扫盘算哈希,发现两份相同内容后通过 ioctl(FIDEDUPERANGE / BTRFS_IOC_FILE_EXTENT_SAME)请求内核把 extent 合并。内核只负责「执行合并」这个动作,不负责发现重复。

所以「在 UI 上开一个去重开关」这种形态,在 Btrfs 上严格说不存在。要么你跑 duperemove,要么你用 cp --reflink 主动共享,要么就没有。

# 手动 reflink:瞬间完成,不占额外空间
cp --reflink=always big.img big-copy.img

# 离线去重(duperemove):-d 去重、-r 递归、-h 人性化大小、--hashfile 复用哈希库,
# 第一次跑建库最慢,之后增量会快很多
duperemove -drh --hashfile=/var/lib/duperemove.hash /mnt/data

# 先空跑,看能省多少、耗时多久(--dry-run 不会真的合并)
duperemove -drh --dry-run /mnt/data

# bees:常驻后台、增量去重(适合一直有重复内容产生的场景)
bees /mnt/data

代价与坑:

  • 去重是 CPU + I/O 密集型。duperemove 全量跑一遍大卷要几小时到几天,期间读放大明显;bees 常驻会持续吃 CPU。
  • 合并之后,改任何一份都会重新 COW,去重带来的空间节省会随时间被「写时分裂」吃掉,需要定期重跑。
  • 去重 + 快照 + defrag 三者互相打架:defrag 会重写 extent、打断共享,把去重省下的空间还回去(详见 2.7 和第 3 节)。
  • 别对数据库/VM 在线盘去重:合并 extent 时的 I/O 和锁竞争会拖慢业务,而且 VM 镜像内部逻辑上的重复块去重收益有限。
  • 去重不校验数据正确性(它是按哈希比对),更不能替代校验和与备份。

2.5 send / receive:把「文件系统状态」当字节流搬运

解决什么:增量备份、迁移、复制带快照的目录树。比 rsync 强的地方在于:它搬的是 extent 和快照关系,不是文件。

机制:btrfs send 把一个只读子卷序列化成一个流(记录每个文件、每个 extent、每个 xattr、每个 reflink 关系),btrfs receive 在另一端重建。带上 -p 父快照就是增量流——只发「和父快照之间变化的部分」。

# 全量:本地落文件
btrfs send -f /backup/data-full.btrfs /mnt/snapshots/data-2026-10-20

# 增量:以昨天的快照为父
btrfs send -p /mnt/snapshots/data-2026-10-19 \
            /mnt/snapshots/data-2026-10-20 \
  | btrfs receive /mnt/backup

# 走网络 + 压缩,这是最常见的备份形态
btrfs send -p "$SNAP_PREV" "$SNAP_NOW" \
  | zstd -T0 -3 \
  | ssh backup-host "zstd -d | btrfs receive /backup/data"

代价与坑:

  • 必须只读快照,可写子卷不能 send。
  • 父快照必须两端都在,否则增量流接收端会报「parent not found」,只能退回全量。
  • 快照之间不能有「被改过的历史」。如果你拿一个已经写过的可写子卷当 clone source,流里可能出现不一致的数据;-c(clone source)就是用来声明「这些旧快照也算共享源」的,用错会静默出错。
  • 流格式不保证跨版本兼容。发送端和接收端的 btrfs-progs 版本别差太远,老版本收新版本的流会直接失败。
  • 小文件多的时候很慢。每个文件都有元数据开销,几十万小文件的卷,send 的吞吐可能只有 rsync 的一半——但它换来了精确的快照语义,值不值看你更怕丢还是更怕慢。

2.6 qgroup:按子卷记账,但账本会自己走偏

解决什么:给子卷/快照设容量上限,或者回答「这个共享目录到底占了多少 exclusive 空间」。

机制:qgroup 以子卷编号为单位记账,记录 referenced(引用的总字节)和 exclusive(只被这个 qgroup 引用的字节),支持层级(父 qgroup 汇总子 qgroup)。限额基于 referenced(默认)或 rfer。

btrfs quota enable /mnt/data
btrfs quota status /mnt/data

# 给 256 号子卷限 500G
btrfs qgroup limit 500G 0/256 /mnt/data
btrfs qgroup show -reF /mnt/data      # r=referenced e=exclusive F=上限(max rfer/excl)

# 账本对不上时重建(可能要跑很久)
btrfs quota rescan -w /mnt/data

代价与坑:

  • quota 一开就有性能开销,写路径要多记账;快照多、子卷多的系统上,开销和内存占用都不小。
  • 账本会漂移。内核和用户态对「引用计数何时变化」的理解一旦有分歧,qgroup 数字就会和 btrfs filesystem usage 对不上,症状是「明明没超限却报 ENOSPC」或反之。修法是 quota rescan,但 rescan 是全盘遍历,几百 GB 起步、几 TB 要跑几小时到一天。
  • 删子卷前要先删 qgroup,否则残留 qgroup 会一直占着编号和统计。
  • qgroup 的 exclusive 和 btrfs filesystem du 的 exclusive 不是一回事,两者口径不同,别拿来做交叉验证然后得出「数据错了」的结论。
  • 结论:没有明确的多租户/配额需求,就别开 quota。开了就要接受「定期 rescan」这件事。

2.7 balance / defrag:两个被误解最深的维护命令

这两个命令常被当成「优化」,其实一个是搬块,一个是重写文件,都不是无痛的。

balance:重新分配 chunk

Btrfs 的物理空间按 chunk 分配(一个 data chunk 通常 1GiB)。balance 的工作是把现有 chunk 里的数据搬出来、按新布局重写回去,所以它同时能干三件事:回收碎 chunk、转换 RAID profile、把数据从某块盘挪走。

# 只处理"使用率低于 10%"的 data chunk —— 日常最常用、最安全的一档
btrfs balance start -dusage=10 /mnt/data

# 转换 profile:单盘 → RAID1(加盘之后)
btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt/data

# 把某块盘从文件系统里摘掉
btrfs balance start -ddevid=2 -mdevid=2 /mnt/data

# 状态、暂停、继续、取消
btrfs balance status /mnt/data
btrfs balance pause|resume|cancel /mnt/data

坑:

  • 不带过滤器的 btrfs balance start /mnt 是全量重写,几 TB 的数据能跑一整天,期间 I/O 打满、空间可能先涨后落(搬块需要临时空间)。
  • 空间快满时跑全量 balance 有把自己跑成 ENOSPC 的风险,务必先看 btrfs filesystem usage 的 Free (estimated)。
  • balance 可暂停可恢复,但别在 pause 状态下关机/断电太久,状态会丢,重来。
  • -dusage=10 这类过滤条件,只处理「使用率低于阈值」的 chunk,是空间碎片化的对症药,不要每周无脑跑,按需(btrfs filesystem usage 显示 Unallocated 很小而 Free 很大时)再跑。

defrag:重写文件,而不是「整理磁盘」

btrfs filesystem defrag 把文件现有的 extent 合并成更大的 extent,减少读放大。它对机械盘上的大文件顺序读确实有收益。

btrfs filesystem defrag -r -f /mnt/data/some-dir      # -r 递归
btrfs filesystem defrag -czstd -r /mnt/data/logs      # 顺带重新压缩

坑(这是全文最该记住的一条之一):

  • defrag 会打断 reflink 共享。快照和被去重共享的 extent,一旦被 defrag 重写,就变成「各存一份」——快照体积瞬间暴涨,去重收益一夜归零。
  • 对开着快照的卷跑 defrag -r 基本是灾难;对 VM 镜像/数据库在线跑 defrag 会带来明显 I/O 抖动。
  • autodefrag 挂载选项在 SSD 上常被建议关掉(写放大),在 VM 镜像场景更是公认的负优化。
  • 现代 SSD 上 defrag 的收益本来就很小(没有寻道成本),别为了「整齐」去 defrag。

2.8 nodatacow:给数据库开后门,代价是放弃安全网

解决什么:数据库、虚拟机镜像这类「原地随机小写」的负载,在 COW 下会严重碎片化 + 写放大。nodatacow 让这些文件回到传统文件系统的原地覆盖语义。

机制:对该 inode 关闭 COW、关闭校验和(隐含 nodatasum)、关闭压缩。写入原地覆盖,不再分配新 extent。

# 对目录设(只对之后新建的文件生效,已有文件不追溯)
chattr +C /mnt/data/vm
lsattr -d /mnt/data/vm

# 对已有文件:先设属性再"重写"一遍内容才生效
chattr +C /mnt/data/vm/disk.img
cp --reflink=never /mnt/data/vm/disk.img /tmp/x && mv /tmp/x /mnt/data/vm/disk.img

坑:

  • 设了 +C 的目录只影响之后新建的文件,老文件要重写才生效——很多人设完发现没效果就是这个原因。
  • nodatacow 的文件不进快照的 COW 语义:快照里它和源文件是同一份数据,源文件原地改了,快照里看到的也变了。等于这块数据没有时间点保护。
  • 没有校验和,静默损坏对它完全失明,scrub 遇到它只会记 Unverified。
  • nodatacow 是挂载选项级别也能设的(-o nodatacow 让整个卷新文件都不 COW),别在系统盘上这么干。
  • 正确姿势是「小范围、按目录」用,而不是全局关。

2.9 原生 RAID:Btrfs 自己管副本,但它不是 RAID 卡

解决什么:不依赖 md/LVM/硬件 RAID,直接在多块盘上做冗余,并且校验和与冗余在同一层——这是它比「Btrfs 骑在 md RAID 上」强的地方。

机制:chunk 级分配。data 和 metadata 的 profile 可以分开设:

profile最少设备冗余能力说明
single1无默认 data
dup1同盘两份默认 metadata(单盘时)
raid02无只要速度
raid121 份冗余最常用
raid1c332 份冗余metadata 常用,比 raid1 更稳
raid1c443 份冗余极端保守
raid1041 份冗余 + 条带性能与冗余平衡
raid5 / raid63 / 41 / 2 份校验生产环境不要用
mkfs.btrfs -d raid1 -m raid1c3 /dev/sdb /dev/sdc /dev/sdd
btrfs filesystem df /mnt/data          # 看各 profile 实际占用

# 加盘后从单盘转 RAID1
btrfs device add /dev/sdd /mnt/data
btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt/data

# 坏盘替换(replace 会自动重建)
btrfs replace start /dev/sdb /dev/sdd /mnt/data
btrfs replace status /mnt/data

坑:

  • raid5/6 至今不建议生产使用。写洞(write hole)、掉盘后 scrub/replace 的边界问题讨论了十几年,虽然内核一直在修(近年有针对 raid56 的重构),但社区和发行版的共识仍然是「能不用就不用」。要空间效率就用 raid10 或 raid1c3,要安全就别省那块盘。
  • raid1 是 chunk 级,不是盘级镜像。数据按 chunk 切分后,每块 chunk 写两份、落在不同盘上,所以任何一块盘坏了数据都还在;但两块盘容量不必相同,Btrfs 按可用空间分配,容量不一致时只能用到小盘那么多(多出来的空间用得上,但要配合 single,那就不再是全冗余了)。
  • 没有写日志(journal),掉电时最后一个事务直接丢,不会像 ext4 那样重放日志。这是设计取舍,不是 bug。
  • 元数据 profile 别偷懒。-m raid1 在双盘上够用;三盘以上建议 raid1c3,因为 metadata 坏了是整个文件系统的事。

3. 六个最容易踩的坑

前面正文里已经零散提过,这里集中列一遍。每条都是「听起来反直觉、但真实发生过」的类型。

坑 1:把快照当备份

快照和源数据在同一个文件系统、同一块盘上。盘挂了、文件系统崩了、误删了整个子卷(包括快照目录),快照跟着一起没。快照解决的是「误操作/升级回滚」,备份解决的是「介质/设备/灾难」。两者不能互相替代,正确组合是「本地快照 + 异地 send/receive 或 rsync」。

坑 2:对有快照的卷跑 defrag -r

defrag 重写 extent → 打断 reflink 共享 → 快照从「几乎不占空间」变成「每个被 defrag 的文件都存一份完整副本」。在开着一堆快照的卷上跑一次全量 defrag,快照体积能从几 GB 涨到几百 GB,把盘撑满。开快照的卷,defrag 只在明确的、不带快照的子卷上做。

坑 3:balance 不带过滤器,或者空间快满时跑

btrfs balance start /mnt 是全量重写,几 TB 跑一天;空间不足时它需要临时空间搬块,可能直接把自己跑成 ENOSPC。日常只用 -dusage=10 这类带阈值的过滤;全量只在换盘、转 profile 时做,并且提前确认 Free (estimated) 够。

坑 4:开了 qgroup 却不接受 rescan

qgroup 的账本会漂移,漂了就得 quota rescan,而 rescan 是大规模全盘扫描。更麻烦的是:如果你在跑 scrub/balance/大量快照的同时 rescan,数字可能又对不上。没有硬性配额需求就别开 quota;开了就把「定期 rescan」写进维护计划。

坑 5:把 nodatacow 当成「性能优化开关」全局打开

全局 -o nodatacow 会让所有新文件失去校验和、失去 COW 快照语义、失去压缩。你换来的是「数据库少一点碎片」,代价是「所有数据对静默损坏失明」。只对数据库/VM 目录按目录 chattr +C,别碰系统盘。

坑 6:以为「有 RAID1」就等于「能修坏块」

这是本文第 5 节的核心,也是你这台机器最真实的坑:Btrfs 的修复能力,取决于它自己看得见几份副本。如果它的下层是 md RAID1、LVM、bcache 这类「只暴露一块设备」的栈,Btrfs 眼里就是 single——它算得出 csum 不符,但没有第二份可修,只能 EIO。RAID 保的是「盘坏」,校验和保的是「数据坏」,只有两者在同一层,才能「坏盘自动修」;分开在两层,就只剩「发现并报警」。


4. 成本收益速查表

11 项,结论分三档:必开 / 特定场景 / 别用。「代价」一栏写的是最容易被低估的那一项。

#特性结论收益主要代价 / 风险
1只读快照 + 定期 send必开误删/升级回滚、增量备份的基础快照会随写入增长;不是备份,必须异地一份
2scrub(定期)必开发现静默损坏、坏扇区,唯一能提前发现 bit rot 的手段I/O 重、耗时;没有冗余时只能发现不能修
3compress=zstd:3必开(默认建议开)日志/文本/容器镜像省 30–60% 空间老数据不追溯;弱 CPU 上小写入变慢
4metadata -m raid1c3必开(≥3 盘)metadata 双冗余,避免整盘崩多占空间,写放大
5data -d raid1/raid10特定场景盘坏不丢数据容量利用率 50%;跨不过下层单设备栈
6nodatacow(按目录)特定场景数据库/VM 减少碎片与写放大失去校验和、失去快照语义
7send/receive 增量备份特定场景精确快照语义、传输量小需要只读快照 + 父快照;小文件慢;版本要匹配
8duperemove / bees特定场景VM 镜像、容器层可省 20–50%CPU/I/O 密集;defrag/写入会逐渐还回去;无在线开关
9qgroup 限额特定场景多租户/共享目录硬配额账本漂移 + rescan 成本;没有需求就别开
10全量 balance / defrag别用(改为带过滤器的按需版)回收碎 chunk、重写碎片文件全量 balance 跑一天;defrag 打断 reflink 撑爆快照
11raid5 / raid6别用空间效率写洞与重建边界问题,社区长期不推荐生产

一句话记法:快照、scrub、压缩、metadata 冗余是「地基」,开了就长期受益;去重、qgroup、send/receive 是「按需」,收益明确再上;全量 balance/defrag 和 raid5/6 是「负资产」,除非你有非常具体的理由。


5. 回到你的机器:UGOS 实测矩阵 + 为什么 CLI 改不动 + csum 只能发现不能修复

前面都是通识。这一节只回答一个问题:一台 UGOS NAS 上,Btrfs 的这些高级特性,你到底能拿到多少。

5.1 先把栈画出来

你的存储栈不是「Btrfs on 两块盘」,而是:

应用 / SMB / NFS
      │
   Btrfs  ← 它只看得见一块设备:bcache0
      │
   bcache(SSD 当缓存,可能 writeback)
      │
   LVM(逻辑卷)
      │
   md RAID1(两块盘互为镜像)
      │
   物理硬盘 ×2

这个顺序在 lsblk、findmnt、btrfs filesystem show 三处能看到:

lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,PKNAME
findmnt -no SOURCE,FSTYPE,OPTIONS /volume1
btrfs filesystem show /volume1
# 输出大概是:Label: none  uuid: ...  Total devices 1  FS bytes used ... /dev/bcache0

关键就在最后一行的那句 Total devices 1。对 Btrfs 来说,这是一个单设备文件系统,data 是 single,metadata 是 dup(同盘两份)。它完全不知道底下 md RAID1 有两个副本,也不知道 bcache 在中间做了缓存。

5.2 UGOS 实测矩阵

下表是「厂商做了什么 / 你能碰什么 / 说明」。每项都给了自查命令——先自查,再下结论,因为不同 UGOS 版本的实现差别不小,而且有的版本已经换到 LVM+XFS 了,命令输出为空不代表命令写错了。

特性UGOS 是否在内部使用用户侧能否操作自查命令说明
子卷(系统/数据分离)是否(只读可见)btrfs subvolume list -p /通常能看到 @、@home 一类的布局
只读快照是(「快照」功能)是(UI 计划任务)btrfs subvolume list / | grep -i snap这是 UGOS 的卖点功能,能用
快照还原是是(UI)—只能整卷/整共享还原,粒度不到文件
scrub是(前台可触发/后台计划)部分(UI 有「扫描」入口)btrfs scrub status /volume1有的版本只给「检查」不给「取消/排期」
压缩视版本否(无 UI 开关)btrfs property get -ts /、mount | grep compress多数版本默认没开 compress=,数据不压
去重否否无;只能自己装 duperemove这就是「内核没有在线去重」的直接体现
send/receive基本不用否which btrfs、btrfs send --help备份走 rsync/SMB 同步,不用流
qgroup / quota不用否btrfs quota status /volume1配额是应用层做的,不是 qgroup
balance不开放否(且不建议自己跑)btrfs balance status /volume1空间回收由厂商后台处理
defrag不开放否—同上
nodatacow不用于数据区否lsattr -d /volume1/xxx—
原生 RAID(btrfs profile)否否btrfs filesystem show 看 devices 数冗余在 md 层,Btrfs 只见单设备
校验和是(默认开)只读(不可关)btrfs inspect-internal dump-super -f /dev/bcache0 | grep -A5 incompat_flags有 csum 能力,但见 5.4

一个反直觉的结论:这张表里「能用」最多的恰恰是快照和 scrub——因为这两个是「用户可理解、厂商好封装、出事了能解释」的功能;而 balance/defrag/qgroup 恰好是「事故率远高于收益」的,厂商不开放是有道理的(见 5.5)。

5.3 为什么 CLI「改不动」

很多人第一反应是「权限问题」,sudo 一下就好了。实际情况通常是下面几层,从下往上任何一层都能把你挡住:

  1. 内核支持 ≠ 用户态工具在。btrfs 命令可能压根没装(which btrfs 空),或者装了但版本老到不认识新特性。
  2. 挂载参数由厂商的挂载器生成,不是 /etc/fstab。你手改 fstab 加 compress=zstd,重启后会被 UGOS 的存储管理服务覆盖;更糟的情况是,它检测到「非预期配置」后拒绝挂载或提示「存储异常」。
  3. 只读根 + 每次升级回滚。系统分区是只读镜像,你 apt install btrfs-progs 装进去的东西,下一次系统更新就被覆盖回去了。
  4. 策略层的拦截。有些版本会检测到非官方 CLI 操作后直接禁用对应功能(或者干脆把卷标成异常)。
  5. 真正的限制是「没人对后果负责」。即使你硬跑 btrfs balance start 成功了,跑挂了也没有售后——这层是运维层的,不是技术层的。

所以正确的姿势不是「想办法绕过」,而是先分清哪些是「厂家的合理取舍」,哪些是「值得接管的」。

5.4 核心推论:在这台机器上,csum 只能发现,不能修复

这是全文最需要你记住的一段,推导链条很短:

  1. Btrfs 只看得见 /dev/bcache0 一块设备,data profile 是 single。
  2. 修复一个 csum 不符的块,需要第二份「Btrfs 认识的」好副本。
  3. 当前栈里,第二份副本存在于 md RAID1 层——但 Btrfs 看不见它。
  4. 所以:scrub 能准确报出「这块数据不对」(csum= 计数 +1),然后返回 EIO;它没有东西可以拿来覆盖坏块。

这就是「支持 RAID1 也可能修不了数据」的完整解释。把各层能力摊开看:

故障类型md RAID1 层bcache 层Btrfs 层最终结果
盘整个坏掉✅ 切到另一块盘缓存可能未命中感知不到业务不中断
读某个扇区 I/O 错误✅ 换另一份读—拿到好数据不丢数据
静默位翻转(数据层)❌ 无校验,发现不了❌ 不校验✅ csum 发现报 EIO,修不了
静默位翻转(metadata 层)❌❌✅ csum 发现 + dup 副本修复通常能自愈
缓存盘(SSD)挂掉—⚠️ writeback 模式下脏数据可能丢可能连挂载都失败最危险的一种

最后一行要单独强调:如果 bcache 是 writeback 模式,SSD 缓存里可能存着「已经向 Btrfs 确认写入、但还没落到机械盘」的脏数据。缓存盘一坏,这部分数据直接消失,Btrfs 的 metadata 可能对不上,严重时整个文件系统挂不起来。 bcache 本身不提供任何冗余。能改的话,把缓存模式改成 writethrough(或直接不要缓存),用一点写性能换掉这个风险,是值得的。

5.5 「厂商偷懒」还是「合理取舍」

把不开放的功能分成两类看,结论会清晰很多:

属于合理取舍的(别接管):

  • balance:全量重写,几 TB 跑一天,中途出问题就是「数据在搬家时出事」。厂商在后台按需跑(回收碎 chunk)比自己开放给用户安全得多。
  • defrag:一旦用户对开了快照的卷跑 defrag -r,快照体积暴涨、盘被撑满,售后根本解释不清。不开放是对的。
  • qgroup:账本漂移 + rescan 成本,用户看到「明明没满却写不进去」只会觉得是 NAS 坏了。
  • 原生 btrfs RAID:在成品 NAS 上,冗余方案是产品设计的一部分(要支持混插、要支持扩容、要能售后换盘),用 md/LVM 是更可控的选择。

属于「值得你自己接管」的:

  • scrub 的排期与结果核验:厂商可能只给你一个「检查」按钮,不给「定期跑 + 出错告警」。而在这台机器上,scrub 只能发现不能修,那「定期跑 + 看 device stats + 有错就报警」就更重要——这是你唯一能提前知道盘在烂的手段。
  • 离线校验与备份验证:定期从备份里抽文件校验(对比 sha256),比任何「备份成功」的日志都可信。
  • bcache 模式与缓存盘健康:这是最值得盯的一层,见上面。
  • 空间与错误监控:btrfs filesystem usage、btrfs device stats、dmesg 里的 Btrfs 报错,这三样必须有人看。

一句话总结这台机器:它能给你「发现问题的能力」,但给不了「自动修复的能力」;所以运维的重点从「配好就完事」变成了「定期跑、认真看、有备份」。


6. 自建 Btrfs 的实践清单

如果你有权限从零搭一台(或者在一台能自由折腾的机器上),下面这份清单可以直接抄。

6.1 挂载参数

# 单盘 / 桌面 / 一般 NAS:够用且保守
noatime,compress=zstd:3,space_cache=v2,discard=async,ssd

# 多盘 RAID:metadata 用 raid1c3,data 用 raid1 或 raid10
noatime,compress=zstd:3,space_cache=v2,commit=120

# 明确不要加的:
#   autodefrag      —— SSD 写放大,VM 镜像负优化
#   nodatacow       —— 全局关掉校验和,等于自废武功
#   compress-force  —— 除非数据确定可压
#   discard(同步) —— 换 discard=async,避免删除时卡顿

/etc/fstab 里记得配 x-systemd.device-timeout= 和 nofail(数据盘,别让挂载失败卡住开机),系统盘则相反,要 passno=0 且别加 nofail(你希望它挂不上就报错,而不是静默进救援模式)。

6.2 排期

任务频率命令 / 要点
只读快照每天(或每小时,视写入量)btrfs subvolume snapshot -r,保留 7/4/12 三档
快照清理每天删旧快照后等 cleaner 回收,别立刻判 ENOSPC
scrub每月(机械盘)/ 每季度(SSD)btrfs scrub start -Bd,夜间 + -c 3 降优先级
device stats 巡检每周有非零计数就查,修好后 -z 清零
增量 send每天以昨天的只读快照为 -p 父
balance半年或按需只用 -dusage=10,别全量
defrag默认不做只有确定「无快照 + 大文件顺序读慢」的目录才做
quota rescan仅在开了 qgroup 且数字异常时提前预留时间窗口

6.3 备份脚本骨架

核心是「只读快照 → 增量 send → 远端 receive → 两端都留保留策略」。脚本必须能容忍「父快照不存在」并自动退回全量,否则某天父快照被清理,备份就静默停摆。

#!/usr/bin/env bash
# btrfs-backup.sh —— 本地只读快照 + 增量 send 到远端
set -euo pipefail

SRC=/mnt/data
SNAPDIR=$SRC/.snapshots
REMOTE=backup@nas2:/backup/data
KEEP=7
STAMP=$(date +%F_%H%M)

mkdir -p "$SNAPDIR"

# 1) 只读快照
btrfs subvolume snapshot -r "$SRC" "$SNAPDIR/data-$STAMP"

# 2) 找最近的一个旧快照当父(用于增量)
PREV=$(ls -1d "$SNAPDIR"/data-* 2>/dev/null | grep -v "data-$STAMP$" | tail -n 1 || true)

# 3) 发送:有父发增量,没父发全量
if [ -n "$PREV" ]; then
  echo "incremental from $(basename "$PREV")"
  btrfs send -p "$PREV" "$SNAPDIR/data-$STAMP" \
    | zstd -T0 -3 \
    | ssh "${REMOTE%%:*}" "zstd -d | btrfs receive ${REMOTE#*:}"
else
  echo "full send (no parent)"
  btrfs send "$SNAPDIR/data-$STAMP" \
    | zstd -T0 -3 \
    | ssh "${REMOTE%%:*}" "zstd -d | btrfs receive ${REMOTE#*:}"
fi

# 4) 本地快照轮转
ls -1d "$SNAPDIR"/data-* | sort | head -n -"$KEEP" | while read -r old; do
  btrfs subvolume delete "$old"
done

# 5) 远端轮转(保留同样的数量)
ssh "${REMOTE%%:*}" "ls -1d ${REMOTE#*:}/* | sort | head -n -$KEEP | xargs -r -n1 btrfs subvolume delete"

echo "done: $STAMP"

几个必须自己加上的检查(脚本骨架里为了让流程清楚省略了):

  • 发送前 btrfs subvolume show "$SNAP" 确认是 readonly。
  • 发送后回读远端 btrfs subvolume list,确认新快照真的到了(不要只看 ssh 退出码)。
  • 空间检查:本地和远端都留够余量,btrfs filesystem usage 里 Free (estimated) 低于 10% 就别发全量。
  • 保留策略里至少留一份「不可被脚本删除的月快照」,防脚本 bug 把历史清空。

6.4 监控项

指标命令 / 位置告警条件
csum/读写错误计数btrfs device stats /mnt任何一项非零
scrub 结果btrfs scrub status -d /mntError summary 的 csum= 非零,或 Status 很久没变成 finished
空间与分配btrfs filesystem usage -T /mntFree (estimated) < 10%,或 Unallocated 极小但 Free 很大(碎片化)
内核报错dmesg -T | grep -i -E 'btrfs|I/O error|checksum'出现 BTRFS error、csum failed、parent transid verify failed
快照增长btrfs filesystem du -s /mnt/.snapshots/*单个快照增速异常(通常是 defrag 或大文件重写)
qgroup 漂移btrfs qgroup show -reF 与 usage 对照quantified 数字与 usage 差距持续扩大
bcache 缓存状态/sys/fs/bcache/*/dirty_data、cache_modedirty_data 长期高位、或模式是 writeback 且你无法接受风险
备份新鲜度远端最新快照时间超过 2 个周期没更新

监控的哲学就一句:Btrfs 不会主动告诉你它在烂,你得自己定期问它。


7. 术语速查

术语一句话解释
COW(Copy-on-Write)改数据写新块、旧块保留,指针原子切换;快照与 send 的基础
extent一段连续物理数据块,Btrfs 的分配与引用单位
chunk更大的物理分配单位(data chunk 常见 1GiB),RAID profile 作用在这一层
csum(校验和)每块数据/元数据的完整性指纹,独立存在 csum tree;默认 CRC32c
subvolume(子卷)文件系统内的独立命名空间,可单独挂载/快照/配额/send
snapshot(快照)新的子卷,共享源卷的 extent;-r 为只读
reflink两个文件共享 extent 的能力;cp --reflink=always 触发
scrub全盘读 + 校验,发现损坏并尝试用冗余副本修复
balance搬 chunk,回收碎空间、转换 profile、腾挪设备
defrag重写文件使其 extent 连续;会打断 reflink 共享
qgroup按子卷记账的配额组,referenced / exclusive 两类数字
nodatacow关闭该文件的 COW 与校验和(隐含 nodatasum),原地覆盖写
profileRAID 布局(single/dup/raid0/raid1/raid1c3/raid10/raid5/raid6)
write hole(写洞)掉电时 RAID5/6 的奇偶校验与数据可能不一致,重建时放大错误
bcache用 SSD 给 HDD 做块级缓存;writeback 模式下缓存盘故障可能丢脏数据
EIO内核返回的 I/O 错误;csum 不符又无副本可修时的最终表现
UGOS绿联 NAS 的操作系统;Btrfs 被封装在存储管理层之下

小结

Btrfs 的高级特性,本质上是三个机制(COW、校验和、子卷)在四个方向上的展开:

  • 时间维度:快照、send/receive —— 让你回到过去、把过去搬走;
  • 完整性维度:scrub、自愈 —— 让你知道数据坏了,并在有冗余时修好;
  • 空间维度:压缩、去重、balance、defrag —— 让你在容量和性能之间换筹码;
  • 可靠性与管理维度:原生 RAID、qgroup、nodatacow —— 让你定义冗余、配额和例外。

而一台成品 NAS 上的真实情况是:这四类能力里,用户能拿到的往往只有第一类和半类第二类。 这不是厂商偷懒,而是「balance/defrag/qgroup 的事故率远高于收益」这个判断在起作用。真正值得你接管的,是那些只能发现、不能自动修复的部分——定期 scrub、盯 device stats、验备份、看 bcache 缓存状态。

最后回到本文开头那句话:「支持 Btrfs」只说明内核里有这个文件系统;「能用 Btrfs」要求你清楚它在你的栈里看得见几份副本、哪些操作有人替你兜底、哪些只能靠自己定期跑。 你这台机器的答案已经很清楚——它能帮你发现数据在烂,但修不了;所以备份和巡检,一样都不能省。

如果你觉得这篇文章不错,有用的话可以点击下面的“赞赏”支持作者,你的支持是我更新的动力(饿饿吃饭)。

  • reward_image1
  • reward_image2
欢迎来到XiaoZou123,这里是一个电脑极客、数码爱好者网站。我平时喜欢关注数码新闻,研究计算机技术。如果你看我头像觉得我是二次元,那我其实还算不上!
最后更新于 2026-10-21