云服务器Btrfs增量备份深潜:从CoW元组到家目录恢复——拒绝rsync全量,链式快照玩到极致

写在最前:为什么rsync不是备份的终点

大部分运维还在用 rsync -avz 做全量拷贝,遇到100G+数据时,哪怕只有1%修改,也要完整扫描inode和file hash。真正的增量备份需要文件系统层的写时复制(CoW)支持。今天扒开Btrfs(写时复制文件系统)的裤衩,从元组(metadata block)的CRC校验到快照差分传输,手写一套秒级增量备份方案。顺便透露,轻云互联的高IO云服务器默认启用Btrfs,直接在块设备层面做了调优,省了Docker存储驱动的二次损耗。

1. 快照不是拷贝:Btrfs的CoW原子操作

传统快照靠标记block,Btrfs直接使用copy-on-write子树(subvolume)。创建一个只读快照只需O(1)时间:

btrfs subvolume snapshot -r /data /data/.snap/$(date +%s)  
# 该命令不拷贝数据,只复制根inode指针(约1K元数据)  
# 后续对/data的任何写入,旧数据块被保留在快照中,新数据写入新block

底层真相:Btrfs的元数据树(extent tree + fs tree)采用COW B-tree,快照只是复制了树根节点的引用(generation+objectid)。写时产生的新节点会被打上前世代diff标记,通过btrfs subvolume show可查看generation级联关系。

2. 增量备份的真正武器:btrfs send/receive

传统的rsync需要比对文件时间戳和大小,而Btrfs通过文件系统级chunk差分直接发送裸块变更。命令如下:

# 创建两个快照作为差分基准
btrfs subvolume snapshot -r /data /backup/base-1  
# 一天后:
btrfs subvolume snapshot -r /data /backup/incr-2  

# 生成增量流(只发送base-1到incr-2的差异数据)
btrfs send -p /backup/base-1 /backup/incr-2 | \
  btrfs receive /remote/backup/  
# 注意:-p参数指定parent,否则发送全量

效率对比:100G数据,连续写入10MB变更,rsync扫描耗时2分钟+传输10MB;btrfs send仅发送变更的extent指针(约200KB元数据)+10MB数据块,网络开销直降两个数量级。

3. 实战:定时增量链式备份 + 自动清理(脚本干货)

写个cron任务,每天凌晨2点创建快照并发送到远程。注意链式结构避免GFS越堆越多:

#!/bin/bash
SNAP_DIR="/.snapshots"
REMOTE_DIR="/mnt/backup"
KEEP=7  # 保留7天

# 创建当天快照(读写快照用于后续增量)  
SNAP_NAME="daily-$(date +%F)"
btrfs subvolume snapshot -r /data $SNAP_DIR/$SNAP_NAME

# 查找前一个快照作为parent  
LAST=$(ls -1t $SNAP_DIR | head -n 2 | tail -n 1)
if [ -n "$LAST" ]; then
  btrfs send -p $SNAP_DIR/$LAST $SNAP_DIR/$SNAP_NAME | \
    btrfs receive $REMOTE_DIR
else
  # 首次全量 
  btrfs send $SNAP_DIR/$SNAP_NAME | btrfs receive $REMOTE_DIR
fi

# 清理旧快照(本地只保留readonly即可,远程不可删)
btrfs subvolume delete $(find $SNAP_DIR -mtime +$KEEP -name "daily-*")

排错点:如果传输中断,远程的接收目录会存在部分写入,必须使用 btrfs subvolume sync 清理残留,否则后续增量会因元数据不一致报错:ERROR: cannot send ... 。解决方案:在receive失败后执行 btrfs subvolume delete $REMOTE_DIR/$SNAP_NAME 再重试。

4. 原理层:文件恢复时如何避免全量拉取

当你想恢复某个文件的旧版本,Btrfs可以在本地快照内直接cp,无需从远程拉全量。利用btrfs的dedupe(去重)重用特性:

# 查看文件在快照内的所有版本(通过extent tree)
btrfs inspect-internal logical-resolve -P /data/app/config.yaml  
# 得到文件在两个快照中的逻辑块地址,可直接cp合并

如果远程恢复,直接 btrfs receive 上次的全量快照,然后 btrfs send -p 增量追上。无需下载全部历史数据——这得益于轻云互联云服务器使用的NVMe SSD搭配Btrfs的checksum校验,即使增量传输几百次,数据一致性仍由文件系统级CRC32C保证。

5. 终极陷阱:跨文件系统send/receive失败与

很多运维把btrfs send用于不同类型文件系统(比如ext4 → btrfs)导致失败。send流只能写入Btrfs目标,且两端扇区大小必须一致。如果你在云服务器上使用LVM + Btrfs,建议mkfs.btrfs -s 4096强制扇区512e,避免因物理扇区差异引发的silent corruption。测试命令:

btrfs filesystem show /data | grep "sector size"
# 若不对等,在接收端重新mkfs为相同扇区

实战血泪:某次上线,源端是512e,目标云主机是4K原生扇区,send流无报错但restore后文件系统元数据校验失败。最后用btrfs check --repair才救回。所以新部署云服务器时,先跑 blockdev --getss /dev/vda 确认扇区。

6. 进阶:对MySQL/git仓库这种随机写入密集型场景

Btrfs的CoW对数据库不友好——频繁小写会导致碎片化。解决方案:在备份前对MySQL使用xtrabackup --parallel生成二进制增量,然后用Btrfs快照锁住一致性点。组合拳:

  • step1: mysql -e "FLUSH TABLES WITH READ LOCK; system btrfs subvolume snapshot -r /data /backup/sql-snap; UNLOCK TABLES;" 记住执行锁表要在同一会话,否则死锁。
  • step2: 对/backup/sql-snap执行btrfs send增量,此快照包含完全一致的binlog位置。
  • step3: 远程接收后,用innobackupex --apply-log --ibbackup=xtrabackup重放redo。

不需要rsync + LVM快照这种二合一脚本,Btrfs原生搞定一切。

总结:备份的本质是元数据增量,不是文件增量

rsync和scp一直是“文件级感知”,而Btrfs send工作在“块级逻辑地址”,天然支持原子一致性。如果你还在用tar包+scp,试试把云服务器的根文件系统格成Btrfs,配合轻云互联业内领先的NVMe IOPS,单条btrfs send命令就可替代一整套备份软件。记住:不要重复造轮子,但必须理解轮子里的B-tree如何支撑你恢复数据。