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

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. 子卷布局应在系统安装时规划好

扩展阅读