首页
/ 深入解析 TiDB BR 备份恢复设计:KV Scan 原理、SST 存储与 Key Rewrite 恢复机制

深入解析 TiDB BR 备份恢复设计:KV Scan 原理、SST 存储与 Key Rewrite 恢复机制

2026-09-05 12:17:30作者:晏闻田Solitary

本文以 TiDB 仓库中 BR(Backup & Restore)模块 2019 年的新版备份恢复设计文档为主体,系统讲解基于 KV Scan 的分布式备份原理、SST 文件流转、GC 安全点保护、Key Rewrite 与 Split & Scatter 等恢复核心机制,并结合当前仓库 br/pkg 下的实际源码印证该设计在 BR 客户端中的落地形态。读完本文后,你将能够理解 TiDB 全量/增量备份的一致性保证来源、恢复时为何必须改写 SST 的 Key,以及备份/恢复流程中每一环的设计动机与源码依据。

设计背景:为什么要做 KV Scan 新方案

设计文档(2019-08-05-new-design-of-backup-restore.md,最后更新于 2020-03-07)开篇说明:由于 BR 需要和 TiDB CDC 在技术上整合,方案的基本思路是采用 KV Scan 的备份方式。它与传统 mydumper 类的 SQL 层导出有四点本质区别:

  1. 分布式导出数据:数据由各个 region 的 leader 生成,理论上备份的吞吐能达到集群极限;
  2. 数据形态是 SST:SST 格式能快速恢复,同时可以直接复用 RocksDB 自带的数据压缩/加密能力;
  3. 直写第三方存储:备份数据直接保存到 S3、GCS 等外部存储;
  4. 一致性保证是 SI(快照隔离):保证能恢复到 point-in-time 的状态。

可以理解为,备份的语义本质上是一次 select *,但执行位置从 TiDB SQL 层下推到了 TiKV 存储层,从而绕开了 SQL 层的逐行传输瓶颈。

备份原理与整体流程

整个备份过程由独立于 TiKV 的外部程序推进(该程序即 BR,入口是 SQL/命令行)。整体流程分五步:

  1. 外部程序根据用户指定备份范围下发备份命令到 TiKV;
  2. TiKV 接受备份请求后交由 region leader 处理,并使用一个特殊的 iterator 读取数据;
  3. TiKV 读取的数据被组织成 SST 文件,这里需要控制 IO 和内存使用;
  4. TiKV 把 SST 文件写到外部存储(本地存储、S3 等),这里需要流控;
  5. TiKV 把执行信息汇报给外部程序。

外部程序(BR 客户端)侧的基本流程则是对应的聚合-重试逻辑:

  1. 下推备份到所有 TiKV;
  2. 接受 TiKV Streaming 发过来的备份结果;
  3. 聚合检查是否有范围没有备份到或发生错误;
  4. 如果有范围错误或者没有备份到则重试;
  5. 重试需要精细化——查询 PD 对应的 leader 然后下发;
  6. 如果需要备份 table 或 database,除了数据外还需要备份 schema。

从源码看:BR 客户端的轮次化备份主循环

当前仓库中,上述"下推—聚合—查漏补缺—重试"的流程由 br/pkg/backup/client.go 中的 RunLoop 实现,其结构与文档描述高度吻合:

  • 轮次(round)机制RunLoop 以"round"为单位循环,每一轮"尝试在所有 TiKV store 上备份所有剩余范围",理想情况下备份在一轮内完成(源码注释:one backup round try to backup all ranges on all tikv stores);
  • 查漏补缺:每轮通过 loop.GlobalProgressTree.GetIncompleteRanges() 计算尚未备份完成的范围(SubRanges),对应文档第 3 步"聚合检查是否有范围没有备份到";当 SubRanges 为空时判定"all range backuped"并结束;
  • 精细化重试:每个 store 有独立的 gRPC 客户端与结果 channel(storeBackupResultChMap),当某个 store 的备份 goroutine 失败时,通过 BackupRetryPolicy{One: storeID} 只对单个 store 重发请求;若集群状态变化(如新 store 加入、store 重启导致 leader 漂移),则通过 {All: true} 通知整轮重建连接,对应文档"重试需要精细化,查询 PD 对应的 leader 然后下发";
  • 事务锁处理:一轮结束时集中 resolve 本轮收集到的 txn lock(ResolveLocksForRead),并把已解决的锁放入下一轮请求的 Context.ResolvedLocks,使 TiKV 扫描器可以跳过这些锁——这正是文档"可恢复异常:KeyLocked,一般由事务冲突造成"在实现层面的处理;
  • 客户端结构Client 结构体(client.go)持有 storage/backend(外部存储后端)、cipher(SST 元信息加密)、checkpointRunner(断点续备)、gcTTL(GC 保护时长)等字段,分别对应文档中外部存储、加密、重试与 GC 时间四个主题。

