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

52次阅读

共计 6586 个字符,预计需要花费 17 分钟才能阅读完成。

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 的容灾备份解决方案。

正文完
 0