云服务器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如何支撑你恢复数据。