Btrfs 进阶与透明压缩
你已经知道“文件系统负责组织文件”,但 Btrfs 还把压缩、子卷和空间核算放进了同一个文件系统。它们很有用,也很容易被几个相似的词误导:子卷不是分区,快照不是备份,开启压缩也不会自动重写旧数据。
本文只解决三件事:透明压缩、以 @/@home 为核心的子卷布局,以及 qgroup(quota group)空间配额。快照策略、发送接收和多设备 RAID 不在本文范围内。
- 适合谁:已经会使用
findmnt、lsblk,并理解挂载点和/etc/fstab的读者。 - 学完能做什么:解释压缩、子卷与 qgroup 的边界;只读识别本机状态;在一次性镜像中创建并撤销配置。
- 环境:示例按截至 2026-07 的 Linux Btrfs 与
btrfs-progs官方文档编写。建议使用内核 6.6 或更新的长期支持版本及同代btrfs-progs;ZSTD 从内核 4.14 起可用,正数压缩级别从 5.1 起可用。 - 前置条件:涉及真实
/etc/fstab前,必须有外部数据备份、可启动救援介质或虚拟机快照。本文的状态变更练习只使用/tmp中的一次性镜像。 - 时间与成功证据:阅读约 25 分钟,练习约 20 分钟;最终应保存子卷列表、挂载选项和 qgroup 输出,并确认测试镜像已清理。
1. 先建立准确模型
Btrfs 是写时复制(Copy-on-Write,CoW)文件系统。修改已有数据时,它通常写入新 extent(区段)并更新引用,而不是原地覆盖。这个机制让多个子卷或快照共享 extent,也让“实际占用多少空间”比传统目录求和更复杂。
可以把一个 Btrfs 文件系统想成一座共享仓库:子卷是不同的取货入口,压缩决定新货物怎样打包,qgroup 记录每个入口能引用和独占多少货物。这个比喻的边界是:子卷没有独占块设备容量,除非另用 qgroup 施加限制;它也不是 LVM 逻辑卷。
| 概念 | 准确定义 | 不代表什么 |
|---|---|---|
| Top-level 子卷 | 新建文件系统自带、ID 为 5 的顶层文件树 | 普通目录或可删除子卷 |
| 子卷 | 有独立 inode 命名空间的 Btrfs 文件树,可单独挂载 | 分区、LVM LV 或天然容量边界 |
| 快照 | 以另一个子卷内容为起点的子卷,初始共享 extent | 异机备份;底层设备损坏时也可能一起损坏 |
referenced | qgroup 中任一子卷可访问的全部空间 | 删除后必然释放的空间 |
exclusive | 只有该 qgroup 内部可访问的空间 | 文件逻辑大小 |
快照与源数据通常位于同一文件系统并共享底层 extent。误写块设备、设备故障或文件系统级损坏可能同时影响两者。重要数据仍需独立介质或异机备份。
2. 先只读观察当前系统
以下命令不修改文件系统。先确认根目录是否真是 Btrfs,再讨论配置:
findmnt -no SOURCE,FSTYPE,OPTIONS /
findmnt -t btrfs -o TARGET,SOURCE,FSTYPE,OPTIONS
uname -r
sudo btrfs version如果 findmnt 显示根目录的 FSTYPE 是 btrfs,再读取其用量和子卷:
sudo btrfs filesystem usage /
sudo btrfs subvolume list /如果根目录不是 btrfs,不要对 / 运行这两条命令;仍可完成一次性镜像练习。SOURCE 可能是 /dev/mapper/...,不要假定它一定是某个 NVMe 或 SATA 分区。
还要记住一个容易忽略的边界:compress 等多数 Btrfs 专用挂载选项作用于整个文件系统,同一文件系统的多个子卷挂载并不能可靠地各用一套不同压缩算法。
3. 透明压缩:只影响新写入的数据
Btrfs 在 extent 写入时压缩,在读取时透明解压;应用看到的仍是原始内容。截至 2026-07,上游支持 ZLIB、LZO 和 ZSTD:
| 算法 | 级别 | 取舍 |
|---|---|---|
zstd | 1..15;默认 3 | 通常是速度与压缩率的实用起点;级别越高越耗 CPU 和内存,收益依数据而异 |
lzo | 无级别 | 压缩/解压快,压缩率通常较低 |
zlib | 1..9;默认 3 | 兼容性好,通常比 ZSTD 慢 |
compress=zstd:3 会使用启发式判断,避免反复处理明显不可压缩的数据。compress-force 会尝试更多数据,但最终比原始数据更大的块仍会以未压缩形式存储;上游并不把强制模式作为通用推荐。
3.1 修改 fstab 前后的完整安全闭环
只有当根文件系统已经是 Btrfs、备份已验证且救援介质可用时,才考虑这一步。先探测真实 UUID 和当前配置:
root_source="$(findmnt -no SOURCE /)"
root_uuid="$(findmnt -no UUID /)"
findmnt -no SOURCE,UUID,FSTYPE,FSROOT,OPTIONS /
printf 'source=%s\nuuid=%s\n' "$root_source" "$root_uuid"
sudo findmnt --verify --verbose如果 root_uuid 为空,或 fstab 使用的是 LUKS 映射、LABEL、PARTUUID 等另一种稳定标识,停下并查阅本发行版的存储布局;不要猜 UUID,也不要擅自改变安装器原本的标识方式。
风险:错误的设备标识、子卷名或选项可能让下次启动无法挂载根文件系统。语法验证也不能证明设备在下次启动时一定可用。
先备份并记录当前状态:
backup="/etc/fstab.bak.$(date +%Y%m%d-%H%M%S)"
sudo cp --preserve=all /etc/fstab "$backup"
findmnt -no SOURCE,FSTYPE,OPTIONS / | tee "$HOME/btrfs-root-before.txt"
printf '%s\n' "$backup"然后使用 sudoedit /etc/fstab,只在已经确认的 Btrfs 行的第四字段加入 compress=zstd:3。结构示例如下,<btrfs-uuid> 必须替换为刚才探测到的值:
UUID=<btrfs-uuid> / btrfs defaults,compress=zstd:3,subvol=@ 0 0保存后先验证,不要为了“试试看”直接重启:
sudo findmnt --verify --verbose预期结果是没有 parse error、unknown option 或无法解析的 source。若验证失败,立即恢复:
sudo cp --preserve=all "$backup" /etc/fstab
sudo findmnt --verify --verbose验证通过只表示配置内部一致。应在有救援条件的计划维护窗口重启,再用下面的命令确认运行时选项:
findmnt -no FSTYPE,OPTIONS /若启动失败,从救援环境挂载根文件系统,将备份复制回 /etc/fstab;不同发行版的根子卷挂载方式不同,因此应在改动前记录真实 SOURCE、UUID 与 subvol。
3.2 为什么旧文件没有立刻变小
挂载选项只影响之后的新写入;现有 extent 不会自动重写。compsize <path> 可估算实际压缩情况,但它是额外工具,未安装时不要把“逻辑大小变小”当成验证方法。
可以用 btrfs filesystem defragment -c... 重写存量文件并压缩,但这会产生大量 I/O,而且不会保留 reflink/快照间的 extent 共享,磁盘占用可能明显增加。不要对整个根目录盲目执行递归 defrag;若确有迁移需求,先在副本和容量充足的测试环境中评估。
4. @ / @home:布局约定,不是硬性标准
平铺布局把多个子卷直接放在 top-level 下。@ 挂载为 /、@home 挂载为 /home 是 Ubuntu/Arch 社区工具中常见的命名约定,不是 Btrfs 协议要求。额外拆分子卷会形成快照边界:快照 @ 时不会递归包含嵌套或单独挂载的 @home。
subvolid=5
├── @ -> /
├── @home -> /home
├── @snapshots -> /.snapshots(按快照工具需要选择)
├── @cache -> /var/cache(可选)
└── @log -> /var/log(可选)@cache 和 @log 是否拆分取决于你希望系统回滚时保留什么。拆出去意味着根快照不包含其内容,也意味着备份工具需要单独覆盖它们。
4.1 在一次性镜像中创建布局
下面的操作会格式化文件,但目标只允许是新建的 /tmp/btrfs-lab.img。不要把 loopdev 替换成真实磁盘。
探测与停止条件:系统需要 btrfs-progs、losetup 和 root 权限;如果镜像文件已经存在,先停下确认它不是你的数据。
command -v mkfs.btrfs
command -v losetup
test ! -e /tmp/btrfs-lab.img创建一次性镜像并记录内核分配的 loop 设备:
truncate -s 1G /tmp/btrfs-lab.img
loopdev="$(sudo losetup --find --show --nooverlap /tmp/btrfs-lab.img)"
printf 'loop device: %s\n' "$loopdev"
sudo lsblk -f "$loopdev"
sudo mkfs.btrfs -L btrfs-lab "$loopdev"
mnt="$(mktemp -d /tmp/btrfs-lab.XXXXXX)"
sudo mount "$loopdev" "$mnt"预期 mkfs.btrfs 显示标签 btrfs-lab,且 findmnt "$mnt" 能看到刚才记录的 loop 设备。随后创建与布局图一致的五个子卷:
for name in @ @home @snapshots @cache @log; do
sudo btrfs subvolume create "$mnt/$name"
done
sudo btrfs subvolume list "$mnt"验收输出应包含五个路径。若中途失败,不要重复格式化;先记录 btrfs subvolume list "$mnt",再决定补建缺少的子卷。清理和回滚见第 6 节统一步骤。
如果 mkfs.btrfs 或首次挂载失败,先保存错误输出;已挂载时正常卸载,然后分离刚才打印的 loopdev,再删除空挂载目录和镜像文件。不要在失败后把命令目标换成真实设备。
真实系统的持久挂载还需为每个目标创建目录,并在 fstab 中用真实 UUID 配置 subvol=@home 等。不要从本文复制一个虚构设备名覆盖安装器生成的布局。
5. qgroup:给子卷核算并限制空间
qgroup 用 referenced 与 exclusive 处理共享 extent 的记账问题。Level 0 qgroup 通常与子卷 ID 对应,例如 0/257。限制 referenced 空间意味着达到上限后的写入可能收到 Quota Exceeded;它不是“自动清理”机制。
启用 quota 需要扫描并持续维护引用关系,会增加元数据与 I/O 开销。若 qgroup 标记为 inconsistent,其 limit 在完成 rescan 前不可靠。先确认确有按子卷限额需求,再在生产系统启用。
继续使用第 4 节的一次性镜像。先记录未启用时的状态,再改变状态:
sudo btrfs qgroup show "$mnt"
sudo btrfs quota enable "$mnt"
sudo btrfs quota rescan -w "$mnt"
sudo btrfs qgroup show -re --sync "$mnt"第一次 qgroup show 可能报告 quota 未启用;启用并等待 rescan 后,应看到与子卷对应的 0/<id>。为 @home 设置 64 MiB referenced 上限:
sudo btrfs qgroup limit 64M "$mnt/@home"
sudo btrfs qgroup show -re --sync "$mnt"验证时找到 @home 行,确认 max_rfer 为约 64 MiB。若命令警告 qgroup 数据 inconsistent:
sudo btrfs quota rescan -w "$mnt"
sudo btrfs qgroup show -re --sync "$mnt"解除单个限制使用 none,不是删除子卷:
sudo btrfs qgroup limit none "$mnt/@home"
sudo btrfs qgroup show -re --sync "$mnt"若决定完全撤销该文件系统的 quota 功能:
sudo btrfs quota disable "$mnt"这会移除 qgroup 配置与统计;在真实系统执行前,应先记录所有限制,因为关闭后不能自动还原它们。
6. 安全清理实验环境
先确认变量仍指向 /tmp 的实验挂载和 loop 设备,再卸载:
findmnt "$mnt"
sudo losetup --list "$loopdev"
sudo umount "$mnt"
sudo losetup --detach "$loopdev"
rmdir "$mnt"
rm /tmp/btrfs-lab.img
test ! -e /tmp/btrfs-lab.img && echo "lab cleaned"预期最终输出 lab cleaned。若 umount 报 target is busy,不要强制删除镜像;先运行 findmnt -R "$mnt" 和 fuser -vm "$mnt" 找出仍在使用它的进程,关闭相关终端或程序后再卸载。
7. 常见故障:先诊断再修复
| 症状 | 常见原因 | 只读诊断 | 下一步决策 |
|---|---|---|---|
| fstab 验证失败 | UUID、文件系统类型、字段或选项错误 | sudo findmnt --verify --verbose | 不重启;恢复备份后重新探测 |
| 重启后未看到压缩选项 | 改错 fstab 行、使用了另一根子卷或参数未生效 | findmnt -no SOURCE,FSTYPE,OPTIONS / | 对照真实 source 与 fstab;先保留当前可启动状态 |
| 新文件看似未压缩 | 数据不可压缩、工具统计范围错误或 NOCOMPRESS 标记 | lsattr <file>;sudo compsize <path> | 对可压缩测试文件验证;不要盲目启用 compress-force |
| 子卷列表缺少预期项 | 挂载的不是 top-level 或创建过程失败 | findmnt -no SOURCE,OPTIONS <mountpoint>;sudo btrfs subvolume list <mountpoint> | 确认 subvolid=5 后再补建,避免同名普通目录 |
qgroup 警告 inconsistent | 引用关系变更后统计未完成 | sudo btrfs qgroup show -re --sync <mountpoint>;sudo btrfs quota rescan -s <mountpoint> | 等待或启动 rescan;一致前不要依赖限额 |
No space left on device 但 df 尚有空间 | 数据/元数据块组不足或 qgroup limit 命中 | sudo btrfs filesystem usage <mountpoint>;sudo btrfs qgroup show -re --sync <mountpoint> | 分清全局空间与配额,再决定清理或调限;不要先 balance |
| 卸载提示 busy | 工作目录或进程仍持有文件 | findmnt -R <mountpoint>;fuser -vm <mountpoint> | 退出使用者后正常卸载,不要直接删除 backing file |
如果文件系统突然只读、出现 I/O error,或底层设备报告介质错误,应停止写入,保存 dmesg 与 btrfs device stats 证据,并转到专门的文件系统救援流程;不要用 btrfs check --repair 试错。
8. 可观察练习
9. 小结与下一站
你现在应该能解释:压缩策略针对新写入 extent;@/@home 是共享同一空间池的子卷约定;qgroup 通过 referenced/exclusive 处理共享 extent 的核算。更重要的是,你能先识别真实文件系统,再在一次性镜像里验证、撤销和清理,而不是拿工作系统试错。
下一篇阅读 磁盘方案进阶(LVM/LUKS):它会把视角下移到块设备抽象与静态数据加密,并说明它们与文件系统的层级关系。
核验资料(截至 2026-07)
- Btrfs 官方文档:Compression
- Btrfs 官方文档:Subvolumes
- Btrfs 官方文档:Quota groups
- Btrfs 官方手册:btrfs-qgroup(8)
- Btrfs 官方文档:Defragmentation