Btrfs 文件系统运维完全教程 / 第 3 章:子卷管理
第 3 章:子卷管理
3.1 子卷概念
3.1.1 什么是子卷
子卷(Subvolume)是 Btrfs 文件系统内部的一个独立文件树。它不是分区,也不是块设备,而是 Btrfs 内部的一个逻辑分组。
子卷的关键特性:
| 特性 | 说明 |
|---|---|
| 共享存储池 | 子卷之间共享文件系统的可用空间 |
| 独立 inode 编号 | 每个子卷有独立的 inode 空间 |
| 可独立挂载 | 通过 subvol= 选项挂载指定子卷 |
| 快照单位 | 快照是针对子卷创建的 |
| 轻量级 | 创建子卷不消耗额外空间(仅少量元数据) |
| 不可跨设备 | 子卷不能跨越多个设备 |
3.1.2 子卷 vs 分区 vs LVM
| 维度 | 子卷 | 分区 | LVM LV |
|---|---|---|---|
| 空间分配 | 动态共享 | 固定大小 | 可动态调整 |
| 创建速度 | 瞬间 | 需要重新分区 | 秒级 |
| 快照 | 原生支持 | 不支持 | 支持但开销较大 |
| 空间浪费 | 无 | 各分区可能有闲置 | 预分配可能浪费 |
| 独立文件系统 | 否(同一 fs 内) | 是 | 是 |
| 嵌套 | 支持 | 不支持 | 不支持 |
| 移动/调整大小 | 不适用 | 复杂 | 可在线调整 |
3.1.3 为什么使用子卷
场景 1:分离关注点
/dev/sda2 挂载为 Btrfs
├── @ → 挂载到 /
├── @home → 挂载到 /home
├── @var → 挂载到 /var
├── @snapshots → 挂载到 /.snapshots
└── @tmp → 挂载到 /tmp
场景 2:独立备份策略
# 可以单独备份某个子卷
btrfs send /mnt/@home | btrfs receive /backup/
# 而不需要备份整个文件系统
场景 3:差异化挂载选项
# /var 频繁写入,关闭压缩
UUID=xxx /var btrfs subvol=/@var,noatime 0 0
# /home 用户数据,启用高压缩
UUID=xxx /home btrfs subvol=/@home,compress=zstd:5,noatime 0 0
# 数据库目录,关闭 COW
UUID=xxx /var/lib/mysql btrfs subvol=/@mysql,noatime,nodatacow 0 0
3.2 子卷创建与删除
3.2.1 创建子卷
# 基础创建
sudo btrfs subvolume create /mnt/data/documents
# 创建嵌套子卷
sudo btrfs subvolume create /mnt/data/documents/projects
sudo btrfs subvolume create /mnt/data/documents/projects/alpha
# 批量创建
for name in home var tmp snapshots docker; do
sudo btrfs subvolume create /mnt/@${name}
done
创建子卷时的输出:
Create subvolume '/mnt/data/documents'
3.2.2 列出子卷
# 列出文件系统的所有子卷
sudo btrfs subvolume list /mnt/data
# 输出示例:
# ID 256 gen 10 top level 5 path @
# ID 257 gen 15 top level 5 path @home
# ID 258 gen 8 top level 5 path @var
# ID 259 gen 20 top level 5 path @snapshots
# ID 261 gen 22 top level 256 path @home/documents
输出字段说明:
| 字段 | 说明 |
|---|---|
| ID | 子卷 ID(唯一标识) |
| gen | 创建/修改的 generation |
| top level | 父子卷的 ID(5 是文件系统根) |
| path | 子卷相对于文件系统根的路径 |
# 按路径排序
sudo btrfs subvolume list -p /mnt/data
# ID 256 parent 5 top level 5 path @
# ID 257 parent 5 top level 5 path @home
# ID 261 parent 256 top level 256 path @home/documents
# 只显示快照
sudo btrfs subvolume list -s /mnt/data
# 按排序字段
sudo btrfs subvolume list -o /mnt/data # 按 origin 排序
sudo btrfs subvolume list -u /mnt/data # 显示 UUID
# 简洁格式
sudo btrfs subvolume list /mnt/data | awk '{print $NF}'
3.2.3 查看子卷信息
# 查看子卷详情
sudo btrfs subvolume show /mnt/data
# 输出示例:
# /mnt/data
# Name: <FS_TREE>
# UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Parent UUID: -
# Received UUID: -
# Creation time: 2026-05-10 10:00:00 +0800
# Subvolume ID: 5
# Generation: 20
# Gen at creation: 1
# Parent ID: 0
# Top level ID: 0
# Flags: -
# Send transid: 0
# Send transid path: -
# Last transid: 20
# Last transid path: -
# Size of qgroup inherited: -
# Snapshot(s):
# 查看特定子卷
sudo btrfs subvolume show /mnt/data/@home
# 查看子卷的 UUID(用于 send/receive)
sudo btrfs subvolume show /mnt/data/@home | grep "Parent UUID"
3.2.4 删除子卷
# 删除子卷
sudo btrfs subvolume delete /mnt/data/documents/projects/alpha
# 删除多个子卷
sudo btrfs subvolume delete /mnt/data/documents/projects /mnt/data/documents
# 删除所有快照子卷
sudo btrfs subvolume list -s /mnt/data | awk '{print $NF}' | while read snap; do
sudo btrfs subvolume delete "/mnt/data/$snap"
done
# 递归删除(需要 btrfs-progs 6.1+)
sudo btrfs subvolume delete -c /mnt/data/documents
⚠️ 警告: 删除子卷是不可逆操作!确保已备份需要的数据。如果子卷有快照,需要先删除快照才能删除子卷。
# 检查是否有快照依赖
sudo btrfs subvolume list -s /mnt/data | grep "@home"
# 如果有输出,说明 @home 有快照,删除 @home 前需要先处理快照
3.3 子卷挂载
3.3.1 通过 subvol= 挂载
# 挂载指定子卷到根
sudo mount -o subvol=/@ /dev/sdb1 /mnt/root
# 挂载其他子卷到不同目录
sudo mount -o subvol=/@home /dev/sdb1 /mnt/home
sudo mount -o subvol=/@var /dev/sdb1 /mnt/var
3.3.2 通过 subvolid= 挂载
# 通过子卷 ID 挂载
sudo mount -o subvolid=256 /dev/sdb1 /mnt/home
# 查看子卷 ID
sudo btrfs subvolume list /mnt/data
💡 提示: 推荐使用
subvol=而不是subvolid=,因为子卷 ID 在文件系统重建后可能变化,但路径名通常保持不变。
3.3.3 挂载文件系统根
# 不指定 subvol 时,默认挂载文件系统的顶层(FS_TREE)
sudo mount /dev/sdb1 /mnt/all
# 此时可以看到所有子卷目录
ls /mnt/all/
# @ @home @var @snapshots
3.3.4 子卷挂载最佳实践
典型 fstab 配置:
# /etc/fstab
# 根分区
UUID=xxx / btrfs subvol=/@,defaults,compress=zstd:1,ssd,discard=async 0 0
# 用户数据
UUID=xxx /home btrfs subvol=/@home,defaults,compress=zstd:3,ssd 0 0
# 日志目录(关闭压缩)
UUID=xxx /var btrfs subvol=/@var,defaults,noatime,ssd 0 0
# 快照目录(不自动挂载到路径,仅用于管理)
UUID=xxx /.snapshots btrfs subvol=/@snapshots,defaults,ssd,noauto 0 0
# tmp(不提交日志,减少 SSD 写入)
UUID=xxx /tmp btrfs subvol=/@tmp,defaults,noatime,nodatacow,ssd 0 0
3.4 默认子卷
3.4.1 设置默认子卷
# 将 @ 子卷设置为默认挂载的子卷
sudo btrfs subvolume set-default 256 /mnt/data
# 确认默认子卷
sudo btrfs subvolume get-default /mnt/data
# ID 256 gen 20 top level 5 path @
# 此后再不带 subvol= 挂载时,将默认挂载 @ 子卷
sudo mount /dev/sdb1 /mnt
# 实际挂载的是 @ 子卷
3.4.2 重置默认子卷
# 将默认子卷重置为 FS_TREE(ID 5)
sudo btrfs subvolume set-default 5 /mnt/data
# 验证
sudo btrfs subvolume get-default /mnt/data
# ID 5 gen 20 top level 5 path <FS_TREE>
📝 注意: 重置默认子卷为 5(FS_TREE)后,挂载时将显示整个文件系统的目录结构,包括所有子卷目录。
3.5 子卷权限与配额
3.5.1 子卷的权限
子卷继承文件系统的挂载权限,但可以单独控制:
# 查看子卷权限
ls -la /mnt/data/
# drwxr-xr-x 1 root root 0 May 10 10:00 @
# drwxr-xr-x 1 root root 0 May 10 10:00 @home
# 修改子卷目录权限
sudo chmod 755 /mnt/data/@home
sudo chown user:group /mnt/data/@home
3.5.2 子卷配额(Qgroup)
配额在第 7 章详细讲解,这里先了解基本概念:
# 启用配额
sudo btrfs quota enable /mnt/data
# 创建配额组
sudo btrfs qgroup create 1/1 /mnt/data
# 限制子卷大小(10GB)
sudo btrfs qgroup limit 10G /mnt/data/@home
# 查看配额
sudo btrfs qgroup show /mnt/data
3.6 嵌套子卷
3.6.1 嵌套子卷的概念
子卷可以嵌套创建,形成树状结构:
/mnt/data/(FS_TREE, ID 5)
├── @ (ID 256) ← 默认子卷
│ ├── @home (ID 257) ← 嵌套在 @ 下
│ │ ├── @home/user1 (ID 262)
│ │ └── @home/user2 (ID 263)
│ ├── @var (ID 258) ← 嵌套在 @ 下
│ └── @snapshots (ID 259) ← 嵌套在 @ 下
└── @backup (ID 260) ← 直接在 FS_TREE 下
3.6.2 嵌套子卷的特点
| 特性 | 说明 |
|---|---|
| 父子卷删除不影响子子卷 | 删除父卷不会自动删除子卷(需要递归删除) |
| 快照不包含嵌套子卷 | 对父卷创建快照时,子卷只记录为挂载点 |
| 独立 inode 编号 | 每个子卷有独立的 inode 编号空间 |
| 配额独立 | 可以对每个层级独立设置配额 |
3.6.3 快照与嵌套子卷
# 创建父卷快照
sudo btrfs subvolume snapshot /mnt/data/@ /mnt/data/@snapshots/@-20260510
# 快照中的嵌套子卷只显示为挂载点(空目录)
ls /mnt/data/@snapshots/@-20260510/
# home/ var/ tmp/ ← 这些是空目录,不是子卷的副本
⚠️ 注意: 这意味着快照不是递归的。如果需要备份嵌套子卷,需要对每个子卷单独创建快照。
3.6.4 子卷布局推荐
桌面系统布局:
FS_TREE (ID 5)
├── @ (ID 256) → /
├── @home (ID 257) → /home
├── @var (ID 258) → /var
├── @tmp (ID 259) → /tmp
├── @snapshots (ID 260) → /.snapshots
└── @swap (ID 261) → swap file
服务器布局:
FS_TREE (ID 5)
├── @root (ID 256) → /
├── @home (ID 257) → /home
├── @var (ID 258) → /var
├── @var-log (ID 259) → /var/log
├── @var-lib-mysql (ID 260) → /var/lib/mysql
├── @docker (ID 261) → /var/lib/docker
├── @snapshots (ID 262) → /.snapshots
└── @backup (ID 263) → /backup
3.7 子卷操作脚本
3.7.1 自动创建布局脚本
#!/bin/bash
# setup-btrfs-layout.sh - 创建标准 Btrfs 子卷布局
set -euo pipefail
DEVICE="${1:?Usage: $0 /dev/sdXN}"
MOUNT_DIR="/mnt/btrfs-setup"
echo "=== Creating Btrfs subvolume layout on $DEVICE ==="
# 临时挂载
sudo mkdir -p "$MOUNT_DIR"
sudo mount "$DEVICE" "$MOUNT_DIR"
# 定义子卷列表
SUBVOLUMES=("@root" "@home" "@var" "@var-log" "@tmp" "@snapshots")
# 创建子卷
for sv in "${SUBVOLUMES[@]}"; do
if sudo btrfs subvolume show "$MOUNT_DIR/$sv" &>/dev/null; then
echo "Subvolume $sv already exists, skipping."
else
sudo btrfs subvolume create "$MOUNT_DIR/$sv"
echo "Created subvolume: $sv"
fi
done
# 设置默认子卷
DEFAULT_ID=$(sudo btrfs subvolume list "$MOUNT_DIR" | grep " path @$" | awk '{print $2}')
if [[ -n "$DEFAULT_ID" ]]; then
sudo btrfs subvolume set-default "$DEFAULT_ID" "$MOUNT_DIR"
echo "Set @ as default subvolume (ID: $DEFAULT_ID)"
fi
echo "=== Subvolume layout created ==="
sudo btrfs subvolume list "$MOUNT_DIR"
sudo umount "$MOUNT_DIR"
rmdir "$MOUNT_DIR"
3.7.2 子卷信息报告脚本
#!/bin/bash
# btrfs-subvol-report.sh - 生成子卷信息报告
set -euo pipefail
MOUNT_POINT="${1:?Usage: $0 /mount/point}"
echo "=== Btrfs Subvolume Report ==="
echo "Mount point: $MOUNT_POINT"
echo "Date: $(date '+%Y-%m-%d %H:%M:%S')"
echo ""
echo "--- Filesystem Info ---"
sudo btrfs filesystem show "$MOUNT_POINT"
echo ""
echo "--- Space Usage ---"
sudo btrfs filesystem df "$MOUNT_POINT"
echo ""
echo "--- Default Subvolume ---"
sudo btrfs subvolume get-default "$MOUNT_POINT"
echo ""
echo "--- All Subvolumes ---"
printf "%-6s %-8s %-6s %-6s %s\n" "ID" "Gen" "Parent" "Top" "Path"
echo "------------------------------------------------"
sudo btrfs subvolume list "$MOUNT_POINT" | while read -r _ id _ gen _ top _ _ path; do
printf "%-6s %-8s %-6s %-6s %s\n" "$id" "$gen" "-" "$top" "$path"
done
echo ""
echo "--- Snapshots ---"
SNAPSHOTS=$(sudo btrfs subvolume list -s "$MOUNT_POINT" 2>/dev/null | wc -l)
echo "Total snapshots: $SNAPSHOTS"
if [[ "$SNAPSHOTS" -gt 0 ]]; then
sudo btrfs subvolume list -s "$MOUNT_POINT" | awk '{print " "$NF}'
fi
3.8 本章小结
| 操作 | 命令 |
|---|---|
| 创建子卷 | btrfs subvolume create /mnt/path |
| 列出子卷 | btrfs subvolume list /mnt |
| 查看子卷信息 | btrfs subvolume show /mnt/path |
| 删除子卷 | btrfs subvolume delete /mnt/path |
| 设置默认子卷 | btrfs subvolume set-default ID /mnt |
| 获取默认子卷 | btrfs subvolume get-default /mnt |
| 挂载子卷 | mount -o subvol=/@ /dev/sdX /mnt |
| 挂载 FS_TREE | 不指定 subvol 选项 |
关键概念回顾:
- 子卷不是分区,共享底层存储池
- 快照是针对子卷的,不递归包含嵌套子卷
- 删除子卷不释放已删除数据的空间(需要 balance)
- 推荐使用
subvol=路径而非subvolid=数字 - 子卷布局应在系统安装时规划好