十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

深入解析 TiDB BR 备份恢复设计:KV Scan 原理、SST 存储与 Key Rewrite 恢复机制 深入解析 TiDB BR 备份恢复设计KV Scan 原理、SST 存储与 Key Rewrite 恢复机制【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb本文以 TiDB 仓库中 BRBackup 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 层导出有四点本质区别分布式导出数据数据由各个 region 的 leader 生成理论上备份的吞吐能达到集群极限数据形态是 SSTSST 格式能快速恢复同时可以直接复用 RocksDB 自带的数据压缩/加密能力直写第三方存储备份数据直接保存到 S3、GCS 等外部存储一致性保证是 SI快照隔离保证能恢复到 point-in-time 的状态。可以理解为备份的语义本质上是一次select *但执行位置从 TiDB SQL 层下推到了 TiKV 存储层从而绕开了 SQL 层的逐行传输瓶颈。备份原理与整体流程整个备份过程由独立于 TiKV 的外部程序推进该程序即 BR入口是 SQL/命令行。整体流程分五步外部程序根据用户指定备份范围下发备份命令到 TiKVTiKV 接受备份请求后交由 region leader 处理并使用一个特殊的 iterator 读取数据TiKV 读取的数据被组织成 SST 文件这里需要控制 IO 和内存使用TiKV 把 SST 文件写到外部存储本地存储、S3 等这里需要流控TiKV 把执行信息汇报给外部程序。外部程序BR 客户端侧的基本流程则是对应的聚合-重试逻辑下推备份到所有 TiKV接受 TiKV Streaming 发过来的备份结果聚合检查是否有范围没有备份到或发生错误如果有范围错误或者没有备份到则重试重试需要精细化——查询 PD 对应的 leader 然后下发如果需要备份 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 客户端与结果 channelstoreBackupResultChMap当某个 store 的备份 goroutine 失败时通过BackupRetryPolicy{One: storeID}只对单个 store 重发请求若集群状态变化如新 store 加入、store 重启导致 leader 漂移则通过{All: true}通知整轮重建连接对应文档重试需要精细化查询 PD 对应的 leader 然后下发事务锁处理一轮结束时集中 resolve 本轮收集到的 txn lockResolveLocksForRead并把已解决的锁放入下一轮请求的Context.ResolvedLocks使 TiKV 扫描器可以跳过这些锁——这正是文档可恢复异常KeyLocked一般由事务冲突造成在实现层面的处理客户端结构Client结构体client.go持有storage/backend外部存储后端、cipherSST 元信息加密、checkpointRunner断点续备、gcTTLGC 保护时长等字段分别对应文档中外部存储、加密、重试与 GC 时间四个主题。备份下推如何把命令细分到 region文档给出两种下推方式region info accessor 下推推荐直接把备份命令下发到 TiKV利用 region info accessor 把命令细分到各个 region leader 上执行 scan。其最大的问题是可能备份多余的数据比如 leader 在备份期间迁移了但文档明确这不是什么正确性问题按 region 逐个下发也很简单但效率没有前者高同样存在备份多余文件的可能只是概率更低。无论哪种方式下推完成后都需要查漏补缺确保所有 key range 都被覆盖这与上文GetIncompleteRanges的进度树机制一致。Iteratorpercolator 事务模型下的备份读取这是整个方案中最具 TiDB 特色的部分。由于 TiDB/TiKV 的事务模型是percolator数据需要存放在 3 个 CFColumn Family中default、lock、write。因此 TiKV 想把数据放进 SST至少要扫出write和default的数据。现有 iterator 不适用原因有二它只能吐出default的 key-value它吐出的是 point-in-time 的数据合并了多版本。文档对全量与增量分别定义了 Iterator 的扫描语义全量备份TiKV 需要根据一个指定的 ts称为backup_ts扫出write和default的 key-value。Iterator 吐出的 key 需要是data key即 key 的第一个字节是z——这是为了在恢复时可以直接 ingest不用 rewrite keydata key 本身不含 tableID 版本信息的问题留给恢复期统一处理。增量备份需要扫出一段时间的增量数据时间段为(backup_ts, current_ts]。由于备份的一致性保证是 SI所有增量数据可以直接通过扫writeCF 得到。需要吐出的记录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/pkg中Client.storage/backendclient.go抽象的源头。异常处理可恢复与不可恢复备份期间的异常与select *一样分为可恢复和不可恢复两类且都可以复用 TiDB 现有的重试机制可恢复异常发生时备份进度不会被打断异常常见成因RegionErrorregion split/merge、not leaderKeyLocked事务冲突Server is busyTiKV 太忙除此之外的其他错误都是不可恢复异常发生后打断备份进度。在实现层面这类异常被 TiKV 逐 region 上报BR 客户端将其转化为下一轮对未完成范围的重发RunLoop中GetIncompleteRanges驱动而 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.golastSafePoint, err : m.pdClient.UpdateServiceGCSafePoint(ctx, sp.ID, sp.TTL, sp.BackupTS-1)即把 GC 安全点固定在备份起点之前一个版本、带上 TTL备份结束后再清零释放。备份客户端也暴露了SetGCTTL/GetGCTTLclient.go用于控制该保护的 TTL与文档延长 GC 时间的诉求一一对应。恢复从 SST 到可用数据恢复所需的工作有五件事创建需要恢复的 database 和 table根据 table 和 SST 文件元信息进行 Split Scatter Region将备份下来的 SST 文件按需读取到 TiKV 各节点根据新 table 的 ID 对 SST 进行 Key Rewrite将处理好的 SST 文件 Ingest 到 TiKV。Key RewritetableID 编码决定了必须改写TiDB 的数据在 TiKV 中的保存形式是见 pkg/tablecodecKey: 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 替换为新创建的 tableIDindexID也需要同样处理。在后续的 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 RewriteImporter 侧写盘/读盘/网络传输代价最小rewrite 只发生 R1 次pre-split Raft-Ingest 前。Split Scatter按 SST 范围预分裂 regionTiKV 对 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 工作线程。恢复流程因此精简为使用备份的 schema 信息创建新表通过备份的元数据构建 key-value 的范围同时依照新表 ID 构建 rewrite rules依照 rewrite rules 和 key 范围分割并打散 regions调用 download 和 ingest API 完成导入。该方案的具体执行分两个阶段引自 importSST 设计文档Process客户端向 region peers 所处的 TiKV 发起 Process 请求TiKV 中的 importer worker 根据请求下载 SST 并 Rewrite SSTIngest客户端发现所有 TiKV 准备好后向 region leader 发起 ingest 请求Leader 通过 Raft 复制给 follower各 TiKV 执行 ingest sst导入结束。错误处理上Download SST 和 Rewrite SST 两步不会对集群数据产生修改出错时客户端重试即可真正修改数据的 Ingest 部分可恢复错误包括 NotLeader、IngestEpochNotMatch均重试Ingest 执行失败则视为不可恢复错误。当前仓库中恢复侧的代码主体位于 br/pkg/restore其中restorer.go、split/、ingestrec/等目录分别对应恢复主控、region 分裂打散与 ingest 回执管理等职责与上述流程的阶段划分相呼应。两个遗留问题在线恢复与原集群恢复原设计文档问题一节提出了两个当时尚未完全解决的难点在线恢复困境在于 ingest SST 失败会导致各个副本之间的数据不一致文档给出的方向是通过 placement rule 实现资源隔离把恢复中的表调度到独立资源从而降低对在线业务与副本一致性的冲击原集群恢复涉及各种 ID 冲突问题最简单办法是 rewrite key——把 key 中老 ID 替换成新 IDCockroachDB 采用同类方案。文档指出我们通过 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.go 的ClientMgr接口——这正是文档第 2 步TiKV Streaming 发过来的备份结果在客户端一侧的形态。小结从 2019 设计到当前实现的映射设计文档概念当前仓库中的对应实现外部程序聚合-重试主循环br/pkg/backup/client.go 的RunLoop轮次机制查漏补缺、精细化重试ProgressRangeTree.GetIncompleteRanges() 单 store 级BackupRetryPolicy重发KeyLocked 可恢复异常轮末ResolveLocksForRead并回填Context.ResolvedLocks延长 GC 时间保护备份数据br/pkg/gc/manager_global.go 的UpdateServiceGCSafePoint(..., sp.BackupTS-1)外部存储抽象、内存生成 SST 直写Client.storage/Client.backendstoreapi.Storage与backuppb.StorageBackendKey Rewrite / Split Scatter / Ingestbr/pkg/restorerestorer.go、split/、ingestrec/等需要说明的是本文主体文档写于 2019-2020 年的 BR 3.x 设计期部分表述如4.0 移除 tikv-importer是当时语境的展望但 KV Scan 备份原理、SST 直写外部存储、GC 安全点保护、Key Rewrite 恢复这些核心机制在br/pkg/backup与br/pkg/restore中依然构成了 BR 备份恢复的基础架构。阅读这份设计文档是理解 TiDB 备份恢复为什么长这样的最佳入口。【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表