{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/optimization-of-tidb-based-on-the-pitr",
    "result": {"pageContext":{"blog":{"id":"Blogs_467","title":"坚如磐石： TiDB 基于时间点的恢复特性优化之路","tags":["TiDB","PiTR"],"category":{"name":"产品技术解读"},"summary":"对于数据库产品而言，基于时间点的恢复是非常重要的基础能力，它允许用户根据需要，将数据库恢复到特定时间点，以帮助客户的数据库免受意外损坏或错误操作的影响。例如，数据库在某个时间点之后的数据遭受了意外的删除或损坏，则可以使用 PiTR 功能将数据库恢复到该时间点之前的状态，从而避免丢失重要数据。","body":"## 基于时间点恢复（PiTR）技术介绍\n\n对于数据库产品而言，基于时间点的恢复是非常重要的基础能力，它允许用户根据需要，将数据库恢复到特定时间点，以帮助客户的数据库免受意外损坏或错误操作的影响。例如，数据库在某个时间点之后的数据遭受了意外的删除或损坏，则可以使用 PiTR 功能将数据库恢复到该时间点之前的状态，从而避免丢失重要数据。\n\n由于 TiDB 数据库，每一次的数据改变都会产生对应的分布式日志，其中记录了数据库每一次变更的信息，包括事务 ID、时间戳和变更的具体内容。\n\n当用户启用 PiTR 功能后，TiDB 会定期将分布式变更日志保存到外部存储（例如：AWS S3，Azure BloB 或 NFS 等）。如果在某个时间点之后的数据被意外删除或遭受了损坏，则可以使用 BR 工具将之前的数据库备份恢复回来，通过应用保存在外部存储上的数据改变到用户指定的时间点，从而达到定点恢复的目的。\n\n![1.png](https://img1.www.pingcap.com/prod/1_b8e9104e19.png)\n\n上面的图示描述了 PiTR 特性的架构：当用户启动了日志备份之后，BR 工具会向 PD 注册一个备份任务。同时，某个 TiDB 节点会被选择成为日志备份的协调者，定期与 PD 进行交互，以便计算全局备份 checkpoint ts。同时，每个 TiKV 节点会运行定期向 PD 上报本节点的备份任务状态，并将数据变更日志发送到指定的外部存储上。  \n\n对于恢复过程，当用户发起了基于时间点的恢复命令之后，BR 工具会读取备份的元数据信息，并通知所有的 TiKV 节点启动恢复工作，TiKV 节点上的 Restore worker 会读取定点之前的变更日志并将其应用集群中，就可以得到指定时间点的 TiDB 集群。\n\n## PiTR 特性的工作机制\n\n接下来，我们进一步看一下日志备份和恢复过程的工作机制。\n\n下面的流程图说明了日志备份的主要工作机制\n\n![2.png](https://img1.www.pingcap.com/prod/2_13fab76e8c.png)\n\n其中主要的交互流程如下：\n\n1.BR 接收备份命令 br log start\n\n解析日志备份任务的日志备份起始时间点和备份存储地址，并向 PD 注册日志备份任务 (log backup task)。\n\n2.TiKV 定期监测新建/更新的日志备份任务\n\n每个 TiKV 节点的日志备份 observer 监听 PD 中创建与更新日志备份任务，然后备份该节点上在备份时间范围内的变更数据日志。\n\n3.TiKV 节点备份 KV 变更日志，并将本地备份进度上报到 TiDB\n\nTiKV 节点中 observer 服务会持续地备份 KV 变更日志，联合从 PD 查询到的 global-checkpoint-ts 来生成备份元数据信息，并定期将日志备份数据和元信息上传到存储中，同时 observer 服务还会防止未备份完成的 MVCC 数据被 PD 回收。\n\n4.TiDB 节点计算并持久化全局备份进度。\n\nTiDB 协调者节点轮询所有 TiKV 节点，获取各个 Region 的备份进度 ，并根据各个节点的备份进度计算出整体日志备份的进度，然后上报给 PD。\n\n对于恢复的过程，可以参考下面的流程图了解其工作机制\n\n![3.png](https://img1.www.pingcap.com/prod/3_d93b3b7629.png)\n\n当用户发起“br restore <timespot>” 命令后，BR 工具会对全量数据和日志数据备份地址、需要恢复到的时间点，需要恢复的数据库对象等信息进行校验，确保信息有效后，开始进行恢复。BR 首先会将全量数据进行恢复，之后读取存在的日志备份数据，计算需要恢复的日志备份数据，并访问 PD 获得需要恢复的 Region 和 KV range 相关的信息，创建恢复日志请求，发送给对应的 TiKV 节点。 TiKV 节点在接收到恢复请求后，启动 restore worker，并从备份介质中下载相应的备份数据到本地，并将需要回复的数据改变恢复到对应的 region 当中。在恢复完成之后，将恢复的执行的结果返回给 BR 工具。\n\n## TiDB 对 PiTR 的优化\n\n从上面的工作机制可以看到， 无论是日志备份还是恢复，其过程都是比较复杂的，所以 TiDB 在PiTR 发布之后，一直对这个特性进行优化，不断的提升 PiTR 的技术指标，稳定性和性能。\n\n例如， 在最初的版本中日志备份会产生大量的小文件，给用户在使用期间带来很多的问题。在最新版本中，我们将日志备份文件聚合成为多个大小至少为128M的文件，很好的解决了这个问题。\n\n对于大规模的 TiDB 集群，其全量备份往往需要运行很长时间，如果不支持断点续传功能的话，当备份过程中出现一些异常情况，导致备份任务中断的话，对用户来说是非常令人绝望的。在 6.5.0 版本中，我们支持了备份的断点续传能力，并且优化了备份的性能，目前单个 TiKV 的数据备份性能可以达到 100MB/s，日志备份对源集群的性能影响可以控制在 5% 左右，这些优化都极大的提升了大规模集群备份的用户体验和备份的成功率。 \n\n由于备份恢复通常都会被用户作为数据安全的最后一道防线，PiTR 的 RPO 和 RTO 指标也是很多用户所关心的。 我们在 PiTR 的稳定性上也做了很多的优化，其中包括：\n\n- 通过优化 BR 与 PD 和 TiKV 的通信机制，在绝大多数 TiDB 集群异常场景和 TiKV 滚动重启场景，PiTR 都可以保证 RPO 小于 5 分钟\n- 通过优化恢复性能，让 PiTR 在应用日志阶段的性能达到30 GB/h，从而降低降低 RTO 时间。 \n \n对于更多的备份恢复性能指标，请参考“[TiDB 备份与恢复概述](https://docs.pingcap.com/zh/tidb/dev/backup-and-restore-overview)” 文档。\n\n## 未来规划\n\n接下来，我们会对 PiTR 这个特性进行更多的优化，不断的提升这个特性的稳定性和性能。并探索备份恢复的更多可能性，将 TiDB 的备份恢复特性打造成稳定可靠的高性能备份恢复解决方案。","date":"2023-02-28","author":"高斌","fillInMethod":"writeDirectly","customUrl":"optimization-of-tidb-based-on-the-pitr","file":null,"relatedBlogs":[{"relatedBlog":{"body":">作者简介：余军，PingCAP 解决方案事业部总经理。\n\n对于金融企业来说，尤其是银行、证券、保险这些行业，在一个 IT 系统运行支撑业务的过程当中，考虑到硬件的故障、网络的故障，等一切可能会对业务产生影响的突发故障。那么，在过去漫长的 IT 发展的过程当中，大量的技术被应用在关于如何解决组件级的高可用，整个服务的容灾和灾备，包括如何保证整体业务的连续性。\n\n在金融行业来说，数据库作为最核心的基础组件之一，要求它能够安全运行和保障数据安全，这是一个刚需。另外，数据库服务本身的高可用，是我们实现整个对外数据服务连续性的最重要的基石。在这些基础上，光有高可用还是不够的，我们需要考虑到机房级的、数据中心级的、站点级的灾难导致的对业务的影响。配套的容灾技术，以及对应事件的方案，应运而生。在过去的二、三十年里面，关于容灾和技术的技术手段、软件工具，包括各种各样的方案、管理方法，在不断的展现。\n\n## 传统数据库支撑关键计算的高可用/容灾方案短板\n\n回到传统的数据库领域，在过去至少三四十年的时间里，我们都是在使用集中式的数据库，比如大家非常熟悉的 Oracle、DB2 包括曾经很辉煌的 Sybase、Informix 等等。这些数据库都是以大家所熟知的“ IOE”的架构来实现数据服务的。\n\n在这些技术体系下，在长期的技术发展过程当中，也有产生对应的高可用和容灾的方案，比如说大家非常熟悉的 Oracle RAC，比如说我们在 DB2 上，经常会用到的 HACMP，还有曾经大名鼎鼎的 Veritas VCS，MC/SG，以及红帽的 RHCS , Pacemaker/Corosync 等实现单机数据库高可用的。\n\n这些技术都是通过数据库，建主从的实例，然后共享数据库的数据文件，放在高端的数据的存储上。这种集中架构的话，它总体是比较稳定的，但是随着 IT 应用场景的不断发展，到今天为止，我们在考虑数据库的时候，除了要考虑它的可靠性之外，还需要考虑它如何应对海量的数据处理，海量的并发请求。**那么我们需要必须寻求扩展，而集中式的结构，它没有办法做横向扩散。此外的话，这种传统的数据库的高可用方式，非常依赖于外部的组件，就是前面说的这些独立的厂商提供的相关的高可用和容灾组件。**\n\n进入到开源数据库的阶段，大家所熟知的 MySQL 和  PostgreSQL，它们也会有对应的高可用的解决方案，比如 MySQL 它最常见的就是通过它的 Binlog 复制建立起来主从队，然后在数据库之外，我们采取类似像 MHA 这样的一个 SwitchManager 的工具，包括 PG 的话，它有 PAF、RepMgr、Ptroni 这样的技术。\n\n**在这样的技术场景中，其实在可用性和容灾方面，还是有很多的问题**，比如说复制的时候的采用的异步复制，增强的半同步复制以及国内好多互联网公司定制的 MySQL，PG 的同步方案，在多节点，跨地域容灾灾备场景中的一致性的问题，始终是一个很大的挑战。特别是在容灾的场景当中，超过 1000 公里以上的站点距离间隔，用上述复制的方式，用独立的 SwitchManager 的故障切换机制，是不是能够保证在千公里以上的容灾的可靠性是很大一个疑问。\n\n**此外，主从复制的模式资源利用率比较低**。到现在为止，我们还有在主从复制基础上，再往前走一步，提高高可用性的一些保障机制，比如说大家都熟知的组同步，像 Codership 之前做的 Galera，像包括 MariaDB Galera Cluster 和 Percona XtraDB Cluster，都把 Galera 组件放在产品当中。包括现在 MySQL 新版本中的复制技术实现了组同步 MGR。但是这些方向又有它的问题，比如说它的性能损耗非常显著，然后在多写场景的冲突的处理复杂性，以及整个集群的扩展规模，受到这样的限制。\n\n## 分布式数据库备份容灾的挑战\n\n所以，从单机数据库进入到分布式数据库的领域，问题的挑战就更加大了。集中来看的话，就是比如说我们最常见的两个传统的分布式的数据库的架构：MySQL、PG 加上主从复制核心组件，再加一个高可用的外部组件实现 Failover/Switchover，然后再加分库分表中间件。那么这样的方案，在传统的分布式架构当中，它的核心的可用性的技术限制和天花板没有变化。它还是如前文所说的：主从复制加上一个数据库外部组件实现 Failover/Switchover 。\n\n**然后，在分布式数据库架构里，我们需要非常认真的去考虑，分布式数据库的伸缩能力和它怎么样去跟高可用及容灾的要求达到平衡。甚至还要想怎么样再去做进一步的优化，这是比较困难和有挑战的。**\n\n另外，在互联网应用场景中， 访问量和数据量非常的大，业务逻辑相对简单。**但是在银行、保险、证券等这样的传统关键金融场景当中，业务的逻辑是非常复杂的。针对于这样的传统高可用及灾备容灾方案，它与应用进行适配，往往要做一些面向应用的反向适配，应用还需要为此进行调整与妥协**。比如说两地三中心的场景当中，应用适配的难度更加大，所以改造过程当中的适配过程和反向适配的风险，也是一个经常让金融行业的 IT 从业人员非常头痛的一件事情。\n\n最严峻的一个事情，当在这样的传统灾备容灾方案为基底的一套系统在运行的过程当中，真的发生了非预期的重大故障和灾难突发的时候，怎么样保证数据的严格安全，以及如何保证在故障发生以后，对于部分组件，对于机房级的灾难，对于中心级的灾难，在灾难发生的时候，要保证对业务的最小影响，也就是我们经常听到的，RPO 和 RTO 这样的要求。那么在这个过程当中，怎么样最大程度减少人工的干预？因为，**在灾难发生的时候，人的干预是必须的，但是人的反应也是比较迟钝的。所以，怎么样通过一个技术手段，在整个方案的能力上，能够去高效执行灾难恢复的处理工作？**\n\n## TiDB 的金融级备份及容灾之道\n\nTiDB 经常这么多年的积累和逐渐完善，在整个分布式数据库的容灾和灾备的领域，我们达到了金融生产级的要求。那么在整个 TiDB 的备份与灾备、容灾的体系里，我们主要是由以下几个方面来组成的。\n\n![1-TiDB-金融级备份及容灾之道](https://img1.www.pingcap.com/prod/1_Ti_DB_6ddab9cf1a.png)\n\n**第一个是我们默认的，也是我们推荐的，多中心的容灾方案，同城的两中心，异地的一中心，或者扩展到三地五中心模式**。这个方案也是 TiDB 最早原生的核心方案。通过多级标签的感知，能够实现服务器级、机架级、机房级、站点级的故障转移。能达到 RPO 等于 0，以及我们的故障影响时间小于 30 秒，也就是 RTO 小于 30 秒的一个刚性指标。\n\n这一套方案，目前我们在国内和国外，已经有了不少用户，尤其是金融行业的用户，在关键场景投产使用了。这其中也是经受过了很多的考验，比如说，同城光纤的抖动，同城到异地之间的通讯线路出现问题，以及机房里面多个节点同时出现了故障，随着在生产环境上持续运行时间的变长，这些问题都暴露出来了。通过 TiDB 的多中心的容灾方案，非常可靠地避免了这些故障对业务的影响，保障了业务连续性及数据安全。\n\n除此之外，在国内的话，从北到南，我们的运营商的线路也是非常的复杂。对于有些用户来说，从投资成本、业务的重要性、客户网络的物理条件来说，没有办法去构成同城多中心加异地的的容灾架构，他可能只能选择两中心的方案，那么在这个过程当中的话，TiDB 经过这几年对这个方面的积累，我们现在已经有了**两中心的容灾方案**，并且，在这个方案里面，我们有多种配置来适应不同的网络条件。即便是在两中心方案当中，我们也能达到 RPO 等于 0 的保护模式。当然也有一些用户的场景，他的网络线路可能延迟非常高，且用户要求有一个托底的容灾方案，同时对于数据的一致性可以有略微的放松和让步，在这个过程当中，**我们也会通过配置来为其提供异步同步的模式，来帮助其实现托底的容灾方案，最大程度保障服务的连续性和数据安全。**\n\n以上是我们交付给用户的多种金融生产级的灾备容灾的方案，它背后的支撑是由核心的 **TiDB 的 Multi Raft 的高可用机制**，以及一系列针对跨中心的调度、数据的调度管理、故障的自动转移判断等这一整套后台的保障技术机制来实现的。\n\n另外，对于数据业务来说，除了在线的热的故障转移、切换等。我们对于数据库的数据本身，也提供了完善的数据备份方案，除了全量的备份、增量、恢复时间点 (PITR ) 之外，**我们在数据的备份模式上面，也提供了包括基于日志的传统逻辑备份。并且，在去年我们也推出了 TiDB BR 工具和备份方案，直接从数据库的 TiDB 存储引擎 TiKV 层上，直接实现备份和恢复，备份与恢复非常高的效率。**\n\n但光有上述方案是不不够的，PingCAP 对于自身产品的要求是非常严格的，既然是要达到金融生产级的要求，除了要有对应的技术方案、对应的技术实现之外，必须为产品本身提供专业的分布式测试的体系和手段。每一个 TiDB 的版本，在我们的内部，都会通过极其严格及复杂的分布式数据库测试。为此，我们也专门根据混沌工程，设计开发了自己的一套测试平台，并且在最近把它开源了，这套工具叫做 [Chaos Mesh](https://pingcap.com/blog-cn/chaos-mesh/)，可以帮助用户更好的检测分布式系统的可用性和鲁棒性。\n\n在 TiDB 内部测试的整条链路上，我们有非常完善的对于可靠性和一致性和安全方面的测试保障。包括自动化故障注入，包括我们引入的 Jepsen 的一致性验证，包括我们对于一些最核心的算法，引入了 TLA+ 的验证。还有我们每天在数据中心，在我们测试环境，不停跑的自动化模拟的业务负载以及各种各样的集成测试。我们相信只要是人写出来的软件，一定是会有问题的，一定会有 Bug，不可能做到完全没有问题。**所以，在这个过程当中，需要的保障手段，除了高可用和软件架构本身设计的机制之外，先进的、完善的、强大的产品测试体系和可靠性 验证能力，也是最重要的保障手段之一。**\n\n## TiDB 灾备与容灾的核心机制\n\nTiDB 容灾的核心机制是我们的 Raft ，相信各位关注 TiDB 的朋友也通过我们的公众号、官网，包括社区里面，听到过小伙伴们提供的分享。Raft 是基于日志与状态机的一种一致性的算法。我们基于它，在 TiDB 里实现了 Multi Raft 的机制。它能够非常可靠的管理我们的数据与数据的副本，在 TiDB 里面，整个数据对我们的业务来说是自动的透明打散的，然后它会以一个一个 Region 的数据组织方式，在不同的存储节点上面进行自动存储和建立 Raft 副本。\n\n通过 Multi Raft 这样的一个机制，我们可以在不同的主机，不同的机房，不同的园区，一套数据库不同的节点上，产生它的第二、第三副本，甚至更多的副本。这个副本是动态可调的，并且我们可以保证，TiDB 上执行的所有的联机交易事务，在数据变更发生时都可以达到多数的一致，**也就是说在一个实施规划和部署正确的 TiDB 集群里面，在一个多中心的灾备容灾 TiDB 集群中，任何的主机，所在的机架、机房，乃至数据中心的失效，包括数据中心间的网络故障，通过 Multi Raft 机制以及 TiDB 的高可用调度机制，都可以完善的去保障，对我们的业务影响最小，同时，非常严格的保证了数据的绝对安全。**\n\n在 Raft 这个机制上，TiDB 的研发团队也做了大量的优化工作。比如说跨数据中心，包括跨区域的运维方式。另外，我们在 Raft 机制上面，也提供了很多的比如说新的一些增强化，像 Lazy Peer、像 Learner、像 Joint consensus 等机制的研发。\n\n## TiDB 高性能分布式备份机制\n\n刚刚我还提到一个叫 [TiDB BR](https://pingcap.com/blog-cn/cluster-data-security-backup/) 的工具，它是一个在存储层实现高性能分布式数据备份恢复的工具。所以，大家由下图可以看到，我们的测试中的备份速度、恢复速度，都是非常惊人的。而且随着节点数量的增加，在数据量一定的前提下，备份/恢复的性能都会有接近线性的增长：\n\n![2-TiDB高性能分布式备份机制](https://img1.www.pingcap.com/prod/2_Ti_DB_875ad97021.png)\n\n\n## 最强的多中心：TiDB 多中心多活容灾方案\n\n在多中心里面，前面提到，我们通过 Multi Raft 的机制，以及相关的工程优化，实现了跨中心的容灾方案。比如说，对于长距离的异地中心，我们在同城设了两个中心，通过光纤连接，异地的话，通常租用比如运营商的 MSTP 类型的线路，构成一个三中心的结构，通过 TiDB 内置的容灾和灾备的相关的一系列的机制与手段，可以构建出非常强健的容灾的架构。**任何中心级失效，都会由另外两个中心来立刻进行故障的转移，以及对外继续提供正常的数据库服务。**\n\n\n![3-TiDB多中心多活容灾方案](https://img1.www.pingcap.com/prod/3_Ti_DB_36dac4fe16.png)\n\n\n## 安全的两中心：TiDB 两中心容灾方案\n\n前面也提到，有些用户的网络，比如说一个中心在上海，一个在北京，延迟是非常大。因为高等级、低延迟线路的租用使用成本非常高。综合考虑成本及所需要保护的业务的关键性等级，不少用户会做一个权衡。部分用户最终希望核心数据库，只需要完成一个主从站点的容灾和灾备就可以了。\n\n通过 TiDB Binlog 模式能够去适配和满足对多中心网络通信成本敏感且对服务/数据防护能力略低的容灾需求的用户，我们会用两中心的 **TiDB Binlog based 方案**，它是异步同步模式，能够适配两个中心长距离间隔其网络延迟较大，比如延迟大于 30~40 毫秒的。采纳这样方案的用户对于 RPO 的要求就比较宽松了，该模式是一个异步同步，当灾难发生的时候，我们已经在 TiDB 的工程优化上尽可能通过多种机制来减少数据丢失的可能，但是从根本上来说，还是会存在一定数据丢失的情况。**该方案提供给用户低成本的保障能力，同时还提供了比较灵活的可选拓扑，比如双向的环形复制等。**\n\n也有用户希望在两中心的方案里，需要有一个强一致保障的方案。所以我们研发了两中心的 **Raft based 容灾方案**，它可以在同城或者接近同城距离的两中心环境中，且中心间网络条件比较好的情况下，实现严格的强一致同步。**这个方案可以达到 RPO 等于 0 的保障要求，也就是数据不丢失前提下的一个高等级的容灾要求。**\n\n\n![4-TiDB两中心容灾方案](https://img1.www.pingcap.com/prod/4_Ti_DB_fdf9238a38.png)\n\n## 结语\n\n最后，我们一直持续在花非常多的精力和投入，研究如何让 TiDB 变得更强，更安全，更可靠。能够达到更好的金融级的数据服务的支撑能力水平，依托于我们整个工程研发团队、 QA 测试团队，以及我们所打造和拥有的强大的测试体系、TiDB 产品的容灾灾备一系列高可用及灾备容灾机制，我们能够为银行、保险、证券等金融客户提供完善的、可靠的、放心的、金融级的分布式数据库服务。\n\n\n>本文整理自余军在 [TiDB DevCon 2020](https://pingcap.com/community-cn/devcon2020/) 上的演讲，大会相关视频回顾可以关注官方 Bilibli 账号 [TiDB_Robot](https://space.bilibili.com/86485707) 。","author":"余军","category":1,"customUrl":"tidb-financial-grade-backup-and-multi-center-disaster-recovery","fillInMethod":"writeDirectly","id":5,"summary":"依托于整个工程研发团队、QA 测试团队，以及所打造和拥有的强大的测试体系、TiDB 产品的容灾灾备一系列高可用及灾备容灾机制，我们能够为银行、保险、证券等金融客户提供完善的、可靠的、放心的、金融级的分布式数据库服务。","tags":["TiDB DevCon 2020","备份恢复","容灾机制"],"title":"TiDB 金融级备份及多中心容灾"}},{"relatedBlog":{"body":">作者介绍：吕磊，Better 队成员、美团点评高级 DBA，Better 队参加了  TiDB Hackathon 2019，其项目「基于 TiDB Binlog 的 Fast-PITR」获得了最佳贡献奖。\n\n维护过数据库的同学应该都能体会，数据备份对于数据库来说可以说至关重要，尤其是关键业务。TiDB 原生的备份恢复方案已经在多家客户得到稳定运行的验证，但是对于业务量巨大的系统存在如下几个痛点:\n\n1.  集群中数据量很大的情况下，很难频繁做全量备份。\n\n2.  传统 [TiDB Binlog](https://pingcap.com/docs-cn/stable/reference/tidb-binlog/overview/) 原样吐出 binlog 增量备份会消耗大量的磁盘空间，并且重放大量 binlog 需要较长时间。\n\n3.  binlog 本身是有向前依赖关系的，任何一个时间点的 binlog 丢失，都会导致后面的数据无法自动恢复。\n\n4.  调大 TiDB gc_life_time 保存更多版本的快照数据，一方面保存时间不能无限长，另一方面过多的版本会影响性能且占用集群空间。 \n\n  ![图 1 原生 Binlog 备份恢复](https://img1.www.pingcap.com/prod/1_4ebc7080f0.png)\n  <div class=\"caption-center\">图 1 原生 binlog 备份恢复</div>\n  \n我们在线上使用 TiDB 已经超过 2 年，从 1.0 RC 版本到 1.0 正式版、2.0、2.1 以及现在的 3.0，我们能感受到 TiDB 的飞速进步和性能提升，但备份恢复的这些痛点，是我们 TiDB 在关键业务中推广的一个掣肘因素。于是，我们选择了这个题目:  基于 TiDB Binlog 的 Fast-PITR (Fast point in time recovery)，即基于 TiDB Binlog 的快速时间点恢复，实现了基于 TiDB Binlog 的逐级 merge，以最小的代价实现快速 PITR，解决了现有 TiDB 原生备份恢复方案的一些痛点问题。 \n\n## 方案介绍\n\n1.  根据互联网行业特征和 2/8 原则，每天真正会被更新的数据只有 20% 而且是频繁更新。我们也统计了线上万亿级别 DML 中 CUD 真实占比为 15:20:2，其中 update 超过了 50%。row 模式的 binlog 中我们只记录前镜像和最终镜像，可以得到一份非常轻量的“差异备份”，如图所示:    \n\n   ![图 2 Binlog merge 原则](https://img1.www.pingcap.com/prod/2_456bffb130.png)\n   <div class=\"caption-center\">图 2 binlog merge 原则</div>\n  \n2.  我们将 binlog 按照时间分段，举例说，每天的 binlog 为一个分段，每段按照上面的原则进行 merge，这段 binlog 合并后成为一个备份集，备份集是一些独立的文件。由于每一个备份集在 merge 阶段已经去掉了冲突，所以一方面对体积进行了压缩，另一方面可以以行级并发回放，提高回放速度，结合 full backup 快速恢复到目标时间点，完成 PITR 功能。而且，这种合并的另一个好处是，生成的备份集与原生 binlog file 可以形成互备关系，备份集能够通过原生 binlog file 重复生成。\n\n  ![图 3 binlog 并行回放](https://img1.www.pingcap.com/prod/3_eebbbf24af.png)\n  <div class=\"caption-center\">图 3 binlog 并行回放</div>\n    \n  binlog 分段方式可以灵活定义起点和终点:  \n  \n  ```\n  -start-datetime string\n        recovery from start-datetime, empty string means starting from the beginning of the first file\n  -start-tso int\n        similar to start-datetime but in pd-server tso format\n  -stop-datetime string\n        recovery end in stop-datetime, empty string means never end.\n  -stop-tso int\n        similar to stop-datetime, but in pd-server tso format\n  ```\n  \n3.  在此基础上，我们做了些优化:    \n\n  ![图 4 优化后](https://img1.www.pingcap.com/prod/4_96d4596b2a.png)\n  <div class=\"caption-center\">图 4 优化后</div>\n  \n  备份集的格式与 TiDB Binlog 相同，所以，备份集之间可以根据需要再次合并，形成新的备份集，加速整个恢复流程。  \n  \n## 实现方式\n\n### Map-Reduce 模型\n\n由于需要将同一 key（主键或者唯一索引键）的所有变更合并到一条 Event 中，需要在内存中维护这个 key 所在行的最新合并数据。如果 binlog 中包含大量不同的 key 的变更，则会占用大量的内存。因此设计了 Map-Reduce 模型来对 binlog 数据进行处理：\n\n![图 5 Binlog 合并方式](https://img1.www.pingcap.com/prod/5_ba3316d2c9.png)\n<div class=\"caption-center\">图 5 binlog 合并方式</div>\n \n *   Mapping 阶段：读取 Binlog file，通过 PITR 工具将文件按库名 + 表名输出，再根据 Key hash 成不同的小文件存储，这样同一行数据的变更都保存在同一文件下，且方便 Reduce 阶段的处理。\n\n*   Reducing 阶段：并发将小文件按照规则合并，去重，生成备份集文件。  \n\n  |  原 Event 类型  | 新 Event 类型  | 合并后的 Event 类型  |\n  |  ----  | ----  |----  |\n  | INSERT  | DELETE | Nil |\n  | INSERT  | UPDATE |INSERT |\n  | UPDATE  | DELETE | DELETE |\n  | UPDATE  | UPDATE | UPDATE |\n  | DELETE  | INSERT | UPDATE |\n  \n*   配合官方的 reparo 工具，将备份集并行回放到下游库。\n\n### DDL 的处理\n\nDrainer 输出的 binlog 文件中只包含了各个列的数据，缺乏必要的表结构信息（PK/UK），因此需要获取初始的表结构信息，并且在处理到 DDL binlog 数据时更新表结构信息。DDL 的处理主要实现在 DDL Handle 结构中：\n\n![图 6 DDL 处理](https://img1.www.pingcap.com/prod/6_8e6d11a8c8.png)\n<div class=\"caption-center\">图 6 DDL 处理</div>\n\n\n首先通过配置 TiDB 的 Restful API 获取 TiKV 中保存的历史 DDL 信息，通过这些历史 DDL 获取 binlog 处理时的初始表结构信息，然后在处理到 DDL binlog 时更新表结构信息。\n\n由于 DDL 的种类比较多，且语法比较复杂，无法在短时间内完成一个完善的 DDL 处理模块，因此使用 [tidb-lite](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FWangXiangUSTC%2Ftidb-lite) 将 mocktikv 模式的 TiDB 内置到程序中，将 DDL 执行到该 TiDB，再重新获取表结构信息。\n\n## 方案总结\n\n1.  恢复速度快：merge 掉了中间状态，不但减少了不必要的回放操作，且实现了行级并发。\n\n2.  节约磁盘空间：测试结果表明，我们的 binlog 压缩率可以达到 30% 左右。\n\n3.  完成度高：程序可以流畅的运行，并进行了现场演示。\n\n4.  表级恢复：由于备份集是按照表存储的，所以可以随时根据需求灵活恢复单表。\n\n5.  兼容性高：方案设计初期就考虑了组件的兼容性，PITR 工具可以兼容大部分的 TiDB 的生态工具。\n\n\n## 方案展望\n\n\nHackathon 比赛时间只有两天，时间紧任务重，我们实现了上面的功能外，还有一些没来得及实现的功能。\n\n### 增量与全量的合并\n\n![图 7 方案展望](https://img1.www.pingcap.com/prod/7_0598d2c19c.png)\n<div class=\"caption-center\">图 7 方案展望</div>\n\n增量备份集，逻辑上是一些 insert+update+delete 语句。\n\n全量备份集，是由 mydumper 生成的 create schema+insert 语句。\n\n我们可以将增量备份中的 insert 语句前置到全量备份集中，全量备份集配合 [Lightning 工具](https://pingcap.com/docs-cn/stable/reference/tools/tidb-lightning/overview/) 急速导入到下游 TiKV 集群，Lightning 恢复速度是逻辑恢复的 5 - 10 倍 ，再加上一份更轻量的增量备份集 (update+delete) 直接实现 PITR 功能。 \n\n### DDL 预处理\n\nPIRT 工具实际上是一个 binlog 的 merge 过程，处理一段 binlog 期间，为了保证数据的一致性，理论上如果遇到 DDL 变更，merge 过程就要主动断掉，生成备份集，再从这个断点继续 merge 工作，因此会生成两个备份集，影响 binlog 的压缩率。\n\n为了加速恢复速度，我们可以将 DDL 做一些预处理，比如发现一段 binlog 中包含某个表的 Drop table 操作，那么完全可以将 Drop table 前置，在程序一开始就忽略掉这个表的 binlog 不做处理，通过这些“前置”或“后置”的预处理，来提高备份和恢复的效率。\n\n![图 8  DDL 预处理](https://img1.www.pingcap.com/prod/8_778a048587.png)\n<div class=\"caption-center\">图 8  DDL 预处理</div>\n\n## 结语\n\n我们是在坤坤（李坤）的热心撮合下组建了 Better 战队，成员包括黄潇、高海涛、我，以及 PingCAP 的王相同学。感谢几位大佬不离不弃带我飞，最终拿到了最佳贡献奖。比赛过程惊险刺激（差点翻车），比赛快结束的时候才调通代码，强烈建议以后参加 Hackathon 的同学们一定要抓紧时间，尽早完成作品。参赛的短短两天让我们学到很多，收获很多，见到非常多优秀的选手和炫酷的作品，我们还有很长的路要走，希望这个项目能继续维护下去，期待明年的 Hackathon 能见到更多优秀的团队和作品。","author":"吕磊","category":4,"customUrl":"fast-pitr-based-on-binlog","fillInMethod":"writeDirectly","id":103,"summary":"基于 TiDB Binlog 的 Fast-PITR (Fast point in time recovery)，即基于 TiDB Binlog 的快速时间点恢复，实现了基于 TiDB Binlog 的逐级 merge，以最小的代价实现快速 PITR，解决了现有 TiDB 原生备份恢复方案的一些痛点问题。","tags":["TiDB Binlog"],"title":"直击备份恢复的痛点：基于 TiDB Binlog 的快速时间点恢复"}},{"relatedBlog":{"body":"BR 选择了在 Transaction KV 层面进行扫描来实现备份。这样，备份的核心便是分布在多个 TiKV 节点上的 MVCC Scan：简单，粗暴，但是有效，它生来就继承了 TiKV 的诸多优势：分布式、利于横向拓展、灵活（可以备份任意范围、未 GC 的任意版本的数据）等等优点。\n \n相较于从前只能使用 mydumper 进行 SQL 层的备份，BR 能够更加高效地备份和恢复：它取消了 SQL 层的开销，同时支持备份索引，而且所有备份都是已经排序的 SST 文件，以此大大加速了恢复。\n\nBR 的实力在之前的文章（[https://pingcap.com/zh/blog/cluster-data-security-backup](https://pingcap.com/zh/blog/cluster-data-security-backup)）中已经展示过了，本文将会详细描述 BR 备份侧的具体实现。简单来讲，BR 就是备份的“算子下推”：通过 gRPC 接口，将任务下发给 TiKV，然后让 TiKV 自己将数据转储到外部存储中。\n## BR 的基本流程\n\n![1.png](https://img1.www.pingcap.com/prod/1_33ce6793a6.png)\n\n### 接口\n为了区别于一般的 MVCC Scan 请求，TiKV 提供一个叫做 `Backup` 的接口，这个接口与一般的读请求不同——它不会返回数据给客户端，而是直接将读到的数据存储到指定的存储器（External Stroage）中：\n\n```\nservice Backup {\n    // 收到 backup 的 TiKV，将会将 Request 指定范围中，所有自身为 leader\n    // 的 region 备份，并流式地返回给客户端（每个 region 对应流中的一个 item）。\n    rpc backup(BackupRequest) returns (stream BackupResponse) {}\n}\n\n// NOTE：隐藏了一些不重要的 field 。\nmessage BackupRequest {\n    // 备份的范围，[start_key, end_key)。\n    bytes start_key = 2;\n    bytes end_key = 3;\n    // 备份的 MVCC 版本。\n    uint64 start_version = 4;\n    uint64 end_version = 5;\n    \n    // 限流接口，为了确保和恢复时候的一致性，限流限制保存备份文件的阶段的速度。\n    uint64 rate_limit = 7;\n    \n    // 备份的目标存储。\n    StorageBackend storage_backend = 9;\n    // 备份的压缩 -- 这是一个用 CPU 资源换取 I/O 带宽的优化。\n    CompressionType compression_type = 12;\n    int32 compression_level = 13;\n    // 备份支持加密。\n    CipherInfo cipher_info = 14;\n}\n\nmessage BackupResponse {\n    Error error = 1;\n    // 备份的请求将会返回多次，每次都会返回一个已经完成的子范围。\n    // 利用这些信息可以统计备份进度。\n    bytes start_key = 2;\n    bytes end_key = 3;\n    // 返回该范围备份文件的列表，用于恢复的时候追踪。\n    repeated File files = 4;\n}\n```\n\n### 客户端\nBR 客户端会借助 TiDB 的接口，根据用户指定需要备份的库和表，计算出来需要备份的范围（“ranges”）。计算的依据是：\n\n1. 依据每个 table 的所有 data key 生成 range。（所有带有 `t{table_id}_r` 前缀的 Key）\n\n2. 依据每个 index 的所有 index key 生成 range。（所有带有 `t{table_id}_i{index_id}` 前缀的 Key）\n\n3. 如果 table 存在 partition（这意味着，它可能有多个 table ID），对于每个 partition，按照上述规则生成 range。\n\n为了获得最大的并行度，BR 客户端会并行地向所有 TiKV 发送这些 Range 上的备份请求。\n\n当然，备份不可能一帆风顺。我们在备份的时候不可避免地会遇到问题：例如网络错误，或者触发了 TiKV 的限流措施（Server is Busy），或者 Key is Locked，这时候，我们必须缩小这些 range，重新发送请求（否则，我们就要重复一遍之前已经做过的工作……）。\n\n在失败之后，选择合适的 range 来重发请求的过程，在 BR 中被称为 “细粒度备份(fine-grained backup)”，具体而言：\n\n1.在之前的 “粗粒度备份” 中，BR 客户端每收到一个 `BackupResponse` 就会将其中的 `[start_key, end_key)` 作为一个 range 存入一颗区间树中（你可以把它想象成一个简单的 `BTreeSet<(Vec<u8>, Vec<u8>)>`）。\n\n2.“粗粒度备份” 遇到任何可重试错误都会忽略掉，只是相应的 range 不会存入这颗区间树中，因此树中会留下一个 “空洞”，这两步的伪代码如下。\n\n```\nfunc Backup(tree RangeTree) {\n    // ... \n    for _, resp := range responses {\n        if resp.Success {\n            tree.Insert(resp.StartKey, resp.EndKey)  \n        }\n    }\n}\n\n// An example: \n// When backing up the ange [1, 5).\n// [1, 2), [3, 4) and [4, 5) successed, and [2, 3) failed:\n// The Tree would be like: { [1, 2), [3, 4), [4, 5) }, \n// and the range [2, 3) became a \"hole\" in it.\n// \n// Given the range tree is sorted, it would be easy to \n// find all holes in O(n) time, where n is the number of ranges.\n```\n\n3.在 “粗粒度备份” 结束之后，我们遍历这颗区间树，找到其中所有 “空洞”，并行地进行 “细粒度备份”：\n\n* 找到包含该空洞的所有 region。\n  \n* 对他们的 leader 发起 region 相应范围的 Backup RPC。\n  \n* 成功之后，将对应的 range 放入区间树中。\n  \n4.在一轮 “细粒度备份” 结束后，如果区间树中还有空洞，则回到 (3)，在超过一定次数的重试失败之后，报错并退出。\n\n在上述 “备份”流程完成之后，BR 会利用 Coprocessor 的接口，向 TiKV 请求执行用户所指定表的 checksum。\n\n这个 checksum 会在恢复的时候用作参考，同时也会和 TiKV 在备份期间生成的逐文件的 checksum 进行对比，这个比对的过程叫做 “fast checksum”。\n\n在 “备份” 的过程中，BR 会通过 TiDB 的接口收集备份的**表结构、备份的时间戳、生成的备份文件**等信息，储存到一个 “backupmeta”中。这个是恢复时候的重要参考。\n\n### TiKV\n为了实现资源隔离，减少资源抢占，Backup 相关的任务都运行在一个单独的线程池里面。这个线程池中的线程叫做 “bkwkr”（“backup worker” 极其抽象的缩写）。\n\n在收到 gRPC 备份的请求之后，这个 `BackupRequest` 会被转化为一个 `Task`。\n\n而后，TiKV 会利用 `Task` 中的 `start_key` 和 `end_key` 生成一个叫做 “`Progress`”的结构：它将会把 `Task` 中庞大的范围划分为多个子范围，通过：\n\n1. 扫描范围内的 Region。\n2. 对于其中当前 TiKV 角色为 `Leader` 的 Region，将该 Region 的范围作为 Backup 的子任务下发。\n\n`Progress` 提供的接口是一个使用 “拉模型” 的接口：`forward`。随后，TiKV 创建的各个 Backup Worker 将会去并行地调用这个接口，获得一组待备份的 Region，然后执行以下三个步骤：\n\n1. 对于这些 Region，Backup Worker 将会通过 `RaftKV` 接口，进行一次 Raft 的读流程，最终获得对应 Region 在 Backup TS 的一个 Snapshot。(“Get Snapshot”)\n2. 对于这个 Snapshot，Backup Worker 会通过 MVCC Read 的流程去扫描 `backup_ts` 的一致版本。这里我们会扫描出 Percolator 的事务，为了恢复方便，我们会准备 “default” 和 “write” 两个临时缓冲区，分别对应 TiKV Percolator 实现中的 Default CF 和 Write CF。(“Scan”)\n3. 然后，我们会先将扫描出来的事务中两个 CF 的 Raw Key 刷入对应缓冲区中，在整个 Region 备份完成（或者有些 Region 实在过大，那么会在途中切分备份文件）之后，再将这两个文件存储到外部存储中，记录它们对应的范围和大小等等，最后返回一个 `BackupResponse` 给 BR。(“Save”)\n\n为了保证文件名的唯一性，备份的文件名会包括当前 TiKV 的 store ID、备份的 region ID、start key 的哈希、CF 名称。\n\n备份文件使用 RocksDB 的 Block Based SST 格式：它的优势是，原生支持文件级别的 checksum 和压缩，同时可以在恢复的时候快速被 ingest 的潜力。\n\n外部存储是为了适配多种备份目标而存在的通用储存抽象：有些类似于 Linux 中的 VFS，不过简化了非常多：仅仅支持简单的保存和下载整个文件的操作。它目前对主流的云盘都做了适配，并且支持以 URL 的形式序列化和反序列化。例如，使用 `s3://some-bucket/some-folder`，可以指定备份到 S3 云盘上的 `some-bucket` 之下的 `some-folder` 目录中。\n\n## BR 的挑战和优化\n通过以上的基本流程，BR 的基本链路已经可以跑通了：类似于算子下推，BR 将备份任务下推到了 TiKV，这样可以合理利用 TiKV 的资源，实现分布式备份的效果。\n\n在这个过程中，我们遇到了许多挑战，在这一节，我们来谈谈这些挑战。\n\n### BackupMeta 和 OOM\n前文中提到，BackupMeta 储存了备份的所有元信息：包括表结构、所有备份文件的索引等等。想象一下你有一个足够大的集群：比如说，十万张表，总共可能有数十 TB 的数据，每张表可能还有若干索引。\n\n如此最终可能产生数百万的文件：在这个时候，BackupMeta 可能会达到数 GB 之大；另一方面，由于 protocol buffer 的特性，我们可能不得不读出整个文件才能将其序列化为 Go 语言的对象，由此峰值内存占用又多一倍。在一些极端环境下，会存在 OOM 的可能性。\n\n为了缓解这个问题，我们设计了一种分层的 BackupMeta 格式，简单来讲，就是将 BackupMeta 拆分成索引文件和数据文件两部分，类似于 B+ 树的结构：\n\n![2.png](https://img1.www.pingcap.com/prod/2_c743455d25.png)\n\n具体来讲，我们会在 BackupMeta 中加上这些 Fields，分别指向对应的 “B+ 树” 的根节点：\n\n```\nmessage BackupMeta {\n    // Some fields omitted...\n    // An index to files contains data files.\n    MetaFile file_index = 13;\n    // An index to files contains Schemas.\n    MetaFile schema_index = 14;\n    // An index to files contains RawRanges.\n    MetaFile raw_range_index = 15;\n    // An index to files contains DDLs.\n    MetaFile ddl_indexes = 16;\n}\n```\n\nMetaFile 就是这颗 “B+ 树” 的节点：\n\n```\n// MetaFile describes a multi-level index of data used in backup.\nmessage MetaFile {\n    // A set of files that contains a MetaFile.\n    // It is used as a multi-level index.\n    repeated File meta_files = 1;\n    \n    // A set of files that contains user data.\n    repeated File data_files = 2;\n    // A set of files that contains Schemas.\n    repeated Schema schemas = 3;\n    // A set of files that contains RawRanges.\n    repeated RawRange raw_ranges = 4;\n    // A set of files that contains DDLs.\n    repeated bytes ddls = 5;\n}\n```\n\n它可能有两种形态：一是承载着对应数据的 “叶子节点”（后四个 field 被填上相应的数据），也可以通过 `meta_files` 将自身指向下一个节点：`File` 是一个到外部存储中其他文件的引用，包含文件名等等基础信息。\n\n目前的实现中，为了回避真正实现类似 B 树的分裂、合并操作的复杂性，我们仅仅使用了一级索引，将的表结构和文件的元数据分别存储到一个个 128M 的小文件中，如此已经足够回避 BackupMeta 带来的 OOM 问题了。\n\n### GC, GC never changes\n在备份扫描的整个过程中，因为时间跨度较长，必然会受到 GC 的影响。\n不仅仅是 BR，别的生态工具也会遇到 GC 的问题：例如，TiCDC 需要增量扫描，如果初始版本已经被 GC 掉，那么就无法同步一致的数据。\n\n过去我们的解决方案一般是让用户手动调大 GC Lifetime，但是这往往会造成 “初见杀” 的效果：用户开开心心备份，然后去做其他事情，几个小时后发现备份因为 GC 而失败了……\n\n这会非常影响用户的心情：为了让用户能更加开心地使用各种生态工具，PD 提供了一个叫做 “Service GC Safepoint” 的功能。各个服务可以通过 PD 上的接口，设置一个 “Safepoint”，TiDB 会保证，在 Safepoint 指定的时间点之后，所有历史版本都不会被 GC。为了防止 BR 在意外退出之后导致集群无法正常 GC，这个 Safepoint 还会存在一个 TTL：在指定时间之后若是没有刷新，则 PD 会移除这个 Service Safe Point。\n\n对于 BR 而言，只需要将这个 Safepoint 设置为 Backup TS 即可，作为参考，这个 Safepoint 会被命名为 “br-<Random UUIDv4>”，并且有五分钟的 TTL。\n\n### 备份压缩\n在全速备份的时候，备份的流量可能相当大：具体可以看看开头“秀肌肉”文章相关的部分。\n\n如果你愿意使用足够多的核心去备份，那么可能很快就会到达网卡的瓶颈（例如，如果不经压缩，千兆网卡大约只需要 4 个核心就会被满。），为了避免网卡成为瓶颈，我们在备份的时候引入了压缩。\n\n我们复用了 RocksDB Block Based Table Format 中提供的压缩功能：默认会使用 zstd 压缩。压缩会增大 CPU 的占用率，但是可以减少网卡的负载，在网卡成为瓶颈的时候，可以显著提升备份的速度。\n\n### 限流与隔离\n为了减少对其他任务的影响，如前文所说，所有的备份请求都会在单独的线程池中执行。\n\n但是即便如此，如果备份消耗了太多的 CPU，不可避免地会对集群中其它负载造成影响：主要的原因是 BR 会占用大量 CPU，影响其它任务的调度；另一方面则是 BR 会大量读盘，影响写任务刷盘的速度。\n\n为了减少资源的使用，BR 提供了一个限流机制。当用户带有 --ratelimit 参数启动 BR 的时候，TiKV 侧的第三步“Save”，将会被限流，与此同时也会限制之前步骤的流量。\n\n这里需要注意一个点：备份数据的大小往往会远远小于集群的实际空间占用。原因是备份只会备份单副本、单 MVCC 版本的数据。通 ratelimit 限流施加于 Save 阶段，因此是限制写备份数据的速度。\n\n在 “服务端” 侧，也可以通过调节线程池的大小来限流，这个参数叫做 backup.num-threads，考虑到我们允许用户侧限流，它的默认值非常高：是全部 CPU 的 75%。如果需要在服务侧进行更加彻底的限流，可以修改这个参数。作为参考，一块 Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz CPU 每个核心大概每秒能生成 10M 经 zstd 压缩的 SST 文件。 \n\n## 总结\n通过 Service Safe Point，我们解决了手动调节 GC 带来的“难用”的问题。\n\n通过新设计的 BackupMeta，我们解决了海量表场景的 OOM 问题。\n通过备份压缩、限流等措施，我们让 BR 对集群影响更小、速度更快（即便二者可能无法兼得）。\n\n总体上而言，**BR 是在 “物理备份” 和 “逻辑备份” 之间的 “第三条路”**：相对于 mydumper 或者 dumpling 等工具，它消解了 SQL 层的额外代价；相对于在分布式系统中寻找物理层的一致性快照，它易于实现且更加灵巧。对于目前阶段而言，是适宜于 TiDB 的容灾备份解决方案。\n","author":"余峻岑","category":1,"customUrl":"br-introduction","fillInMethod":"writeDirectly","id":328,"summary":"本文将会详细描述 BR 备份侧的具体实现。简单来讲，BR 就是备份的“算子下推”：通过 gRPC 接口，将任务下发给 TiKV，然后让 TiKV 自己将数据转储到外部存储中。","tags":["BR","备份恢复"],"title":"备份的 “算子下推”：TiDB BR 简介"}}]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}