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

Btrfs 文件系统运维完全教程 / 第 4 章:快照与回滚

第 4 章:快照与回滚

4.1 快照原理

4.1.1 COW 快照的魔力

Btrfs 快照利用 Copy-on-Write(COW)机制实现零拷贝创建:

创建快照前:
  子卷 @home
  └── Block A [数据1]  Block B [数据2]  Block C [数据3]

创建快照后(瞬间完成):
  子卷 @home                  快照 @home-snap
  ├── Block A [数据1]  ─────────────────────────┐
  ├── Block B [数据2]  ─────────────────────────┤
  └── Block C [数据3]  ─────────────────────────┤
                                                │
  所有数据块共享,快照不额外占用空间 ◄────────────┘

修改子卷中的 Block B 后:
  子卷 @home                  快照 @home-snap
  ├── Block A [数据1]  ─────────────────────────┐
  ├── Block B'[新数据] ← 写入新位置              │
  └── Block C [数据3]  ─────────────────────────┤
                                                │
                     Block B [原始数据] ◄────────┘
  只有被修改的块占用额外空间

4.1.2 快照的特性

特性说明
创建速度瞬间(O(1) 复杂度)
初始空间占用几乎为零
空间增长仅在原数据被修改时才增长
可写/只读支持两种模式
独立性快照是独立的子卷,可单独删除
非递归不包含嵌套子卷的数据

4.2 创建快照

4.2.1 可写快照

# 创建可写快照
sudo btrfs subvolume snapshot /mnt/@home /mnt/@snapshots/home-20260510
# Create a snapshot of '/mnt/@home' in '/mnt/@snapshots/home-20260510'

# 验证
sudo btrfs subvolume show /mnt/@snapshots/home-20260510

4.2.2 只读快照

# 创建只读快照(推荐用于备份)
sudo btrfs subvolume snapshot -r /mnt/@home /mnt/@snapshots/home-20260510-ro
# Create a readonly snapshot of '/mnt/@home' in '/mnt/@snapshots/home-20260510-ro'

# 只读快照可以作为 send 的源
btrfs send /mnt/@snapshots/home-20260510-ro | ...

💡 提示: 始终对用于备份的快照使用只读模式(-r)。btrfs send 要求源快照是只读的。

4.2.3 快照命名规范

建议使用有意义的命名:

# 日期格式
home-20260510          # 年月日
home-20260510-143000   # 年月日-时分秒
home-before-upgrade    # 操作前的快照
home-pre-upgrade-20260510  # 结合日期和描述
var-before-config-change   # 配置变更前

# 脚本化命名
SNAP_NAME="$(date '+%Y%m%d-%H%M%S')"
sudo btrfs subvolume snapshot -r /mnt/@home "/mnt/@snapshots/home-${SNAP_NAME}"

4.3 管理快照

4.3.1 列出快照

# 列出所有快照
sudo btrfs subvolume list -s /mnt

# 输出示例:
# ID 300 gen 50 top level 260 path @snapshots/home-20260501
# ID 301 gen 55 top level 260 path @snapshots/home-20260510
# ID 302 gen 60 top level 260 path @snapshots/var-20260510

# 只读快照的标志
sudo btrfs subvolume list -s -r /mnt

4.3.2 删除快照

# 删除单个快照
sudo btrfs subvolume delete /mnt/@snapshots/home-20260501

# 批量删除过期快照
sudo btrfs subvolume list -s /mnt | grep "home-20260[12]" | awk '{print $NF}' | while read snap; do
    echo "Deleting: $snap"
    sudo btrfs subvolume delete "/mnt/$snap"
done

4.3.3 只读转可写

# 只读快照可以转为可写
# 方法:创建一个新的可写快照
sudo btrfs subvolume snapshot /mnt/@snapshots/home-20260510-ro /mnt/@snapshots/home-20260510-rw

📝 注意: 不能直接修改只读快照的属性。需要基于只读快照创建新的可写快照。


4.4 快照回滚

4.4.1 回滚场景

