强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 选项

关键概念回顾

  1. 子卷不是分区,共享底层存储池
  2. 快照是针对子卷的,不递归包含嵌套子卷
  3. 删除子卷不释放已删除数据的空间(需要 balance)
  4. 推荐使用 subvol= 路径而非 subvolid= 数字
  5. 子卷布局应在系统安装时规划好

扩展阅读