关于数据库:备份的-算子下推BR-简介丨TiDB-工具分享

BR 抉择了在 Transaction KV 层面进行扫描来实现备份。这样,备份的外围便是散布在多个 TiKV 节点上的 MVCC Scan:简略,粗犷,然而无效,它生来就继承了 TiKV 的诸多劣势:分布式、利于横向拓展、灵便(能够备份任意范畴、未 GC 的任意版本的数据)等等长处。

相较于从前只能应用 mydumper 进行 SQL 层的备份,BR 可能更加高效地备份和复原:它勾销了 SQL 层的开销,同时反对备份索引,而且所有备份都是曾经排序的 SST 文件,以此大大减速了复原。

BR 的实力在之前的文章(https://pingcap.com/zh/blog/cluster-data-security-backup)中曾经展现过了,本文将会详细描述 BR 备份侧的具体实现:简略来讲,BR 就是备份的“算子下推”:通过 gRPC 接口,将工作下发给 TiKV,而后让 TiKV 本人将数据转储到内部存储中。

BR 的根本流程

接口

为了区别于个别的 MVCC Scan 申请,TiKV 提供一个叫做 Backup 的接口,这个接口与个别的读申请不同——它不会返回数据给客户端,而是间接将读到的数据存储到指定的存储器(External Stroage)中:

service Backup {
    // 收到 backup 的 TiKV,将会将 Request 指定范畴中,所有本身为 leader
    // 的 region 备份,并流式地返回给客户端(每个 region 对应流中的一个 item)。
    rpc backup(BackupRequest) returns (stream BackupResponse) {}
}

// NOTE:暗藏了一些不重要的 field 。
message BackupRequest {
    // 备份的范畴,[start_key, end_key)。
    bytes start_key = 2;
    bytes end_key = 3;
    // 备份的 MVCC 版本。
    uint64 start_version = 4;
    uint64 end_version = 5;
    
    // 限流接口,为了确保和复原时候的一致性,限流限度保留备份文件的阶段的速度。
    uint64 rate_limit = 7;
    
    // 备份的指标存储。
    StorageBackend storage_backend = 9;
    // 备份的压缩 -- 这是一个用 CPU 资源换取 I/O 带宽的优化。
    CompressionType compression_type = 12;
    int32 compression_level = 13;
    // 备份反对加密。
    CipherInfo cipher_info = 14;
}

message BackupResponse {
    Error error = 1;
    // 备份的申请将会返回屡次,每次都会返回一个曾经实现的子范畴。
    // 利用这些信息能够统计备份进度。
    bytes start_key = 2;
    bytes end_key = 3;
    // 返回该范畴备份文件的列表,用于复原的时候追踪。
    repeated File files = 4;
}

客户端

BR 客户端会借助 TiDB 的接口,依据用户指定须要备份的库和表,计算出来须要备份的范畴(“ranges”)。计算的根据是:

  1. 根据每个 table 的所有 data key 生成 range。(所有带有 t{table_id}_r 前缀的 Key)
  2. 根据每个 index 的所有 index key 生成 range。(所有带有 t{table_id}_i{index_id} 前缀的 Key)
  3. 如果 table 存在 partition(这意味着,它可能有多个 table ID),对于每个 partition,依照上述规定生成 range。

为了取得最大的并行度,BR 客户端会并行地向所有 TiKV 发送这些 Range 上的备份申请。

当然,备份不可能一帆风顺。咱们在备份的时候不可避免地会遇到问题:例如网络谬误,或者触发了 TiKV 的限流措施(Server is Busy),或者 Key is Locked,这时候,咱们必须放大这些 range,从新发送申请(否则,咱们就要反复一遍之前曾经做过的工作……)。

在失败之后,抉择适合的 range 来重发申请的过程,在 BR 中被称为 “细粒度备份(fine-grained backup)”,具体而言:

  1. 在之前的 “粗粒度备份” 中,BR 客户端每收到一个 BackupResponse 就会将其中的 [start_key, end_key) 作为一个 range 存入一颗区间树中(你能够把它设想成一个简略的 BTreeSet<(Vec<u8>, Vec<u8>)>)。
  2. “粗粒度备份” 遇到任何可重试谬误都会疏忽掉,只是相应的 range 不会存入这颗区间树中,因而树中会留下一个 “空洞”,这两步的伪代码如下。
func Backup(tree RangeTree) {
    // ... 
    for _, resp := range responses {
        if resp.Success {
            tree.Insert(resp.StartKey, resp.EndKey)  
        }
    }
}

// An example: 
// When backing up the ange [1, 5).
// [1, 2), [3, 4) and [4, 5) successed, and [2, 3) failed:
// The Tree would be like: { [1, 2), [3, 4), [4, 5) }, 
// and the range [2, 3) became a "hole" in it.
// 
// Given the range tree is sorted, it would be easy to 
// find all holes in O(n) time, where n is the number of ranges.
  1. 在 “粗粒度备份” 完结之后,咱们遍历这颗区间树,找到其中所有 “空洞”,并行地进行 “细粒度备份”:
  • 找到蕴含该空洞的所有 region。
  • 对他们的 leader 发动 region 相应范畴的 Backup RPC。
  • 胜利之后,将对应的 range 放入区间树中。
  1. 在一轮 “细粒度备份” 完结后,如果区间树中还有空洞,则回到 (3),在超过肯定次数的重试失败之后,报错并退出。

在上述 “备份”流程实现之后,BR 会利用 Coprocessor 的接口,向 TiKV 申请执行用户所指定表的 checksum。

这个 checksum 会在复原的时候用作参考,同时也会和 TiKV 在备份期间生成的逐文件的 checksum 进行比照,这个比对的过程叫做 “fast checksum”。

在 “备份” 的过程中,BR 会通过 TiDB 的接口收集备份的表构造、备份的工夫戳、生成的备份文件等信息,贮存到一个 “backupmeta”中。这个是复原时候的重要参考。

TiKV

为了实现资源隔离,缩小资源抢占,Backup 相干的工作都运行在一个独自的线程池外面。这个线程池中的线程叫做 “bkwkr”(“backup worker” 极其形象的缩写)。

在收到 gRPC 备份的申请之后,这个 BackupRequest 会被转化为一个 Task

而后,TiKV 会利用 Task 中的 start_keyend_key 生成一个叫做 “Progress”的构造:它将会把 Task 中宏大的范畴划分为多个子范畴,通过:

  1. 扫描范畴内的 Region。
  2. 对于其中以后 TiKV 角色为 Leader 的 Region,将该 Region 的范畴作为 Backup 的子工作下发。

Progress 提供的接口是一个应用 “拉模型” 的接口:forward。随后,TiKV 创立的各个 Backup Worker 将会去并行地调用这个接口,取得一组待备份的 Region,而后执行以下三个步骤:

  1. 对于这些 Region,Backup Worker 将会通过 RaftKV 接口,进行一次 Raft 的读流程,最终取得对应 Region 在 Backup TS 的一个 Snapshot。(“Get Snapshot”)
  2. 对于这个 Snapshot,Backup Worker 会通过 MVCC Read 的流程去扫描 backup_ts 的统一版本。这里咱们会扫描出 Percolator 的事务,为了复原不便,咱们会筹备 “default” 和 “write” 两个长期缓冲区,别离对应 TiKV Percolator 实现中的 Default CF 和 Write CF。(“Scan”)
  3. 而后,咱们会先将扫描进去的事务中两个 CF 的 Raw Key 刷入对应缓冲区中,在整个 Region 备份实现(或者有些 Region 切实过大,那么会在途中切分备份文件)之后,再将这两个文件存储到内部存储中,记录它们对应的范畴和大小等等,最初返回一个 BackupResponse 给 BR。(“Save”)

为了保障文件名的唯一性,备份的文件名会包含以后 TiKV 的 store ID、备份的 region ID、start key 的哈希、CF 名称。

备份文件应用 RocksDB 的 Block Based SST 格局:它的劣势是,原生反对文件级别的 checksum 和压缩,同时能够在复原的时候疾速被 ingest 的后劲。

内部存储是为了适配多种备份指标而存在的通用贮存形象:有些相似于 Linux 中的 VFS,不过简化了十分多:仅仅反对简略的保留和下载整个文件的操作。它目前对支流的云盘都做了适配,并且反对以 URL 的模式序列化和反序列化。例如,应用 s3://some-bucket/some-folder,能够指定备份到 S3 云盘上的 some-bucket 之下的 some-folder 目录中。

BR 的挑战和优化

通过以上的根本流程,BR 的根本链路曾经能够跑通了:相似于算子下推,BR 将备份工作下推到了 TiKV,这样能够正当利用 TiKV 的资源,实现分布式备份的成果。

在这个过程中,咱们遇到了许多挑战,在这一节,咱们来谈谈这些挑战。

BackupMeta 和 OOM

前文中提到,BackupMeta 贮存了备份的所有元信息:包含表构造、所有备份文件的索引等等。设想一下你有一个足够大的集群:比如说,十万张表,总共可能有数十 TB 的数据,每张表可能还有若干索引。

如此最终可能产生数百万的文件:在这个时候,BackupMeta 可能会达到数 GB 之大;另一方面,因为 protocol buffer 的个性,咱们可能不得不读出整个文件能力将其序列化为 Go 语言的对象,由此峰值内存占用又多一倍。在一些极其环境下,会存在 OOM 的可能性。

为了缓解这个问题,咱们设计了一种分层的 BackupMeta 格局,简略来讲,就是将 BackupMeta 拆分成索引文件和数据文件两局部,相似于 B+ 树的构造:

具体来讲,咱们会在 BackupMeta 中加上这些 Fields,别离指向对应的 “B+ 树” 的根节点:

message BackupMeta {
    // Some fields omitted...
    // An index to files contains data files.
    MetaFile file_index = 13;
    // An index to files contains Schemas.
    MetaFile schema_index = 14;
    // An index to files contains RawRanges.
    MetaFile raw_range_index = 15;
    // An index to files contains DDLs.
    MetaFile ddl_indexes = 16;
}

MetaFile 就是这颗 “B+ 树” 的节点:

// MetaFile describes a multi-level index of data used in backup.
message MetaFile {
    // A set of files that contains a MetaFile.
    // It is used as a multi-level index.
    repeated File meta_files = 1;
    
    // A set of files that contains user data.
    repeated File data_files = 2;
    // A set of files that contains Schemas.
    repeated Schema schemas = 3;
    // A set of files that contains RawRanges.
    repeated RawRange raw_ranges = 4;
    // A set of files that contains DDLs.
    repeated bytes ddls = 5;
}

它可能有两种状态:一是承载着对应数据的 “叶子节点”(后四个 field 被填上相应的数据),也能够通过 meta_files 将本身指向下一个节点:File 是一个到内部存储中其余文件的援用,蕴含文件名等等根底信息。

目前的实现中,为了回避真正实现相似 B 树的决裂、合并操作的复杂性,咱们仅仅应用了一级索引,将的表构造和文件的元数据别离存储到一个个 128M 的小文件中,如此曾经足够回避 BackupMeta 带来的 OOM 问题了。

GC, GC never changes

在备份扫描的整个过程中,因为时间跨度较长,必然会受到 GC 的影响。
不仅仅是 BR,别的生态工具也会遇到 GC 的问题:例如,TiCDC 须要增量扫描,如果初始版本曾经被 GC 掉,那么就无奈同步统一的数据。

过来咱们的解决方案个别是让用户手动调大 GC Lifetime,然而这往往会造成 “初见杀” 的成果:用户开开心心备份,而后去做其余事件,几个小时后发现备份因为 GC 而失败了……

这会十分影响用户的情绪:为了让用户能更加开心地应用各种生态工具,PD 提供了一个叫做 “Service GC Safepoint” 的性能。各个服务能够通过 PD 上的接口,设置一个 “Safepoint”,TiDB 会保障,在 Safepoint 指定的工夫点之后,所有历史版本都不会被 GC。为了避免 BR 在意外退出之后导致集群无奈失常 GC,这个 Safepoint 还会存在一个 TTL:在指定工夫之后若是没有刷新,则 PD 会移除这个 Service Safe Point。

对于 BR 而言,只须要将这个 Safepoint 设置为 Backup TS 即可,作为参考,这个 Safepoint 会被命名为 “br-<Random UUIDv4>”,并且有五分钟的 TTL。

备份压缩

在全速备份的时候,备份的流量可能相当大:具体能够看看结尾“秀肌肉”文章相干的局部。

如果你违心应用足够多的外围去备份,那么可能很快就会达到网卡的瓶颈(例如,如果不经压缩,千兆网卡大概只须要 4 个外围就会被满。),为了防止网卡成为瓶颈,咱们在备份的时候引入了压缩。

咱们复用了 RocksDB Block Based Table Format 中提供的压缩性能:默认会应用 zstd 压缩。压缩会增大 CPU 的占用率,然而能够缩小网卡的负载,在网卡成为瓶颈的时候,能够显著晋升备份的速度。

限流与隔离

为了缩小对其余工作的影响,如前文所说,所有的备份申请都会在独自的线程池中执行。

然而即便如此,如果备份耗费了太多的 CPU,不可避免地会对集群中其它负载造成影响:次要的起因是 BR 会占用大量 CPU,影响其它工作的调度;另一方面则是 BR 会大量读盘,影响写工作刷盘的速度。

为了缩小资源的应用,BR 提供了一个限流机制。当用户带有 –ratelimit 参数启动 BR 的时候,TiKV 侧的第三步“Save”,将会被限流,与此同时也会限度之前步骤的流量。

这里须要留神一个点:备份数据的大小往往会远远小于集群的理论空间占用。起因是备份只会备份单正本、单 MVCC 版本的数据。通 ratelimit 限流施加于 Save 阶段,因而是限度写备份数据的速度。

在 “服务端” 侧,也能够通过调节线程池的大小来限流,这个参数叫做 backup.num-threads,思考到咱们容许用户侧限流,它的默认值十分高:是全副 CPU 的 75%。如果须要在服务侧进行更加彻底的限流,能够批改这个参数。作为参考,一块 Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz CPU 每个外围大略每秒能生成 10M 经 zstd 压缩的 SST 文件。

总结

通过 Service Safe Point,咱们解决了手动调节 GC 带来的“难用”的问题。

通过新设计的 BackupMeta,咱们解决了海量表场景的 OOM 问题。
通过备份压缩、限流等措施,咱们让 BR 对集群影响更小、速度更快(即使二者可能无奈兼得)。

总体上而言,BR 是在 “物理备份” 和 “逻辑备份” 之间的 “第三条路”:绝对于 mydumper 或者 dumpling 等工具,它消解了 SQL 层的额定代价;绝对于在分布式系统中寻找物理层的一致性快照,它易于实现且更加乖巧。对于目前阶段而言,是合适于 TiDB 的容灾备份解决方案。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理