回滚通常用于:

  • 系统更新后出现问题
  • 误删文件或误操作
  • 软件配置变更导致故障
  • 测试环境快速恢复

4.4.2 单子卷回滚

# 场景:@home 出问题,需要回滚到昨天的快照

# 方法 1:删除当前子卷,将快照复制为新子卷
sudo btrfs subvolume delete /mnt/@home
sudo btrfs subvolume snapshot /mnt/@snapshots/home-20260509 /mnt/@home

# 方法 2:如果快照是可写的,直接重命名(不推荐,可能有路径问题)
# 更好的方法是重新挂载

# 方法 3:修改 fstab 中的 subvol 路径
# 指向快照目录即可

4.4.3 根分区回滚(openSUSE/Fedora 方式)

# openSUSE 使用 snapper 工具管理根分区快照
# 如果系统无法启动,可以从 Live CD 回滚

# 1. 从 Live CD 启动
# 2. 挂载 Btrfs
sudo mount /dev/sda2 /mnt

# 3. 重命名当前根子卷
sudo mv /mnt/@ /mnt/@.broken

# 4. 复制快照为新的根子卷
sudo btrfs subvolume snapshot /mnt/@snapshots/2026/snapshot-123 /mnt/@

# 5. 重启

4.4.4 GRUB 集成回滚

某些发行版(如 openSUSE)支持从 GRUB 菜单选择快照启动:

# GRUB 菜单示例
> openSUSE Tumbleweed
> openSUSE Tumbleweed (Snapshot 2026-05-09)
> openSUSE Tumbleweed (Snapshot 2026-05-01)
> openSUSE Tumbleweed (Snapshot 2026-04-25)

配置 GRUB 快照引导(以 openSUSE 为例):

# snapper 已经与 GRUB 集成
# 创建快照后自动出现在 GRUB 菜单
sudo snapper create -d "Before kernel update" -t pre
sudo zypper update
sudo snapper create -d "After kernel update" -t post

4.5 Send/Receive 基础

4.5.1 Send/Receive 概述

btrfs send 将快照的数据以流的方式输出,btrfs receive 接收流并重建快照。这是 Btrfs 备份的核心机制。

源文件系统                           目标文件系统
┌──────────────┐     send stream    ┌──────────────┐
│ @snap-1 (ro) │ ─────────────────→ │ @snap-1      │
│ @snap-2 (ro) │     (增量)         │ @snap-2      │
│ @snap-3 (ro) │ ─────────────────→ │ @snap-3      │
└──────────────┘                    └──────────────┘

4.5.2 全量发送

# 创建只读快照
sudo btrfs subvolume snapshot -r /mnt/@home /mnt/@snapshots/home-full

# 全量发送到另一个 Btrfs 文件系统
sudo btrfs send /mnt/@snapshots/home-full | sudo btrfs receive /backup/

# 发送到文件(用于离线备份)
sudo btrfs send /mnt/@snapshots/home-full > /backup/home-full.send

# 从文件恢复
sudo btrfs receive /mnt/ < /backup/home-full.send

4.5.3 增量发送

# 创建增量快照
sudo btrfs subvolume snapshot -r /mnt/@home /mnt/@snapshots/home-incremental

# 增量发送(只发送差异部分)
sudo btrfs send -p /mnt/@snapshots/home-full /mnt/@snapshots/home-incremental | \
    sudo btrfs receive /backup/

# 增量发送到文件
sudo btrfs send -p /mnt/@snapshots/home-full /mnt/@snapshots/home-incremental > \
    /backup/home-incremental.send

4.5.4 Send 选项

选项说明
-p <parent>指定父快照(增量基准)
-f <file>输出到文件
--compressed-data直接传输压缩数据(不重新压缩)
--no-data只发送元数据变更
--proto <N>协议版本
--limit <N>限制发送速率(bytes/sec)
# 使用压缩数据传输(节省带宽)
sudo btrfs send --compressed-data -p /snap-old /snap-new | \
    sudo btrfs receive /backup/

