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/ |
关键要点:
- 快照利用 COW 实现瞬间创建,几乎不占用空间
- 只读快照是 Send/Receive 的基础
- 快照不递归包含嵌套子卷
- 建议使用 snapper 或自定义脚本管理快照生命周期
- 回滚操作需要先卸载子卷或重启