备份下推:如何把命令细分到 region

文档给出两种下推方式:

  • region info accessor 下推(推荐):直接把备份命令下发到 TiKV,利用 region info accessor 把命令细分到各个 region leader 上执行 scan。其最大的问题是可能备份多余的数据(比如 leader 在备份期间迁移了),但文档明确"这不是什么正确性问题";
  • 按 region 逐个下发:也很简单,但效率没有前者高,同样存在备份多余文件的可能,只是概率更低。

无论哪种方式,下推完成后都需要"查漏补缺"确保所有 key range 都被覆盖,这与上文 GetIncompleteRanges 的进度树机制一致。

Iterator:percolator 事务模型下的备份读取

这是整个方案中最具 TiDB 特色的部分。由于 TiDB/TiKV 的事务模型是 percolator,数据需要存放在 3 个 CF(Column Family)中:defaultlockwrite。因此 TiKV 想把数据放进 SST,至少要扫出 writedefault 的数据。现有 iterator 不适用,原因有二:

  1. 它只能吐出 default 的 key-value;
  2. 它吐出的是 point-in-time 的数据(合并了多版本)。

文档对全量与增量分别定义了 Iterator 的扫描语义:

  • 全量备份:TiKV 需要根据一个指定的 ts(称为 backup_ts)扫出 writedefault 的 key-value。Iterator 吐出的 key 需要是 data key,即 key 的第一个字节是 z——这是为了在恢复时可以直接 ingest,不用 rewrite key(data key 本身不含 tableID 版本信息的问题留给恢复期统一处理)。
  • 增量备份:需要扫出一段时间的增量数据,时间段为 (backup_ts, current_ts]。由于备份的一致性保证是 SI,所有增量数据可以直接通过扫 write CF 得到。需要吐出的记录:
    • Put:新增的数据;
    • Delete:删除的数据。
    • 不需要吐出的记录:Lockselect for update 写的记录,实际上没有任何数据变更)、Rollback(清理由提交失败造成的垃圾数据)。

性能考量

性能是 KV Scan 方案最头疼的问题之一:全表扫描势必对线上业务造成影响,而增量备份的实现同样是全表扫(需要扫描整段的 write CF),这与传统数据库基于 binlog/WAL 的增量完全不同。文档给出的优化方向是借鉴 CockroachDB 在 SST 上加上 max ts 的 table property,从而在扫描时可以跳过时间戳上界不在增量范围内的 SST 文件。

外部存储:内存缓冲直写

备份存储设计了一个接口,下层可以有不同的实现(本地磁盘、S3 等)。由于外部存储不一定是真正的磁盘,TiKV 在生成 SST 时不会先把数据写到本地磁盘,而是先缓存在内存中,生成完毕后直接写到外部存储,避免一次本地落盘 IO,提高整体吞吐——当然需要内存控制。这一"内存生成 SST、直传对象存储"的设计是当前 br/pkgClient.storage/backendclient.go)抽象的源头。

异常处理:可恢复与不可恢复

备份期间的异常与 select * 一样,分为可恢复和不可恢复两类,且都可以复用 TiDB 现有的重试机制:

可恢复异常(发生时备份进度不会被打断):

异常 常见成因
RegionError region split/merge、not leader
KeyLocked 事务冲突
Server is busy TiKV 太忙

除此之外的其他错误都是不可恢复异常,发生后打断备份进度。

在实现层面,这类异常被 TiKV 逐 region 上报,BR 客户端将其转化为下一轮对未完成范围的重发(RunLoopGetIncompleteRanges 驱动);而 KeyLocked 这类锁冲突还会被聚合后经 ResolveLocksForRead 统一解决,如前述源码所示。

超出 GC 时间:备份窗口与 GC 的博弈

"超出 GC 时间"指需要备份的数据已经被 GC 回收,这种情况一般发生在增量备份上,会导致增量不完整。发生该错误时,外部程序需要重新执行一次全量备份。