# 限速传输(避免影响业务)
sudo btrfs send --limit 10485760 -p /snap-old /snap-new | \
    sudo btrfs receive /backup/

4.5.5 远程 Send/Receive

# 通过 SSH 发送
sudo btrfs send -p /mnt/@snapshots/home-old /mnt/@snapshots/home-new | \
    ssh user@backup-server "sudo btrfs receive /backup/"

# 通过 SSH 发送到远程文件
sudo btrfs send /mnt/@snapshots/home-full | \
    ssh user@backup-server "cat > /backup/home-full.send"

# 使用 pv 监控传输进度
sudo btrfs send -p /snap-old /snap-new | \
    pv | ssh user@backup "sudo btrfs receive /backup/"

4.6 自动化快照策略

4.6.1 snapper(推荐)

snapper 是 openSUSE 开发的 Btrfs 快照管理工具:

# 安装
sudo apt install snapper          # Debian/Ubuntu
sudo dnf install snapper          # Fedora
sudo zypper install snapper       # openSUSE

# 创建 snapper 配置
sudo snapper -c home create-config /home

# 编辑配置
sudo vim /etc/snapper/configs/home

snapper 配置示例:

# /etc/snapper/configs/home
SUBVOLUME="/home"
FSTYPE="btrfs"
QGROUP=""

# 时间表(TIMELINE_CREATE=yes 时自动创建)
TIMELINE_CREATE="yes"
TIMELINE_LIMIT_HOURLY="10"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="4"
TIMELINE_LIMIT_MONTHLY="6"
TIMELINE_LIMIT_YEARLY="2"

# 清理算法
EMPTY_PRE_POST_CLEANUP="yes"
EMPTY_PRE_POST_MIN_AGE="1800"

# 快照类型
NUMBER_CLEANUP="yes"
NUMBER_LIMIT="50"
NUMBER_LIMIT_IMPORTANT="10"
# 手动创建快照
sudo snapper -c home create -d "Before upgrade" -t pre
# ... 执行操作 ...
sudo snapper -c home create -d "After upgrade" -t post

# 列出快照
sudo snapper -c home list

# 回滚
sudo snapper -c home undochange 1..2

# 删除快照
sudo snapper -c home delete 1

# 清理旧快照
sudo snapper -c home cleanup number
sudo snapper -c home cleanup timeline

4.6.2 自定义快照脚本

#!/bin/bash
# btrfs-snapshot-manager.sh - Btrfs 快照管理脚本
set -euo pipefail

# 配置
SNAPSHOT_DIR="/mnt/@snapshots"
MAX_HOURLY=24
MAX_DAILY=7
MAX_WEEKLY=4
MAX_MONTHLY=12
SUBVOLUMES=("/mnt/@home" "/mnt/@var")

# 日志
LOG_FILE="/var/log/btrfs-snapshots.log"
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# 创建快照
create_snapshot() {
    local subvol="$1"
    local name=$(basename "$subvol")
    local timestamp=$(date '+%Y%m%d-%H%M%S')
    local snap_name="${name}-${timestamp}"
    local snap_path="${SNAPSHOT_DIR}/${snap_name}"

    log "Creating snapshot: $snap_name"
    if sudo btrfs subvolume snapshot -r "$subvol" "$snap_path" 2>/dev/null; then
        log "Snapshot created successfully: $snap_name"
    else
        log "ERROR: Failed to create snapshot: $snap_name"
        return 1
    fi
}

