btrfs 深入 — B-tree 是万能的 / Btrfs in Depth and Its B-Tree Architecture
📅 创建时间:2026-06-17 🏷️ 标签:#btrfs #COW #B-tree #subvolume #snapshot #压缩 📚 前置知识:[[04-filesystem-concepts]], [[05-filesystem-formats]], [[12-ext4-deep-dive]]
📋 本章目标
- 理解 btrfs 的核心理念——B-tree 管理一切
- 深入 COW 的底层实现——修改一棵叶子引发整条路径复制
- 掌握 subvolume——不需要预先规划大小的"分区"
- 掌握 snapshot——基于 COW 的瞬间快照
- 理解透明压缩——zstd 在 btrfs 中的实际效果
- 了解内置 RAID 和 Send/Receive
- 认识 btrfs 的已知缺陷和避坑指南
第1部分:B-tree 是一切 — btrfs 的核心哲学
1.1 btrfs 和 ext4 的根本区别
┌─────────────────────────────────────────────────────────────┐
│ ext4 的数据结构 btrfs 的数据结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ext4: 多种固定位置的结构 btrfs: 一切皆 B-tree │
│ │
│ Superblock → 固定位置 Superblock → 存 B-tree 根地址│
│ GDT → Superblock 之后 B-tree → 存 inode 定位 │
│ Bitmap → 每个 Group 里 B-tree → 存 extent 映射 │
│ Inode Table → 每个 Group 里 B-tree → 存空闲空间 │
│ Inode → Inode Table 里 B-tree → 存目录项 │
│ Extent Tree → Inode 里 ... │
│ │
│ ext4 的缺点: btrfs 的优势: │
│ ┌─────────────────────────────────────┐ ┌─────────────────────┐ │
│ │ 每种数据结构位置固定 │ │ 动态分配, 按需增长 │ │
│ │ inode 数量在格式化时固定 │ │ inode 动态分配 │ │
│ │ 不能缩容 │ │ 可以缩容 │ │
│ └─────────────────────────────────────┘ └─────────────────────┘ │
│ │
│ B-tree 的一个节点: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ header: 层级, 条目数, 类型 │ │
│ │ key[0]: (objectid, type, offset) → 数据 │ │
│ │ key[1]: (objectid, type, offset) → 数据 │ │
│ │ ... │ │
│ │ │ │
│ │ 所有东西都是 key-value → 统一的数据结构 │ │
│ │ 代码复用, 一致的操作 → insert/search/delete 都一样 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1.2 btrfs 的 B-tree 森林
┌─────────────────────────────────────────────────────────────┐
│ btrfs 的主要 B-tree │
├─────────────────────────────────────────────────────────────┤
│ │
│ 一棵 btrfs 文件系统包含多棵 B-tree: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ROOT_TREE 存所有其他 B-tree 的根地址 │ │
│ │ │ │ │
│ │ ├── FS_TREE (subvolume 0) │ │
│ │ │ ├── inode 项 │ │
│ │ │ ├── extent 数据项 │ │
│ │ │ └── 目录项 │ │
│ │ │ │ │
│ │ ├── EXTENT_TREE 空闲空间 + 已分配 extent │ │
│ │ ├── CHUNK_TREE 逻辑地址 → 物理设备映射 │ │
│ │ ├── DEV_TREE 设备信息 │ │
│ │ ├── CSUM_TREE 数据块校验和 │ │
│ │ └── DATA_RELOC 在线数据迁移用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键: FS_TREE 每个 subvolume 有一棵 │
│ 所以你创建的 @ 和 @home 各有自己独立的 FS_TREE │
│ 它们互不干扰,但共享底层 EXTENT_TREE │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
第2部分:COW 的底层实现 — 复制整条路径
2.1 修改一个块的代价
┌─────────────────────────────────────────────────────────────┐
│ COW 修改一个数据块的实际流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 你要修改文件的一个数据块 (物理块 P): │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 修改前: │ │
│ │ ROOT_TREE │ │
│ │ └→ FS_TREE (subvolume @) │ │
│ │ └→ inode item (inode 42) │ │
│ │ └→ extent item (逻辑L→物理P) │ │
│ │ 数据在 P │ │
│ │ │ │
│ │ 修改流程: │ │
│ │ 1. 分配新数据块 P' → 把修改后的数据写进去 │ │
│ │ │ │
│ │ 2. 更新 extent item: "逻辑L→物理P" → "逻辑L→物理P'"│ │
│ │ 但 extent item 本身在一个 B-tree 叶子节点里 │ │
│ │ 修改一个叶子 → COW: 复制这条叶子 (在磁盘上) │ │
│ │ │ │
│ │ 3. 叶子节点被复制 → 它的父节点指针要更新 │ │
│ │ → 复制父节点 → 再往上... │ │
│ │ │ │
│ │ 4. 一直复制到树根 → 根地址变了 → 更新 ROOT_TREE │ │
│ │ → 复制 ROOT_TREE 的叶子 → ... │ │
│ │ │ │
│ │ 5. 最终: 根地址更新 → 一次原子写入 → 提交完成 │ │
│ │ │ │
│ │ 小结: 修改 1 个 4KB 数据块 → 可能复制 数十个 │ │
│ │ 元数据节点 (每个也是 4KB+) │ │
│ │ 这就是 COW 的"树路径复制"成本 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么这不算太糟: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 只复制修改路径上的节点 → 不是整棵树 │ │
│ │ 2. 树的高度通常只有 3-4 层 → 通常是 ~4-10 个节点 │ │
│ │ 3. 相近时间的修改可以批量提交 → 分摊成本 │ │
│ │ 4. 延迟分配类比 → btrfs 也在内存中积累修改再刷盘 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2.2 COW 对随机写入的影响
┌─────────────────────────────────────────────────────────────┐
│ 为什么数据库在 btrfs 上跑得慢 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 数据库的特点: 频繁修改固定位置的 page (8KB) │
│ │
│ btrfs 的困境: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 数据库修改 1 个 8KB page: │ │
│ │ → COW: 8KB 写到新位置 + 复制树路径 │ │
│ │ → 旧 page 成为 COW 碎片 │ │
│ │ │ │
│ │ 数据库每秒修改几千个 page: │ │
│ │ → 几千 × COW 树路径复制 │ │
│ │ → 文件碎片化 (每次写一个新位置) │ │
│ │ → 元数据 B-tree 快速增长 │ │
│ │ → GC 压力增大 │ │
│ │ → 性能崩溃 │ │
│ │ │ │
│ │ 解决: chattr +C /mnt/db ← 此目录禁用 COW │ │
│ │ 但代价: 失去 checksum, 压缩, snapshot │ │
│ │ (对此目录而言) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 哪些场景禁用 COW: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ❌ 数据库文件 (MySQL/PostgreSQL/SQLite) │ │
│ │ ❌ 虚拟机镜像 (.qcow2/.vdi/.vmdk) │ │
│ │ ❌ BitTorrent 下载目录 (随机写) │ │
│ │ ❌ swap 文件 (如果有的话) │ │
│ │ │ │
│ │ ✅ 普通文件、源代码、文档 → COW 完全 OK │ │
│ │ ✅ 媒体文件 (写一次读多次) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
第3部分:Subvolume 与 Snapshot — btrfs 的杀手功能
3.1 Subvolume — 灵活的"分区"
┌─────────────────────────────────────────────────────────────┐
│ Subvolume 与传统分区 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统分区: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 100GB 磁盘 │ │
│ │ sda1: 40GB (/) ← 固定边界 │ │
│ │ sda2: 60GB (/home) ← 固定边界 │ │
│ │ │ │
│ │ / 满了, /home 还有 30GB → 无法调整! 需要 resize2fs │ │
│ │ 而且 resize 不是所有文件系统都支持 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ btrfs subvolume: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 100GB btrfs 分区 │ │
│ │ @ (/) ← subvolume, 当前用了 35GB │ │
│ │ @home (/home) ← subvolume, 当前用了 20GB │ │
│ │ │ │
│ │ 共享 100GB 池 → @ 和 @home 按需取用 │ │
│ │ 不需要"调整分区大小" → subvolume 没有固定大小! │ │
│ │ │ │
│ │ $ btrfs subvolume create /mnt/@vms │ │
│ │ $ btrfs subvolume list / │ │
│ │ ID 256 gen 12345 top level 5 path @ │ │
│ │ ID 257 gen 12346 top level 5 path @home │ │
│ │ ID 258 gen 12347 top level 5 path @vms │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Subvolume 的操作: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ btrfs subvolume create <path> 创建子卷 │ │
│ │ btrfs subvolume delete <path> 删除子卷 │ │
│ │ btrfs subvolume list <path> 列出子卷 │ │
│ │ btrfs subvolume show <path> 显示子卷详情 │ │
│ │ btrfs subvolume set-default <id> 设置默认挂载子卷 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Subvolume 的"边界"概念: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Subvolume 之间不可硬链接 — 它们有不同的 inode 编号空间│ │
│ │ Subvolume 看起来像目录 — 但对应用来说是独立挂载点 │ │
│ │ Snapshot 只能对 subvolume 做 — 不能对普通目录做 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
3.2 Snapshot — 时间冻结
┌─────────────────────────────────────────────────────────────┐
│ Snapshot = COW 的副产物 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 创建快照: │
│ $ btrfs subvolume snapshot / /.snap-20260617 │
│ Create a snapshot of '/' in '/.snap-20260617' │
│ │
│ 这做了什么? 几乎什么都没做。 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 创建 snapshot 时: │ │
│ │ 1. 复制 @ (/) 的 FS_TREE 根节点地址 │ │
│ │ 2. 在 ROOT_TREE 里新增一条"这里有个 snapshot" │ │
│ │ 3. 给这个 snapshot 一个新的 subvolume ID │ │
│ │ │ │
│ │ 没有复制任何文件数据! │ │
│ │ 两个 subvolume 共享同样的数据块 │ │
│ │ │ │
│ │ 只有当你修改文件时, COW 才真正"分裂": │ │
│ │ → 原 subvolume: 数据 → COW → 新位置 │ │
│ │ → snapshot: 数据 → 还在老位置 (不变) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 实际应用: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 场景 1: 系统更新前建快照 │ │
│ │ $ btrfs subvolume snapshot / /.snap-before-update │ │
│ │ $ sudo pacman -Syu ← 更新失败? │ │
│ │ $ sudo mount -o subvol=.snap-before-update ... │ │
│ │ → 还原到快照 → 一切恢复 │ │
│ │ │ │
│ │ 场景 2: 用 snapper 自动管理快照 │ │
│ │ $ sudo pacman -S snapper │ │
│ │ $ sudo snapper -c root create-config / │ │
│ │ snapper 自动: │ │
│ │ • 每次 pacman 事务前后建快照 │ │
│ │ • 每小时建一次 │ │
│ │ • 每天/每周/每月保留策略 │ │
│ │ • 老旧快照自动清理 │ │
│ │ │ │
│ │ 场景 3: Send/Receive — 备份/迁移 │ │
│ │ $ btrfs send /.snap-20260617 | ssh server \ │ │
│ │ btrfs receive /backup/pool │ │
│ │ → 增量传输整个 subvolume 状态到远程机器 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
第4部分:透明压缩 — 对应用不可见的省空间魔法
┌─────────────────────────────────────────────────────────────┐
│ btrfs 透明压缩 — 使用指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ mount -o compress=zstd → 一切写入自动压缩 │
│ │
│ 压缩效果 (实际测试): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据类型 原始大小 zstd压缩后 压缩比 │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ 源代码 (C/Rust) 1GB 250MB 4:1 │ │
│ │ 文本日志 500MB 80MB 6:1 │ │
│ │ 已压缩文件 1GB 1GB 1:1 │ │
│ │ (视频/jpg/zip) (zstd 检测到不可压缩 → 跳过) │ │
│ │ 虚拟机镜像 10GB 8GB 1.25:1 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 三种压缩算法: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ zlib → 最高压缩比, 最慢 │ │
│ │ lzo → 最快, 压缩比一般 (已不推荐) │ │
│ │ zstd → 速度和压缩比的甜点 ← 推荐! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 压缩选项: │
│ compress=zstd → 自动压缩 │
│ compress=zstd:3 → zstd 级别 3 (默认) │
│ compress=zstd:9 → 更高压缩比, 更慢 │
│ compress-force=zstd → 即使不可压缩也强制压缩 │
│ (一般不要用, 白费 CPU) │
│ │
│ 查看压缩效果: │
│ $ sudo compsize / │
│ Type Perc Disk Usage Uncompressed Referenced │
│ TOTAL 71% 8.5G 12G 11G │
│ zstd 60% 5.0G 8.3G 7.0G │
│ none 100% 3.5G 3.5G 3.5G │
│ ↑ 空间省了 29% │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
第5部分:已知缺陷和避坑指南
┌─────────────────────────────────────────────────────────────┐
│ btrfs 避坑指南 (截至 2026) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. RAID5/6 的 Write Hole │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ RAID5/6 在断电时可能产生不完整条带 → 静默数据损坏 │ │
│ │ → 不要用 btrfs 的 RAID5/6!用 RAID0/1/10 │ │
│ │ → 如果需要 RAID5/6 → 用 mdadm 底层做, btrfs 上层用 │ │
│ │ → 或用 ZFS (RAID-Z 解决了 write hole) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 大量快照 → 碎片 → 性能下降 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 症状: 系统用久了变慢 │ │
│ │ 原因: 数百个快照 → 大量旧版本数据 → 元数据膨胀 │ │
│ │ 解法: │ │
│ │ • 设置 snapper 的保留策略, 定期清理旧快照 │ │
│ │ • btrfs balance (重新平衡数据分布) │ │
│ │ • btrfs subvolume delete 删除不需要的快照 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. 高性能随机写 → 禁用 COW │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ chattr +C /path/to/db (对目录) │ │
│ │ chattr +C /path/to/vm.img (对文件, 必须先 touch 再设)│ │
│ │ │ │
│ │ 或用 mount 选项在整个 subvolume 禁用: │ │
│ │ mount -o nodatacow /dev/xxx /mnt/nocow │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 4. fsck 不够成熟 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ btrfs check 可以修复大部分问题 │ │
│ │ 但极端损坏情况下不如 ext4 的 fsck 可靠 │ │
│ │ → 定期做 btrfs scrub (数据完整性检查) │ │
│ │ → 重要数据用 snapshot + send 做远程备份 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ btrfs 日常维护三板斧: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ $ sudo btrfs scrub start / ← 每月一次 │ │
│ │ 检查所有数据的 checksum → 有损坏报警 │ │
│ │ │ │
│ │ $ sudo btrfs balance start / ← 每季度一次 │ │
│ │ 重新平衡数据分布 → 清理碎片 │ │
│ │ (不需要 full balance, 用 dusage=5 只平衡低利用率块)│ │
│ │ │ │
│ │ $ sudo btrfs filesystem df / ← 随时可查 │ │
│ │ 查看 btrfs 内部空间分配情况 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
核心总结
总结1:btrfs 快速参考卡
核心数据结构: B-tree (所有东西)
更新方式: COW (写时复制)
杀手功能: subvolume + snapshot + 透明压缩
已知缺陷: RAID5/6 write hole, 大量快照导致碎片
维护: scrub (每月) + balance (每季度)1
2
3
4
5
2
3
4
5
总结2:btrfs vs ext4 选型
选 btrfs: 需要 snapshot/快照回滚/透明压缩/灵活 subvolume
选 ext4: 追求极稳/跑数据库/VM/不想学维护命令1
2
2
总结3:COW 的正确用法
普通文件 → COW → 完美
数据库/VM → nocow → 必需
snapshot → COW 的馈赠 → 用好它1
2
3
2
3
章节测试
测试1:COW 成本
修改 btrfs 中一个 4KB 数据块。除了这个数据块本身,还需要写入多少块?为什么?
测试2:snapshot 的占用
创建一个 100GB subvolume 的快照。快照会立即占用多少额外磁盘空间?
测试3:nocow
chattr +C 对目录的作用是什么?代价是什么?
测试4:scrub vs balance
btrfs scrub 和 btrfs balance 各是什么用途?应该多久执行一次?
参考答案
测试1答案
除了数据块本身,还需要写入从该数据块的 extent item 所在的叶子节点一直到树根的所有节点。因为 B-tree 的 COW 要求修改叶子 → 复制叶子 → 修改父节点 → 复制父节点 → ... → 直到根。树高通常 3-4 层,所以通常需要额外写入 4-10 个元数据块(每个 4KB+)。这就是 COW 的写放大成本。
测试2答案
几乎不占。snapshot 创建时只是复制了 subvolume 的 FS_TREE 根指针,没有复制任何数据块。两个 subvolume 共享所有数据块。只有当原始 subvolume 中的文件被修改时,COW 才将修改的数据写入新块,此时新旧版本开始分化,snapshot 才逐渐占据额外的空间。
测试3答案
chattr +C 对该目录及其中的新文件禁用 COW。代价:失去该目录中所有文件的 checksum 保护、透明压缩和 snapshot 的一致性保证。对于数据库和 VM 镜像这是必要的性能优化,但对于一般文件不应该使用。
测试4答案
- scrub:遍历所有数据块,重新计算 checksum,与存储的 checksum 对比。检测静默数据损坏。建议每月执行。
- balance:重新分配数据块在磁盘上的分布,回收未充分利用的块组,减少碎片。类似磁盘碎片整理 + 空间回收。建议每季度执行(但不需要 full balance,用过滤器如
dusage=5)。
相关笔记
- [[04-filesystem-concepts]] - COW 的概念基础
- [[05-filesystem-formats]] - btrfs 与 ext4/ZFS 的横向对比
- [[12-ext4-deep-dive]] - 另一种哲学:Journal vs COW
- [[10-linux-disk-practice]] - btrfs subvolume 的创建和挂载
下一步学习
学习状态:🟡 开始学习