备份做得很勤,真到数据丢失时恢复却失败,这是运维和DBA最怕遇到的场景。问题的根源大多不在恢复工具,而在备份设计本身:增量备份链条不完整、快照被误删、恢复时的时间点不一致,都会让恢复流程在最后一步崩掉。本文把增量备份和快照这两个最常用的备份手段拆开讲清楚,再讲它们各自的失败场景和正确的配合方式。

先弄清楚:增量备份和快照到底差在哪
增量备份(Incremental Backup)记录的是自上一次备份以来发生变化的数据块或日志。第一次必须是全量备份,之后的每一次增量都基于前一次,形成一条链。恢复时必须从全量开始,按顺序逐个应用增量,链条中任何一个文件损坏或缺失,整条链的恢复就会中断。
快照(Snapshot)则不同,它不是一份拷贝,而是源数据在某个时间点的逻辑视图。主流实现是写时复制(Copy-On-Write):创建快照几乎是瞬间完成的,之后源数据被修改时,系统先把原始数据块复制到快照区域,再执行写入。快照的恢复速度极快,因为它本质上是把指针指回旧数据块,不需要真正复制数据。
两者的核心差异可以总结为:增量备份是离散的、可离线存放的文件序列,依赖链条完整性;快照是依赖源存储设备的运行时视图,依赖源卷存在且快照本身有效。很多恢复失败案例,就是把这个差异搞混导致的。
增量备份恢复失败的四个典型原因
第一个原因是链条断裂。假设你周日做全量,周一到周六每天做增量,那么周六的数据恢复需要全量加六个增量文件。如果周三的增量文件因为磁盘坏道或误删丢失,周四之后的增量就全部失效,因为它们记录的是相对周三的变化。这种失效在备份时不会报错,只有恢复时才会暴露。
第二个原因是备份软件的链式依赖实现不一致。有的工具(如Percona XtraBackup的增量恢复)要求增量必须严格按顺序apply到全量上,顺序错一个就会报日志序列号不匹配;MySQL的binlog恢复则要求起始位点必须精确对齐。下面是一段典型的XtraBackup增量恢复流程:
# 先准备全量备份,只回滚未提交事务,不应用redo之外的内容 xtrabackup --prepare --apply-log-only --target-dir=/backup/full # 按顺序应用每个增量,最后一个增量之前都加apply-log-only xtrabackup --prepare --apply-log-only --target-dir=/backup/full \ --incremental-dir=/backup/inc1 xtrabackup --prepare --apply-log-only --target-dir=/backup/full \ --incremental-dir=/backup/inc2 # 最后一个增量正常prepare xtrabackup --prepare --target-dir=/backup/full \ --incremental-dir=/backup/inc3
第三个原因是校验缺失。备份文件在传输或长期存放后可能悄然损坏,如果备份时没有同时生成校验和,恢复时才发现文件无法解压,为时已晚。第四个原因是恢复目标环境不一致,比如数据库版本不同、页大小不同、字符集配置不同,即使备份文件完好,恢复也可能报错。
快照恢复失败的坑:依赖源卷与生命周期管理
快照最常见的失败是源卷故障。因为COW快照并没有复制完整数据,原始数据块仍然在源卷上,一旦源盘物理损坏,快照跟着一起丢。把快照当成备份,是很多团队踩过的大坑。快照的定位应该是短时间内的快速回滚手段,而不是灾备方案。
第二个坑是快照链过长。每个快照都会让后续写入多一次COW操作,快照堆得越多,写性能下降越明显,极端情况下LVM快照空间耗尽,快照直接失效。Linux下LVM快照空间不足时会变成不可用的悬空设备,此时恢复自然失败。合理的做法是定期删除旧快照,或者执行快照合并(merge),把变更数据写回源卷:
# 合并LVM快照,将其数据写回源逻辑卷 lvconvert --merge /dev/vg0/snap_dbdata # 合并在下次激活卷组时生效,可强制重新激活 vgchange -an vg0 vgchange -ay vg0
第三个坑是一致性问题。对正在写入的数据库直接打快照,拿到的是崩溃一致性的镜像,InnoDB恢复时需要走crash recovery。如果要应用一致性,MySQL场景必须先执行FLUSH TABLES WITH READ LOCK再打快照,或者依赖文件系统级的fsfreeze,否则可能出现事务日志与数据文件不一致,恢复时报redo日志损坏。
正确姿势:快照与增量备份的混合方案
生产环境比较稳妥的做法是把两者组合:用快照提供瞬时的一致性时间点,从快照挂载出的只读视图读取数据,再做增量或全量备份。这样备份过程不锁库、不影响线上写入,同时备份数据真正落到了独立存储上,不依赖源卷。
以LVM加MySQL为例,流程大致是:短暂加全局读锁并记录binlog位点,创建LVM快照,立即解锁,然后挂载快照复制数据。位点信息记录下来,后续可以用binlog做时间点恢复。示意图如下:
mysql -e "FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS;" > /tmp/binlog_pos.txt lvcreate --size 5G --snapshot --name snap_db /dev/vg0/lv_db mysql -e "UNLOCK TABLES;" mount /dev/vg0/snap_db /mnt/snap rsync -a /mnt/snap/data/ /backup/daily_$(date +%F)/ umount /mnt/snap && lvremove -f /dev/vg0/snap_db
除了组合使用,还有几条经验能显著降低恢复失败率。第一,控制增量链条长度,比如每七天做一次全量重建链条,避免链条无限延长;第二,备份时生成SHA256校验和,恢复前先校验;第三,最重要的,定期做真实的恢复演练,在隔离环境把备份真正恢复一遍,验证可用性。备份只有恢复成功过,才算真正的备份。