直接说结论:分布式物理备份的一致性,核心矛盾不在于“能不能备”,而在于“备出来的数据敢不敢用”。物理备份直接拷贝底层数据文件,比如InnoDB的.ibd文件、日志文件、元数据,速度远超逻辑备份,但在分布式数据库里,各个节点的时间戳、事务状态、全局时钟很难天然对齐。如果不解决一致性问题,恢复出来的数据可能是一个“从未存在过的中间态”——某个分片的数据已经提交,另一个分片的数据还卡在未提交状态,这种断层在金融、电商场景里是致命的。

要彻底搞清楚这个问题,得先拆解分布式物理备份面对的三种一致性层级:节点内一致性、节点间一致性和全局事务一致性。节点内一致性相对成熟,单机数据库的崩溃恢复机制,比如redo log和undo log的重放,能保证单个节点在备份时刻的物理文件是内部自洽的。真正棘手的是后两者,尤其是当备份跨越数十甚至上百个物理节点时,如何保证所有节点捕获到的数据镜像对应同一个逻辑时刻。

全局时间戳与快照隔离的工程落地

最硬核的方案是基于全局授时服务生成一致性快照。以TiDB为例,它通过PD(Placement Driver)组件分配全局单调递增的时间戳TSO,备份工具BR(Backup & Restore)在启动时会获取一个全局快照时间戳,然后所有TiKV节点根据这个时间戳执行Raft读或直接读取对应MVCC版本的数据。关键细节在于,BR并不是简单地“咔嚓”一下把所有文件同时拷贝,而是利用MVCC机制,只备份在快照时间戳之前已经提交、且在该时间戳之后不会被垃圾回收的版本。这就要求备份程序必须与GC(Garbage Collection)机制紧密配合,备份期间需要安全地保留历史版本,防止MVCC旧版本被清理导致备份失败或数据空洞。

实际执行时,每个TiKV节点会扫描本地SST文件中的键值对,过滤掉时间戳大于快照时间戳的数据,输出一致的SST文件集合。这里有一个容易踩坑的地方:Region分裂和合并。备份过程中如果发生Region调度,备份工具必须能够处理“同一个Region在不同时间点出现在不同节点”的情况,通常通过记录备份元数据并在恢复时进行Region边界对齐来解决。这个过程的代码逻辑大致如下:

// 伪代码:获取一致性备份点
func CreateConsistentBackup(ts uint64) error {
    // 1. 从PD获取全局快照时间戳
    snapshotTS := pdClient.GetTS()
    
    // 2. 暂停或延长GC safepoint
    pdClient.UpdateServiceGCSafePoint("backup", snapshotTS, 3600)
    defer pdClient.DeleteServiceGCSafePoint("backup")
    
    // 3. 遍历所有TiKV节点,下发备份指令
    for _, store := range stores {
        go backupStore(store, snapshotTS)
    }
    
    // 4. 等待所有节点完成,收集SST文件元数据
    return collectBackupMeta()
}

这种方案的优势是恢复后的数据全局一致,严格满足ACID中的隔离性要求,但代价是依赖中心化时间戳服务,跨地域部署时延迟敏感,且备份期间对GC的压力控制需要精细调参。

基于Redo Log的实时捕获与合成

另一种路线不走快照,而是通过持续抓取各个节点的redo log或binlog,在备份端合成一致性的物理映像。这种思路在MySQL分布式集群中很常见,比如基于XtraBackup的变种方案。核心操作是:先对每个节点独立发起物理备份,同时记录备份过程中产生的redo log序列号(LSN),备份完成后,将所有节点的redo log在统一的时间窗口内进行截断和合并。

具体来说,假设一个分布式表按用户ID哈希分片到4个节点,备份工具在每个节点上执行类似xtrabackup --backup的操作,并在备份开始和结束时分别记录LSN区间。关键步骤在于“一致性位点”的确定:需要从全局事务管理器(GTM)或分布式事务协调器中获取一个全局事务ID(GTID)集合,然后每个节点只回放那些GTID在一致性位点之前、且LSN在备份区间内的日志。如果某个分布式事务跨了多个节点,必须确保要么所有参与节点都包含该事务,要么都不包含,否则就会出现“事务半截”的灾难。

