强曰为道

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

第 7 章:快照管理

第 7 章:快照管理

时间机器:让文件系统拥有"后悔药"


7.1 快照基础

7.1.1 快照原理

Bcachefs 快照是 CoW 的自然延伸:

创建前:
  ┌─────────────────────────────────────────┐
  │ B-Tree: /data/file.txt → Block A        │
  │ 引用计数: Block A = 1                    │
  └─────────────────────────────────────────┘

创建快照后:
  ┌─────────────────────────────────────────┐
  │ B-Tree: /data/file.txt → Block A        │
  │ B-Tree: /snap/file.txt → Block A        │
  │ 引用计数: Block A = 2                    │
  │ 空间占用: 0(共享数据块)                 │
  └─────────────────────────────────────────┘

修改 /data/file.txt 后:
  ┌─────────────────────────────────────────┐
  │ B-Tree: /data/file.txt → Block B (新)    │
  │ B-Tree: /snap/file.txt → Block A (旧)    │
  │ 引用计数: Block A = 1, Block B = 1       │
  │ 空间占用: 新数据的大小                    │
  └─────────────────────────────────────────┘

7.1.2 快照 vs 备份

维度快照传统备份
创建速度毫秒级(O(1))分钟-小时级
空间开销几乎为零(初始)完整副本
数据位置同一文件系统不同设备/位置
灾难恢复不能抵御设备故障可以
性能影响极小备份时影响 I/O
恢复速度即时取决于备份大小

7.1.3 快照类型

Bcachefs 快照类型:

1. 子卷快照 (Subvolume Snapshot)
   - 对整个子卷创建快照
   - 最常用的方式

2. 文件系统快照 (Filesystem Snapshot)
   - 对整个文件系统创建快照
   - 需要文件系统级的保护

3. 只读快照 (Read-only Snapshot)
   - 不可修改
   - 推荐用于备份

4. 可写快照 (Writable Snapshot)
   - 可以修改
   - 适用于测试/开发

7.2 创建快照

7.2.1 创建只读快照

# 创建只读快照(推荐)
sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/snap-$(date +%Y%m%d)

# 查看快照
ls -la /mnt/snapshots/
# 快照看起来像普通目录,但实际是只读的

7.2.2 创建可写快照

# 创建可写快照(可以修改)
sudo bcachefs subvolume snapshot /mnt/data /mnt/snapshots/writable-snap

# 可以直接修改快照中的文件
echo "修改" > /mnt/snapshots/writable-snap/test.txt

7.2.3 对子卷创建快照

# 先创建子卷
sudo bcachefs subvolume create /mnt/data/projects

# 对子卷创建快照
sudo bcachefs subvolume snapshot -r /mnt/data/projects /mnt/snapshots/projects-$(date +%Y%m%d)

# 查看子卷列表和快照
sudo bcachefs subvolume list /mnt/data

7.2.4 带标签的快照

# 使用时间戳作为快照名
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/snap-${TIMESTAMP}

# 使用描述性名称
sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/before-upgrade
sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/after-upgrade

7.2.5 创建快照的最佳实践

#!/bin/bash
# create-snapshot.sh

set -e

MOUNT="/mnt/data"
SNAPSHOT_DIR="/mnt/snapshots"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
SNAPSHOT_NAME="snap-${TIMESTAMP}"

echo "=== 创建快照 ==="
echo "源: $MOUNT"
echo "快照: $SNAPSHOT_DIR/$SNAPSHOT_NAME"
echo ""

# 1. 刷新文件系统缓冲
sync

# 2. 如果是数据库,刷新数据库缓冲
# 例如 PostgreSQL:
# psql -c "CHECKPOINT"
# 例如 MySQL:
# mysql -e "FLUSH TABLES WITH READ LOCK;"

# 3. 创建只读快照
sudo bcachefs subvolume snapshot -r "$MOUNT" "$SNAPSHOT_DIR/$SNAPSHOT_NAME"

# 4. 验证快照
if [ -d "$SNAPSHOT_DIR/$SNAPSHOT_NAME" ]; then
    echo "✅ 快照创建成功"
    ls -la "$SNAPSHOT_DIR/$SNAPSHOT_NAME"
else
    echo "❌ 快照创建失败"
    exit 1
fi

7.3 管理快照

7.3.1 列出快照

# 列出所有子卷(包括快照)
sudo bcachefs subvolume list /mnt/data

# 输出示例:
# ID 256: /projects
# ID 257: /snapshots/snap-20250101
# ID 258: /snapshots/snap-20250102
# ID 259: /snapshots/snap-20250103

