同一套 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」这句话至少漏掉了三个问题:
- 支持到哪一层? 是内核编进了 Btrfs 模块,还是连
btrfs-progs都齐?内核支持只决定「能挂载」,用户态工具才决定「能运维」。 - 谁能操作? 是厂商替你跑,还是把命令交给你?前者意味着你买到的是「结果」,不是「能力」。
- 底下垫了什么? 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 没有在线去重。它只有两样东西:
- reflink(内置能力):
cp --reflink=always复制文件时不复制数据,两个文件共享同一批 extent。这是内核原生支持的,但需要显式的cp动作。 - 离线去重工具:
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 | 最少设备 | 冗余能力 | 说明 |
|---|---|---|---|
single | 1 | 无 | 默认 data |
dup | 1 | 同盘两份 | 默认 metadata(单盘时) |
raid0 | 2 | 无 | 只要速度 |
raid1 | 2 | 1 份冗余 | 最常用 |
raid1c3 | 3 | 2 份冗余 | metadata 常用,比 raid1 更稳 |
raid1c4 | 4 | 3 份冗余 | 极端保守 |
raid10 | 4 | 1 份冗余 + 条带 | 性能与冗余平衡 |
raid5 / raid6 | 3 / 4 | 1 / 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 | 必开 | 误删/升级回滚、增量备份的基础 | 快照会随写入增长;不是备份,必须异地一份 |
| 2 | scrub(定期) | 必开 | 发现静默损坏、坏扇区,唯一能提前发现 bit rot 的手段 | I/O 重、耗时;没有冗余时只能发现不能修 |
| 3 | compress=zstd:3 | 必开(默认建议开) | 日志/文本/容器镜像省 30–60% 空间 | 老数据不追溯;弱 CPU 上小写入变慢 |
| 4 | metadata -m raid1c3 | 必开(≥3 盘) | metadata 双冗余,避免整盘崩 | 多占空间,写放大 |
| 5 | data -d raid1/raid10 | 特定场景 | 盘坏不丢数据 | 容量利用率 50%;跨不过下层单设备栈 |
| 6 | nodatacow(按目录) | 特定场景 | 数据库/VM 减少碎片与写放大 | 失去校验和、失去快照语义 |
| 7 | send/receive 增量备份 | 特定场景 | 精确快照语义、传输量小 | 需要只读快照 + 父快照;小文件慢;版本要匹配 |
| 8 | duperemove / bees | 特定场景 | VM 镜像、容器层可省 20–50% | CPU/I/O 密集;defrag/写入会逐渐还回去;无在线开关 |
| 9 | qgroup 限额 | 特定场景 | 多租户/共享目录硬配额 | 账本漂移 + rescan 成本;没有需求就别开 |
| 10 | 全量 balance / defrag | 别用(改为带过滤器的按需版) | 回收碎 chunk、重写碎片文件 | 全量 balance 跑一天;defrag 打断 reflink 撑爆快照 |
| 11 | raid5 / 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 一下就好了。实际情况通常是下面几层,从下往上任何一层都能把你挡住:
- 内核支持 ≠ 用户态工具在。
btrfs命令可能压根没装(which btrfs空),或者装了但版本老到不认识新特性。 - 挂载参数由厂商的挂载器生成,不是
/etc/fstab。你手改fstab加compress=zstd,重启后会被 UGOS 的存储管理服务覆盖;更糟的情况是,它检测到「非预期配置」后拒绝挂载或提示「存储异常」。 - 只读根 + 每次升级回滚。系统分区是只读镜像,你
apt install btrfs-progs装进去的东西,下一次系统更新就被覆盖回去了。 - 策略层的拦截。有些版本会检测到非官方 CLI 操作后直接禁用对应功能(或者干脆把卷标成异常)。
- 真正的限制是「没人对后果负责」。即使你硬跑
btrfs balance start成功了,跑挂了也没有售后——这层是运维层的,不是技术层的。
所以正确的姿势不是「想办法绕过」,而是先分清哪些是「厂家的合理取舍」,哪些是「值得接管的」。
5.4 核心推论:在这台机器上,csum 只能发现,不能修复
这是全文最需要你记住的一段,推导链条很短:
- Btrfs 只看得见
/dev/bcache0一块设备,data profile 是single。 - 修复一个 csum 不符的块,需要第二份「Btrfs 认识的」好副本。
- 当前栈里,第二份副本存在于 md RAID1 层——但 Btrfs 看不见它。
- 所以: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 /mnt | Error summary 的 csum= 非零,或 Status 很久没变成 finished |
| 空间与分配 | btrfs filesystem usage -T /mnt | Free (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_mode | dirty_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),原地覆盖写 |
| profile | RAID 布局(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」要求你清楚它在你的栈里看得见几份副本、哪些操作有人替你兜底、哪些只能靠自己定期跑。 你这台机器的答案已经很清楚——它能帮你发现数据在烂,但修不了;所以备份和巡检,一样都不能省。
如果你觉得这篇文章不错,有用的话可以点击下面的“赞赏”支持作者,你的支持是我更新的动力(饿饿吃饭)。


Comments NOTHING