文档指出的关键问题是:默认 GC 时间是 10 分钟,实在太短——一旦开启全量备份,增量备份必须马上接上且运行间隔必须在 10 分钟内,这个频率集群肯定受不了;CDC 也会遇到同样问题。因此在开始备份或 CDC 的集群上需要适当延长 GC 时间,比如延长到 6 小时或 12 小时

当前仓库中这一机制的具体实现是:备份期间 BR 通过 PD 的 service GC safepoint API 把安全点抬到 backup_ts - 1,见 br/pkg/gc/manager_global.go

lastSafePoint, err := m.pdClient.UpdateServiceGCSafePoint(ctx, sp.ID, sp.TTL, sp.BackupTS-1)

即把 GC 安全点固定在备份起点之前一个版本、带上 TTL,备份结束后再清零释放。备份客户端也暴露了 SetGCTTL/GetGCTTLclient.go)用于控制该保护的 TTL,与文档"延长 GC 时间"的诉求一一对应。

恢复:从 SST 到可用数据

恢复所需的工作有五件事:

  1. 创建需要恢复的 database 和 table;
  2. 根据 table 和 SST 文件元信息,进行 Split & Scatter Region;
  3. 将备份下来的 SST 文件按需读取到 TiKV 各节点;
  4. 根据新 table 的 ID 对 SST 进行 Key Rewrite;
  5. 将处理好的 SST 文件 Ingest 到 TiKV。

Key Rewrite:tableID 编码决定了必须改写

TiDB 的数据在 TiKV 中的保存形式是(见 pkg/tablecodec):

Key: tablePrefix{tableID}_recordPrefixSep{rowID}
Value: [col1, col2, col3, col4]

Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: rowID

由于 Key 中编码了 tableID,不能把备份下来的 SST 文件不经处理直接恢复到 TiKV,否则可能因 tableID 对不上而导致数据错乱。解决方案是在恢复前对 SST 文件做 key 改写:把原有 tableID 替换为新创建的 tableID,indexID 也需要同样处理。

在后续的 importSST 设计文档(2019-09-17-design-of-reorganize-importSST-to-TiKV.md)中,Key Rewrite 被进一步具体化为前缀替换规则,一个 BR 的 SST 可能包含多个 table,所以必须支持多条 Rewrite Rule 同时生效;且 SST 可能来自非 TiDB 系统,Importer 不应有 key 编码格式假设。建议的数据结构:

message RewriteRule {
	bytes old_prefix = 1;  // this can be empty for universal prefix insertion!
	bytes new_prefix = 2;  // these are _not_ just an integer!
}

message RestoreRequest {
	...
	repeated RewriteRule rewrite_rules = N;
	...
}

配套的正向改写与反向还原函数:

// 正向替代一个 Key
fn rewrite_key(rules: &[RewriteRule], key: &[u8]) -> Cow<[u8]> {
    for rule in rules {
        if key.starts_with(rule.old_prefix) {
            return Cow::Owned(rule.new_prefix + key[rule.old_prefix.len()..])
        }
    }
    Cow::Borrowed(key)
}

// 反向还原一个 Key
fn undo_rewrite_key(rules: &[RewriteRule], key: &[u8]) -> Cow<[u8]> {
    for rule in rules {
        if key.starts_with(rule.new_prefix) {
            return Cow::Owned(rule.old_prefix + key[rule.new_prefix.len()..])
        }
    }
    Cow::Borrowed(key)
}

关于"在哪个位置做 rewrite"的完整权衡(源数据必然是 rewrite 前的、PD 和 Ingest 后的结果必然是 rewrite 后的),见 2019-09-09-BR-key-rewrite-disscussion.md,其结论是"Key rewrite before ingest in every replica":每个 Peer 独自在 ingest 之前进行 Key Rewrite,Importer 侧写盘/读盘/网络传输代价最小,rewrite 只发生 R+1 次(pre-split + Raft-Ingest 前)。

Split & Scatter:按 SST 范围预分裂 region

TiKV 对 Region 的大小是有限制的,默认为 96MB,超出阈值则需要分裂(Split)。集群刚启动时只有少量 region,恢复时不能把所有数据都恢复到这些 region 中,因此需要提前将 Region 分裂(Split)并打散(Scatter)

由于备份 SST 文件是按照 region 生成的,天然就有合适的大小和对应的数据范围,所以可以根据各个 SST 文件中的数据范围对集群中的 region 进行分裂;分裂完成后还需要打散新分裂出来的 region,防止发生数据倾斜。

恢复流程的演进:弃用 tikv-importer