7.3.2 查看快照信息

# 查看快照占用的空间
sudo bcachefs fs usage /mnt/data

# 比较快照与当前数据的差异
diff -rq /mnt/data/ /mnt/snapshots/snap-20250101/ 2>/dev/null | head -20

# 统计快照中文件数量
find /mnt/snapshots/snap-20250101 -type f | wc -l

7.3.3 删除快照

# 删除单个快照
sudo bcachefs subvolume delete /mnt/snapshots/snap-20250101

# 批量删除旧快照
for snap in /mnt/snapshots/snap-2024*; do
    echo "删除: $snap"
    sudo bcachefs subvolume delete "$snap"
done

# 删除所有快照(谨慎操作!)
sudo bcachefs subvolume delete --recursive /mnt/snapshots/

7.3.4 重命名快照

# Bcachefs 不直接支持重命名子卷/快照
# 需要通过复制实现

# 创建新名称的快照
sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/new-name

# 删除旧快照
sudo bcachefs subvolume delete /mnt/snapshots/old-name

7.4 从快照恢复

7.4.1 直接复制恢复

# 从快照中恢复单个文件
cp /mnt/snapshots/snap-20250101/important.txt /mnt/data/

# 恢复整个目录
cp -a /mnt/snapshots/snap-20250101/projects/ /mnt/data/projects-restored/

# 使用 rsync 恢复(保留权限)
sudo rsync -av /mnt/snapshots/snap-20250101/ /mnt/data/

7.4.2 完整恢复流程

#!/bin/bash
# restore-from-snapshot.sh

set -e

SNAPSHOT="/mnt/snapshots/snap-20250101"
TARGET="/mnt/data"

echo "=== 从快照恢复 ==="
echo "快照: $SNAPSHOT"
echo "目标: $TARGET"
echo ""

# 确认
read -p "这会覆盖当前数据!确认继续?(y/N) " confirm
if [ "$confirm" != "y" ]; then
    echo "取消操作"
    exit 0
fi

# 1. 创建当前状态的快照(以防万一)
echo "创建当前状态快照..."
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
sudo bcachefs subvolume snapshot -r "$TARGET" "/mnt/snapshots/before-restore-${TIMESTAMP}"

# 2. 清空当前数据
echo "清空当前数据..."
sudo rm -rf "${TARGET:?}"/*

# 3. 从快照恢复
echo "从快照恢复..."
sudo rsync -av "$SNAPSHOT/" "$TARGET/"

# 4. 验证
echo ""
echo "=== 恢复完成 ==="
echo "已恢复的文件数量:"
find "$TARGET" -type f | wc -l

7.4.3 部分恢复

# 只恢复特定文件
cp /mnt/snapshots/snap-20250101/documents/report.pdf /mnt/data/documents/

# 只恢复特定类型的文件
find /mnt/snapshots/snap-20250101 -name "*.conf" -exec cp {} /mnt/data/config/ \;

# 恢复到不同位置
cp -r /mnt/snapshots/snap-20250101/projects/ /mnt/data/old-projects/

7.5 自动化快照策略

7.5.1 定时快照脚本

#!/bin/bash
# auto-snapshot.sh

set -e

MOUNT="/mnt/data"
SNAPSHOT_DIR="/mnt/snapshots"
DAILY_KEEP=7
WEEKLY_KEEP=4
MONTHLY_KEEP=6

TIMESTAMP=$(date +%Y%m%d-%H%M%S)
DAY_OF_WEEK=$(date +%u)
DAY_OF_MONTH=$(date +%d)

# 创建每日快照
create_daily() {
    local name="daily-${TIMESTAMP}"
    echo "创建每日快照: $name"
    sudo bcachefs subvolume snapshot -r "$MOUNT" "$SNAPSHOT_DIR/$name"
}

# 创建每周快照
create_weekly() {
    if [ "$DAY_OF_WEEK" = "7" ]; then  # 周日
        local name="weekly-${TIMESTAMP}"
        echo "创建每周快照: $name"
        sudo bcachefs subvolume snapshot -r "$MOUNT" "$SNAPSHOT_DIR/$name"
    fi
}

# 创建每月快照
create_monthly() {
    if [ "$DAY_OF_MONTH" = "01" ]; then  # 每月 1 号
        local name="monthly-${TIMESTAMP}"
        echo "创建每月快照: $name"
        sudo bcachefs subvolume snapshot -r "$MOUNT" "$SNAPSHOT_DIR/$name"
    fi
}

# 清理旧快照
cleanup_old() {
    echo "清理旧快照..."
    
    # 保留最近 N 个每日快照
    ls -dt "$SNAPSHOT_DIR"/daily-* 2>/dev/null | tail -n +$((DAILY_KEEP+1)) | while read snap; do
        echo "删除: $snap"
        sudo bcachefs subvolume delete "$snap"
    done
    
    # 保留最近 N 个每周快照
    ls -dt "$SNAPSHOT_DIR"/weekly-* 2>/dev/null | tail -n +$((WEEKLY_KEEP+1)) | while read snap; do
        echo "删除: $snap"
        sudo bcachefs subvolume delete "$snap"
    done
    
    # 保留最近 N 个月度快照
    ls -dt "$SNAPSHOT_DIR"/monthly-* 2>/dev/null | tail -n +$((MONTHLY_KEEP+1)) | while read snap; do
        echo "删除: $snap"
        sudo bcachefs subvolume delete "$snap"
    done
}

# 执行
echo "=== 自动快照 $(date) ==="
create_daily
create_weekly
create_monthly
cleanup_old

echo ""
echo "=== 完成 ==="
echo "当前快照数量:"
ls "$SNAPSHOT_DIR" | wc -l

7.5.2 配置 Crontab

# 编辑 crontab
sudo crontab -e

# 添加以下行:
# 每天凌晨 2 点创建快照
0 2 * * * /usr/local/bin/auto-snapshot.sh >> /var/log/snapshot.log 2>&1

# 每 6 小时创建快照(适合频繁变化的数据)
0 */6 * * * /usr/local/bin/auto-snapshot.sh >> /var/log/snapshot.log 2>&1

