备份为什么要验证恢复:从文件到可用数据
卷中目录
备份任务成功,只说明文件被写过
定时任务返回成功、磁盘上出现一个新文件、监控面板保持绿色,这些都只证明写入过程没有报错。它们不证明这份文件能变回可用的数据。文件可能是空的,因为导出命令鉴权失败,脚本却没有检查退出码,仍然把空输出重定向成了文件。文件可能只包含一部分表,因为导出账号少了权限,命令仍以成功结束。文件可能在复制过程中被写了一半,数据库当时还在提交。文件也可能完整,但属于已经损坏的库,备份只是把损坏又存了一份。

验证恢复就是把备份放回一套隔离环境,让数据库引擎接受它,让应用用匹配的版本打开它,再按业务不变量核对内容。中间任何一步失败,这份备份都不能算数。没有做过这一次的备份,保留的是希望,不是恢复能力。
先写下两个时间,再谈工具
恢复点目标描述能接受丢失多长时间内的新数据。恢复时间目标描述从决定恢复到数据重新可用,允许花多久。这两个数如果只写在制度里,没有被演练量过,就还是口号。演练要记录两个实际观察。备份里最后一条关键记录的时间,与模拟故障时刻之间的差距,是这次真正达到的恢复点。从开始恢复到完成下面要说的可用性检查,经过的时间是这次真正达到的恢复时间。实际数字比目标差,就调整备份频率、日志保留或恢复步骤,而不是把目标改得更松来迁就没有测过的流程。
目标还决定备份类型。只靠每天一次的逻辑导出,恢复点就是两次导出之间的间隔,间隔内的提交默认会丢。若业务要求更短的间隔,就要连续的日志:预写日志、二进制日志或存储层可以一致回溯的快照。有了日志,还要验证日志链没有断。缺一段日志时,文件看起来都在,恢复会停在缺口之前,实际恢复点比预期更早。这个缺口只有把链应用到恢复环境时才看得见。
文件层:存在、大小、校验和、能否解密
归档层的检查便宜,适合每次备份都做,但不能代替恢复。检查对象是否存在,大小是否落在近期的合理区间。突然变成零,或比昨天小一个数量级,应告警,即使命令返回成功。对归档计算校验和,与备份结束时写下的校验和对比,发现传输和存储过程中的损坏。
加密备份要在隔离环境里用当时的密钥解开。密钥若只存在于产生备份的那台机器上,机器丢失时备份无法解开。密钥轮换之后,旧备份仍要用旧密钥。演练应包含解开一份当前密钥的备份,和一份轮换前的备份。只解开最新一份,发现不了旧密钥已经丢了。
对象存储上的生命周期和版本同样要查。生命周期可能在保留期之前删除对象。版本功能若被关闭,一次错误的覆盖会替换掉唯一的好副本。验证时指定要恢复的版本,不要默认取当前键下面的最新对象,因为最新对象可能正是那次错误覆盖。
这些检查通过,只说明字节还在、能够解密。字节是不是一份一致的数据库,要交给引擎。
一致性:文件能复制,不等于库能打开
复制正在使用的数据库文件,可能复制到写了一半的页。看起来文件存在,打开时引擎报损坏,或更糟,打开后默默读到半新半旧的行。不同引擎有各自的一致备份方式,不能用普通文件复制代替。
以 SQLite 为例。预写日志模式开启时,近期提交可能还在日志文件里,主文件并不是最新的全部内容。只复制主文件,会丢掉仍在日志里的事务。正确的做法是使用引擎自带的在线备份接口,或在确认没有写入者时,把主文件与日志及相关的共享内存文件一起复制。恢复之后运行完整性检查,不要只尝试打开连接。完整性检查通过,说明页结构和索引在引擎看来是连贯的。它不说明这是最新的业务数据,所以还要接着做业务核对。轻量的快速检查可以放进每次备份,完整的完整性检查放进定期演练,因为后者更慢。
外键检查也要单独做。完整性检查不一定会按业务规则把外键走一遍。若应用依赖外键,恢复后的库应跑外键检查,并记下违规行。违规若在备份前就存在,说明源库已经坏了,备份策略还要加上对源库的定期检查,否则每次都在忠实地备份损坏。
以 PostgreSQL 为例。逻辑导出在一个一致的视图里取出数据,适合跨版本迁移和抽表,但角色、权限、大对象和部分集群级对象可能需要另外的导出。恢复后要核对序列是否跟着数据前进。序列若落在已有主键之后,应用一写入就会主键冲突,这是逻辑恢复里很常见的可用性问题,只看表行数发现不了。物理基础备份必须带上覆盖备份开始到结束的日志。恢复到空的数据目录,使用与备份匹配的主版本。版本不符时,不要指望新版本的程序直接打开旧的数据目录。恢复完成的标志是引擎声明一致并接受连接,而不是复制命令结束。
操作系统的排序规则变化时,物理恢复可能打开成功,索引顺序却和当前规则不一致,范围查询和唯一性判断会错。演练环境若使用与生产不同的系统库版本,应包含一条已知顺序的查询,或在版本差异存在时重建索引后再做业务核对。只看到连接成功就通过,会把这类问题留到切回生产之后。
应用层:库能开,还不等于产品能用
应用有自己的版本、迁移和配置。备份出来的库结构若比即将启动的程序更新,旧程序可能拒绝迁移或写坏新列。结构若更旧,程序启动时会执行迁移。演练要使用与这份备份匹配的应用版本,再按真实事故里会做的那样升级到目标版本,观察迁移是否成功。跳过这一步,只在数据库客户端里数行,看不到迁移脚本会不会失败。
恢复出的环境必须与生产隔离。同一份配置若指向生产的支付、短信、对象存储或邮件,演练中的应用会向真实用户发信、向真实接口写入,或把测试时产生的对象写进生产桶。隔离包括网络、密钥和回调地址。演练用的密钥是测试密钥。任务队列若会自动执行,先确认它不会连上生产。这一点要写进恢复步骤的前面,而不是等演练真的发出真实通知之后再补。
然后做可用性检查,而不是只看进程是否监听端口。用一份事先记下的抽样核对:某篇已知稿件的标题、状态、更新时间是否与备份时刻一致;属于该稿件的媒体是否能按记录中的地址读出;用户能否登录演练账号并打开这篇稿件。再写一条可以丢弃的演练记录,确认序列、约束和存储都接受写入,然后删除这条记录,确认删除也生效。读写都做,是因为只读成功仍可能在写入时碰到序列和权限问题。
媒体文件和数据库要来自同一时刻。只恢复数据库、沿用现网的对象桶,可能读到比数据库更新的文件,或数据库引用了桶里已经没有的键。只恢复文件、不恢复数据库,应用会指向不存在的行。演练时把两类备份按同一恢复点配对,列出孤儿文件和缺失对象的数量。数量突然变大,说明备份时刻没有对齐,或生命周期提前删掉了对象。
一次演练要留下的记录
每次演练写成同一张简短记录:备份标识、备份所代表的时间、使用的应用版本、恢复开始与结束、完整性检查的结果、业务抽样的结果、写入演练记录是否成功、与目标恢复点及恢复时间的差距、失败时停在哪一步。失败的演练比成功的更要保留。只保留成功记录,流程会看起来一直可用,卡住的那一步却没有被修。
步骤本身应可重复,不依赖某个记得口令放在哪的人。命令、环境变量、密钥所在的保管处、隔离方式、核对用的查询,都写在运行手册里。演练按手册做,手册与实际不符时先改手册再算通过。口头补上的一步,下一次事故里会消失。
频率按数据变化和目标来。每次备份都做校验和与大小检查。完整恢复演练的间隔应短到能在人们忘记步骤之前再做一次,并且在重大升级、存储迁移、密钥轮换之后额外做。只在年初演练一次,到真正需要时,版本、路径和网络策略都已经变了。
看起来通过、实际上没有验证的几种情况
导出的是已经停机的副本,副本本身落后生产却没有被发现。监控只检查任务是否启动,不检查退出码。恢复到与生产同一块磁盘,磁盘故障时备份和原件一起消失,演练却从未在另一份存储上做过。核对只列出表名,不查询关键表。用生产应用的热连接去读备份文件,读的仍是生产。演练账号的权限过大,绕过了应用真实会走的访问控制,于是登录检查通过了,普通用户的路径没有被验证。
还有一种是备份窗口内应用仍在写入,而导出方式不提供一致视图。结果是父记录存在、子记录缺失,单表行数都正常。业务核对要包含这种跨表关系:稿件指向的作者存在,状态历史的最新一条与稿件当前状态一致,媒体行指向的稿件存在。关系查询比单表计数更能看见撕裂。
把验证收成每次都能跑的分层
层与层不要互相替代。每次备份:退出码、非空、大小区间、校验和、密钥标识是否仍可取得。每次备份之后的短检查:用一致备份方式确认源库快速完整性,避免持续备份一个已经坏掉的源。定期演练:在隔离环境恢复到引擎接受连接,跑完整完整性检查和外键或关系查询,启动匹配版本的应用,完成抽样读取和一次写入,记录实际恢复点和恢复时间。重大变更之后:加做密钥轮换、跨版本迁移和日志链缺口的演练。
任何一层失败都让这份备份不能当作可恢复。上层的成功也不能回填下层的失败。应用抽样通过,不代表另一份没有被抽到的备份可以解密。文件校验通过,不代表序列能继续写入。
恢复验证是备份流程的一部分,不是额外的安心项目。从文件到可用数据,中间经过解密、一致的引擎页、匹配的应用版本、对齐的媒体、隔离的配置,以及一条用户真正走得通的读写路径。只停在文件存在,事故发生时才初次发现中间缺了哪一步,而那时候已经没有时间补做演练。
见字如晤