# 清理旧快照
cleanup_old_snapshots() {
    local subvol_name="$1"
    local max_count="$2"
    local pattern="${subvol_name}-*"

    # 获取所有快照并按时间排序
    local snapshots=($(sudo btrfs subvolume list -s "$SNAPSHOT_DIR/.." | \
        grep "path ${SNAPSHOT_DIR##*/}/${subvol_name}-" | \
        sort -k2 -n | awk '{print $NF}'))

    local count=${#snapshots[@]}
    if (( count > max_count )); then
        local to_delete=$(( count - max_count ))
        log "Cleaning up $to_delete old snapshots for $subvol_name"
        for (( i=0; i<to_delete; i++ )); do
            local snap="${snapshots[$i]}"
            log "Deleting old snapshot: $snap"
            sudo btrfs subvolume delete "/mnt/$snap" 2>/dev/null || true
        done
    fi
}

# 主流程
main() {
    log "=== Starting Btrfs snapshot management ==="

    for subvol in "${SUBVOLUMES[@]}"; do
        if [[ -d "$subvol" ]]; then
            create_snapshot "$subvol"
            local name=$(basename "$subvol")
            cleanup_old_snapshots "$name" "$MAX_DAILY"
        else
            log "WARNING: Subvolume not found: $subvol"
        fi
    done

    log "=== Snapshot management completed ==="
}

main "$@"

4.6.3 配置 cron 定时快照

# 每小时创建快照
0 * * * * /usr/local/bin/btrfs-snapshot-manager.sh

# 每天凌晨 2 点创建每日快照
0 2 * * * /usr/local/bin/btrfs-snapshot-manager.sh --daily

# 每周日凌晨 3 点创建每周快照
0 3 * * 0 /usr/local/bin/btrfs-snapshot-manager.sh --weekly

4.6.4 systemd 定时器

# /etc/systemd/system/btrfs-snapshot.timer
[Unit]
Description=Btrfs Snapshot Timer

[Timer]
OnCalendar=*-*-* *:00:00
Persistent=true

[Install]
WantedBy=timers.target
# /etc/systemd/system/btrfs-snapshot.service
[Unit]
Description=Btrfs Snapshot Service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/btrfs-snapshot-manager.sh
# 启用定时器
sudo systemctl enable --now btrfs-snapshot.timer

# 查看定时器状态
sudo systemctl status btrfs-snapshot.timer
sudo systemctl list-timers btrfs-snapshot.timer

4.7 快照策略建议

4.7.1 桌面系统策略

快照类型保留数量说明
系统更新前最近 5 个系统更新前自动创建
手动创建不限用户手动管理
每日快照7 天每天凌晨自动创建
每周快照4 周每周日自动创建

4.7.2 服务器策略

快照类型保留数量说明
每小时24业务数据频繁变更
每日30保留一个月
每周12保留 3 个月
每月12保留一年
配置变更前最近 20 个运维操作前手动创建

4.7.3 快照清理策略

# snapper 清理算法
# number    - 保留指定数量
# timeline  - 按时间线保留
# empty-pre-post - 清理空的 pre/post 快照对

# 手动清理
sudo snapper -c config cleanup number
sudo snapper -c config cleanup timeline

# 自定义清理脚本
#!/bin/bash
# 清理超过 30 天的快照
CUTOFF=$(date -d "30 days ago" +%s)
sudo btrfs subvolume list -s /mnt | while read -r line; do
    snap=$(echo "$line" | awk '{print $NF}')
    # 从快照名中提取日期
    date_str=$(echo "$snap" | grep -oP '\d{8}')
    if [[ -n "$date_str" ]]; then
        snap_ts=$(date -d "$date_str" +%s 2>/dev/null)
        if (( snap_ts < CUTOFF )); then
            echo "Deleting old snapshot: $snap"
            sudo btrfs subvolume delete "/mnt/$snap"
        fi
    fi
done

4.8 本章小结

操作命令
创建可写快照btrfs subvolume snapshot /src /dst
创建只读快照btrfs subvolume snapshot -r /src /dst
删除快照btrfs subvolume delete /path
列出快照btrfs subvolume list -s /mnt
全量发送btrfs send /snap
增量发送btrfs send -p /parent /snap
接收快照btrfs receive /backup/

关键要点

  1. 快照利用 COW 实现瞬间创建,几乎不占用空间
  2. 只读快照是 Send/Receive 的基础
  3. 快照不递归包含嵌套子卷
  4. 建议使用 snapper 或自定义脚本管理快照生命周期
  5. 回滚操作需要先卸载子卷或重启

扩展阅读