7.5.3 Systemd Timer 配置

# /etc/systemd/system/bcachefs-snapshot.service
[Unit]
Description=Bcachefs automatic snapshot
After=local-fs.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/auto-snapshot.sh
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/bcachefs-snapshot.timer
[Unit]
Description=Run Bcachefs snapshot every 6 hours

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

[Install]
WantedBy=timers.target
# 启用定时器
sudo systemctl enable --now bcachefs-snapshot.timer

# 检查状态
sudo systemctl status bcachefs-snapshot.timer
sudo journalctl -u bcachefs-snapshot.service

7.6 快照与数据库一致性

7.6.1 数据库一致性挑战

问题: 数据库可能有脏页在内存中
  ┌─────────────────────────────────────────┐
  │ 内存中的脏页: [A', B', C']              │
  │ 磁盘上的数据: [A, B, C]                 │
  │                                         │
  │ 如果直接创建快照:                        │
  │   快照包含: [A, B, C]                   │
  │   实际应该是: [A', B', C']              │
  │   → 不一致!                            │
  └─────────────────────────────────────────┘

解决方案: 创建快照前刷新数据库缓冲

7.6.2 PostgreSQL

#!/bin/bash
# snapshot-postgres.sh

# 1. 暂停写入(可选)
psql -c "SELECT pg_start_backup('bcachefs-snapshot', true);"

# 2. 创建快照
sudo bcachefs subvolume snapshot -r /var/lib/postgresql /mnt/snapshots/pg-$(date +%Y%m%d)

# 3. 恢复写入
psql -c "SELECT pg_stop_backup();"

7.6.3 MySQL / MariaDB

#!/bin/bash
# snapshot-mysql.sh

# 1. 刷新并锁定表
mysql -e "FLUSH TABLES WITH READ LOCK;"

# 2. 创建快照
sudo bcachefs subvolume snapshot -r /var/lib/mysql /mnt/snapshots/mysql-$(date +%Y%m%d)

# 3. 解锁
mysql -e "UNLOCK TABLES;"

7.6.4 SQLite

#!/bin/bash
# snapshot-sqlite.sh

# SQLite 使用文件锁,只需确保没有写入
# 1. 检查是否有写入进程
lsof /path/to/database.db

# 2. 创建快照
sudo bcachefs subvolume snapshot -r /path/to/data /mnt/snapshots/sqlite-$(date +%Y%m%d)

7.7 快照监控

7.7.1 监控快照数量

#!/bin/bash
# monitor-snapshots.sh

SNAPSHOT_DIR="/mnt/snapshots"
MAX_SNAPSHOTS=50

count=$(ls "$SNAPSHOT_DIR" | wc -l)

echo "当前快照数量: $count"

if [ "$count" -gt "$MAX_SNAPSHOTS" ]; then
    echo "⚠️  警告: 快照数量超过阈值 ($MAX_SNAPSHOTS)"
    # 发送告警
fi

7.7.2 监控快照空间

#!/bin/bash
# monitor-snapshot-space.sh

MOUNT="/mnt/data"

# 查看文件系统使用情况
sudo bcachefs fs usage "$MOUNT"

# 快照本身不占用额外空间(初始时)
# 但修改后的 CoW 数据会占用空间

7.7.3 Prometheus 指标

# 如果使用 node_exporter,可以自定义指标
# /etc/node_exporter/textfile_collector/bcachefs_snapshots.prom

#!/bin/bash
SNAPSHOT_COUNT=$(ls /mnt/snapshots | wc -l)
echo "bcachefs_snapshot_count $SNAPSHOT_COUNT"

7.8 快照性能影响

7.8.1 创建性能

# 快照创建是 O(1) 操作,几乎不消耗时间
time sudo bcachefs subvolume snapshot -r /mnt/data /mnt/snapshots/test-snap
# 通常 < 0.01 秒

7.8.2 写入性能影响

大量快照时的写入开销:

1 个快照:
  写入 Block A → 只需更新 1 个引用 → 开销小

N 个快照:
  写入 Block A → 需要检查 N 个引用 → 开销增加

建议:
  - 保留不超过 50-100 个快照
  - 定期清理旧快照

7.8.3 读取性能影响

# 快照不影响正常读取性能
# 读取快照文件与读取普通文件性能相同

# 但大量快照可能影响元数据查询
# 建议使用子卷隔离快照

7.9 快照与子卷的关系

7.9.1 使用子卷组织快照

# 创建数据子卷
sudo bcachefs subvolume create /mnt/data/documents
sudo bcachefs subvolume create /mnt/data/projects

# 为每个子卷创建独立的快照
sudo bcachefs subvolume snapshot -r /mnt/data/documents /mnt/snapshots/docs-$(date +%Y%m%d)
sudo bcachefs subvolume snapshot -r /mnt/data/projects /mnt/snapshots/proj-$(date +%Y%m%d)

# 优势: 可以独立管理不同数据集的快照

7.9.2 子卷快照 vs 根快照

子卷快照 (推荐):
  /mnt/data/
  ├── documents/ (子卷)
  │   └── ...数据...
  └── projects/ (子卷)
      └── ...数据...

  快照:
  /mnt/snapshots/
  ├── docs-20250101/    ← 只包含 documents
  └── proj-20250101/    ← 只包含 projects

根快照:
  对整个 /mnt/data 创建快照
  包含所有子卷 → 更大,管理更复杂

7.10 快照灾难恢复

7.10.1 快照不能替代备份

快照在同一文件系统上:
  设备故障 → 快照也丢失!

正确的备份策略:
  快照(本地保护)+ 远程备份(灾难恢复)

推荐架构:
  ┌─────────────────────────────────────────┐
  │ 本地存储                                │
  │ ├── 数据                                │
  │ ├── 每日快照                            │
  │ └── 周/月快照                           │
  ├─────────────────────────────────────────┤
  │ 远程备份                                │
  │ ├── rsync 到远程服务器                   │
  │ ├── 磁带备份                            │
  │ └── 云存储                              │
  └─────────────────────────────────────────┘

7.10.2 从快照创建远程备份

#!/bin/bash
# snapshot-to-remote.sh

SNAPSHOT_DIR="/mnt/snapshots"
REMOTE_HOST="backup-server"
REMOTE_PATH="/backup/data"

# 获取最新快照
LATEST_SNAP=$(ls -dt "$SNAPSHOT_DIR"/daily-* | head -1)

echo "同步快照到远程: $LATEST_SNAP"

# 使用 rsync 同步
rsync -avz --delete \
    "$LATEST_SNAP/" \
    "${REMOTE_HOST}:${REMOTE_PATH}/"

echo "同步完成"

7.11 本章小结

操作命令说明
创建只读快照bcachefs subvolume snapshot -r src dst推荐方式
创建可写快照bcachefs subvolume snapshot src dst可修改
列出快照bcachefs subvolume list /mnt查看所有子卷/快照
删除快照bcachefs subvolume delete /path释放空间
恢复数据cp /snap/file /data/直接复制

最佳实践:

  1. 使用只读快照保护数据
  2. 通过子卷隔离不同数据集
  3. 自动化快照策略
  4. 定期清理旧快照
  5. 快照不能替代远程备份

扩展阅读


上一章: ← 第 6 章:多设备与分层存储 | 下一章: 第 8 章:压缩策略 →