这个方案的工程复杂度极高,因为要处理分布式事务的2PC(两阶段提交)状态。假设一个事务在节点A已提交,在节点B处于prepare状态但未提交,备份时如果只根据LSN截断,很容易漏掉B节点上该事务的提交日志。正确的做法是,在恢复阶段引入“事务修复”流程:扫描所有节点备份文件中的prepare状态事务,到其他节点查找对应事务的最终状态,如果找到commit记录则补提交,否则回滚。这本质上是在恢复时重做了一遍分布式事务的决议过程。

共享存储架构下的天然优势与隐藏陷阱

如果分布式数据库本身基于共享存储,比如Oracle RAC或PolarDB,物理备份的一致性似乎简单很多——直接对共享存储做快照就行。但别高兴太早,存储级快照只能保证磁盘数据在某个时间点的物理一致性,不保证内存中未刷盘的事务状态一致。真正的坑在于,共享存储上的数据文件可能包含“脏数据”:已提交但未写入数据文件、或者已写入数据文件但对应的undo信息不完整。

正确的做法是“存储快照+实例崩溃恢复”组合拳。先对共享存储执行一致性快照,这个快照本身是存储层面的原子操作;恢复时,用这个快照拉起一个新的数据库实例,让数据库自身的崩溃恢复机制(比如Oracle的instance recovery或MySQL的redo log apply)去重放日志、回滚未提交事务。这里有一个必须验证的点:快照必须包含完整的online redo log,否则恢复时日志断层,数据库无法达到一致状态。很多运维事故就是因为只快照了数据文件,漏掉了日志卷,导致恢复出来的数据库需要做不完全恢复,丢数据是必然的。

多级备份策略与一致性校验闭环

光有备份不行,必须建立一致性校验的闭环。物理备份最怕静默错误——备份文件本身损坏或者逻辑不一致,但备份进程没有报错。分布式场景下,校验要分三层:首先是文件级校验,对每个SST文件或数据文件计算CRC或MD5,与备份元数据中的记录比对;其次是节点级校验,在单节点恢复后跑一遍InnoDB的checksum检查或等效的存储引擎完整性检查;最后是全局事务级校验,恢复完成后执行一组预定义的一致性查询,比如检查所有分片的行数总和是否与逻辑备份的元数据一致、检查跨分片的外键约束是否完整、检查分布式事务的GTID连续性。

一个值得借鉴的做法是“端到端恢复演练”:定期将备份恢复到独立的验证集群,跑完整的业务回归测试套件。这听起来成本高,但对于核心系统来说,这是唯一能真正验证备份可用的手段。演练中要特别关注“时间旅行查询”——用AS OF TIMESTAMP语法查询恢复后数据库在备份时间点的状态,与生产环境当时的快照读结果对比,任何差异都意味着备份一致性出了问题。

增量备份的一致性挑战与日志序列化

增量物理备份的一致性更难做,因为它不是全量快照,而是基于上一次全量或增量备份的变更集合。分布式环境下,增量备份必须解决“变更边界”问题:每个节点的增量LSN区间必须严格对齐到同一个全局时间点,否则不同节点的增量集合会覆盖不同长度的时间窗口,恢复时数据会错位。

工程上的解法是引入“增量基线时间戳”。每次增量备份开始时,记录一个全局时间戳T_incre,然后每个节点备份自上次基线以来、且在T_incre之前提交的所有变更。这要求备份工具能精确地按时间戳过滤MVCC版本或按LSN区间截断redo log。更复杂的是,如果两次备份之间发生了节点扩容或缩容,数据分布变了,增量备份必须能处理“部分数据迁移到新节点”的情况,通常需要全量备份作为新基线,或者在元数据中记录数据迁移的边界信息,恢复时按边界重组数据。

最后想强调一个容易被忽视的细节:备份元数据本身的一致性。分布式物理备份会产生大量元数据——文件列表、LSN区间、Region分布、时间戳信息等,这些元数据必须原子性地写入可靠存储,并且与备份数据文件严格对应。如果元数据损坏或丢失,即使数据文件完好,恢复也无法进行。建议将备份元数据写入支持事务的分布式存储,或者至少写入多个副本,并在备份完成后立即做一次元数据的完整性校验。

分布式物理备份一致性没有银弹,它是对数据库内核机制、分布式协调能力和存储特性的综合考验。选型时不要只看备份速度,要问清楚:恢复出来的数据,敢不敢直接承接生产流量?如果答案不是100%肯定,那备份策略就需要重新审视。