原设计文档记录了恢复方案的一次关键演进:不再使用 tikv-importer(预计 4.0 移除),转而使用 TiKV 一组能够下载/导入 SST 文件的新 API(相关内容见 2019-09-17-design-of-reorganize-importSST-to-TiKV.md,即把 key rewrite 下推到 TiKV 的 sst-importer 工作线程)。恢复流程因此精简为:

  1. 使用备份的 schema 信息创建新表;
  2. 通过备份的元数据构建 key-value 的范围,同时依照新表 ID 构建 rewrite rules;
  3. 依照 rewrite rules 和 key 范围分割并打散 regions;
  4. 调用 download 和 ingest API 完成导入。

该方案的具体执行分两个阶段(引自 importSST 设计文档):

  • Process:客户端向 region peers 所处的 TiKV 发起 Process 请求,TiKV 中的 importer worker 根据请求下载 SST 并 Rewrite SST;
  • Ingest:客户端发现所有 TiKV 准备好后,向 region leader 发起 ingest 请求,Leader 通过 Raft 复制给 follower,各 TiKV 执行 ingest sst,导入结束。

错误处理上:Download SST 和 Rewrite SST 两步不会对集群数据产生修改,出错时客户端重试即可;真正修改数据的 Ingest 部分,可恢复错误包括 NotLeader、IngestEpochNotMatch(均重试),Ingest 执行失败则视为不可恢复错误。

当前仓库中恢复侧的代码主体位于 br/pkg/restore,其中 restorer.gosplit/ingestrec/ 等目录分别对应恢复主控、region 分裂打散与 ingest 回执管理等职责,与上述流程的阶段划分相呼应。

两个遗留问题:在线恢复与原集群恢复

原设计文档"问题"一节提出了两个当时尚未完全解决的难点:

  1. 在线恢复:困境在于 ingest SST 失败会导致各个副本之间的数据不一致;文档给出的方向是通过 placement rule 实现资源隔离(把恢复中的表调度到独立资源,从而降低对在线业务与副本一致性的冲击);
  2. 原集群恢复:涉及各种 ID 冲突问题,最简单办法是 rewrite key——把 key 中老 ID 替换成新 ID(CockroachDB 采用同类方案)。文档指出"我们通过 Key rewrite 在理论上支持原集群恢复",即恢复目标与原集群相同时,用新分配的 tableID 整体替换旧 ID,避免与现存数据冲突。

附录:Backup/Import gRPC 服务

文档附录说明:备份 RPC 的协议定义位于 kvproto 仓库的 backup.protobackuppb),新的恢复 RPC 见 import_sstpb.proto。当前仓库源码中也可以看到这些协议的直接引用,例如 br/pkg/backup/client.go 顶部的 backuppb "github.com/pingcap/kvproto/pkg/brpb" 导入,以及 MainBackupLoop 中通过 backuppb.BackupClient 与每个 TiKV store 建立的 gRPC streaming 连接(GetBackupClient/ResetBackupClient,见 client.goClientMgr 接口)——这正是文档第 2 步"TiKV Streaming 发过来的备份结果"在客户端一侧的形态。

小结:从 2019 设计到当前实现的映射

设计文档概念 当前仓库中的对应实现
外部程序聚合-重试主循环 br/pkg/backup/client.goRunLoop 轮次机制
查漏补缺、精细化重试 ProgressRangeTree.GetIncompleteRanges() + 单 store 级 BackupRetryPolicy 重发
KeyLocked 可恢复异常 轮末 ResolveLocksForRead 并回填 Context.ResolvedLocks
延长 GC 时间保护备份数据 br/pkg/gc/manager_global.goUpdateServiceGCSafePoint(..., sp.BackupTS-1)
外部存储抽象、内存生成 SST 直写 Client.storage/Client.backendstoreapi.Storagebackuppb.StorageBackend
Key Rewrite / Split & Scatter / Ingest br/pkg/restorerestorer.gosplit/ingestrec/ 等)

需要说明的是:本文主体文档写于 2019-2020 年的 BR 3.x 设计期,部分表述(如"4.0 移除 tikv-importer")是当时语境的展望;但 KV Scan 备份原理、SST 直写外部存储、GC 安全点保护、Key Rewrite 恢复这些核心机制,在 br/pkg/backupbr/pkg/restore 中依然构成了 BR 备份恢复的基础架构。阅读这份设计文档,是理解 TiDB 备份恢复"为什么长这样"的最佳入口。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384