快速掌握 PolarDB-X 拆分规则变更能力!

作者:姑胥



背景


PolarDB-X作为一款云原生分布式关系型数据库[6],支持通过拆分规则将一张逻辑表的数据分布在多个数据节点中。同时,也支持变更拆分规则,将数据进行重新分布[9]。数据重新分布(Repartition)的能力是分布式数据库的核心能力之一。当业务规模扩大时,它可以将数据分布到更多的节点,以达到水平扩展(ScaleOut)的目的。当业务规则剧烈变化时,它也可以将数据以新的拆分规则分布,从而提高查询的性能,更好地适应新的业务规则。下图展示如何将一张单表,通过一条简单的DDL语句online地变更为一个分库分表。


快速掌握 PolarDB-X 拆分规则变更能力!



数据如何重新分布?


其实早在分布式数据库中间件大行其道的年代,开发人员就尝试过各种方式来解决数据Repartition的问题。但是无一例外,这些方案都非常复杂和危险,开发人员通常只能在半夜流量低峰期做Repartition,前前后后需要准备好详细的设计、论证、回滚方案。并且基于分布式的特点,在新老数据切换的那一段时间,几乎总是得在数据一致性和可用性之间做一个痛苦的选择。所以很多方案会有一个停写阶段或数据比对阶段,或两者皆有。


PolarDB-X充分论证了分布式数据库Repartition的各项技术细节后,能够做到Repartition过程中数据强一致、高可用、对业务透明、一条简单DDL语句搞定。



数据Repartition的关键问题


本文讨论的主要是表级别的数据Repartition。比如:将一张拆分表的数据按照新的规则重新分布到不同的数据节点。但是重新分布的过程需要时间,这段时间内又会有新的增量数据进入系统。当所有的数据都重新分布完成后,要将新老数据进行切换,最后停止双写并删掉所有的老数据。仔细研究上述过程的细节后,可以将其概括为这3个关键子问题,后续我们将按照这3个子问题评估各个实现方案:


  1. 如何进行存量、增量的数据同步
    1. 比如:应用层双写、数据同步中间件、数据库触发器等。
  1. 如何进行流量切换
    1. 主要考虑读写流量在新老数据中的迁移。
  1. 如何控制Repartition的整体流程
    1. 主要考虑如何组合各个步骤,比如什么时候开启/停止增量数据同步,什么时候进行流量切换。
    2. 如果故障了怎么恢复或回滚等。



传统的Repartition方案


快速掌握 PolarDB-X 拆分规则变更能力!


一个传统的Repartition流程大概是这样的:


  1. 应用集群初始状态的读写流量都在老的数据表上。
  2. 增加新表并开启双写。双写方式可能是应用层双写,或基于binlog数据同步,或数据库触发器实现双写等。但这个步骤,如果没有事务保证的话就很容易造成数据不一致。
  3. 启动存量数据同步。
  4. 可能会启动一个数据比对服务,如果有数据不一致的话,也需要人工介入处理。
  5. 当存量数据同步完成,增量数据追平后,将读流量切到新表,保持双写一段时间。
  6. 撤走老表的写流量,完成Repartition


但是如果我们仔细推敲一下,发现很多步骤根本就没办法保障数据一致性:


  1. 上述第2步,开启双写时,分布式系统无法保证所有节点都一起开启双写。所以一定有一段时间只有部分节点开启了双写。那就会出现Orphan Data Anomaly的问题(后文将说明)[2][8],造成新老表的数据不一致。传统的Repartition方案要解决这个问题的话,只能停写。
  2. 同理,上述第6步撤走老表的写流量时,也会出现数据不一致问题。
  3. 上述第2步,增量数据的双写一致性难以保障。


除了数据一致性问题外,依然存在很多繁琐但依然十分重要的问题:


  1. 整个流程基本由人工串联,多个环节需要人工介入。所以前期需要学习和理解成本,过程中也需要参与成本。
  2. 传统方案如果要避免数据不一致问题,在关键节点需要停写,这会对业务造成干扰。
  3. 整个流程十分脆弱地粘合在了一起,容错性非常差。



PolarDB-X的Repartition方案


PolarDB-X 是一款存储与计算分离的分布式数据库,所以在架构中会分为计算节点(CN)和存储节点(DN),以及一个元数据库(GMS)。CN负责SQL的parse、optimize、execute;DN负责数据存储;GMS负责存储元数据。为了性能考虑,每个CN节点都会缓存一份元数据。


快速掌握 PolarDB-X 拆分规则变更能力!


如何进行存量、增量数据同步


增量数据双写


在分布式的增量数据双写场景中,双写的2端经常会分布在不同的DN节点中,所以自然地单机事务无法使用。而前文已经论证过,XA事务、binlog同步、触发器都无法保证双写的2端数据强一致。PolarDB-X使用了内置的基于TSO的分布式事务[7](????感兴趣的同学可以点击直达文章实现增量数据同步,从而保证了Repartition过程中,任意时刻读流量切换的数据的强一致性。


值得注意的是,如果一行数据的拆分列的值被修改了,那这行数据可能会路由到另一个数据节点[1]。所以这时候执行的其实是:原数据节点的delete操作+新数据节点的insert操作。在Repartition过程中,因为前后的拆分规则不同,所以一行数据的update操作,可能会演变成4个数据节点参与的分布式事务(如果有全局二级索引会更多)。PolarDB-X会处理好所有这些工作,用户无需任何感知,可将PolarDB-X视为一个单机数据库操作。


存量数据同步


在存量数据同步时,PolarDB-X会分段进行同步。每一段同步过程中,PolarDB-X会在TSO事务中,先尝试获取源端数据的S锁,然后再写入目标端。如果目标段已经有相同数据,则表明增量双写阶段已经将这些数据同步过了,忽略即可。


分布式事务与单机事务一样,会产生死锁。当存量同步而给原表某段数据加S锁时,如果业务Update流量较大,可能导致分布式死锁的发生,PolarDB-X有一个分布式死锁检测模块可以解决这个问题。死锁解除后,存量同步模块会重试。


如何进行流量切换


Online Schema Change


首先我们来看一下前文提到的Orphan Data Anomaly问题[2]是怎么发生的:当开启增量数据双写时,PolarDB-X的CN节点内存中的元数据不是同时刷新的,而是有一个先后顺序,所以一定存在一段时间,某些CN节点开启了增量双写,而另一些没有。于是会出现这样的情况:


  1. 计算节点CN0已开启双写,并在老表和新表分别写入了3条数据,如下图
  2. 计算节点CN1未开启双写,执行了delete语句,删除了id为3的一条数据,但是未删除新表的数据
  3. 新表和老表出现了数据不一致


快速掌握 PolarDB-X 拆分规则变更能力!


这一问题在Google的Online, Asynchronous Schema Change in F1这篇论文中有过详细的论证。PolarDB-X引入了Online Schema Change(????感兴趣的同学可以点击直达文章)以解决此类的问题,在此不再赘述。对于Repartition来说,引入下图中的这些状态,以此保证任意2个相邻状态都是兼容的,不会发生数据一致性问题。我们可以具体来看看其中几个最关键的状态:


  • target_delete_only和target_write_only:如上所述,当我们有多个CN节点时,直接开启增量双写会导致Orphan Data Anomaly问题。所以在开启双写之前,我们先让所有的CN节点都达到target_delete_only状态,然后再达到target_write_only状态(就是双写状态)。CN节点在target_delete_only下,只会执行delete语句(如果是update,则转换成delete执行)。代入上图中的例子:CN1会先达到target_delete_only状态,所以即便未开启双写,也能够删除新表中id为3的数据,从而保证了数据的一致性。
  • source_delete_only和source_absent:前文也论述过,如果直接停掉老表的双写,会造成数据不一致。所以我们在source_absent(也就是停止了双写的状态)之前引入了一个source_delete_only状态。它也保证了老表在下线过程中不会产生Orphan Data Anomaly问题。


快速掌握 PolarDB-X 拆分规则变更能力!


如何控制整体流程并保证稳定性


PolarDB-X以DDL的形式为用户提供拆分规则变更(也就是Repartition)的能力,DDL也需要保障ACID特性,但数据重新分布可能需要花费较长时间,系统因断电等原因而故障在所难免。


DDL引擎


所以PolarDB-X也实现了一套稳定的DDL执行框架,它会将DDL分成很多个步骤,每个步骤都是幂等的。这样就可以保证DDL任务可以随时中断,然后恢复继续运行或者回滚。通过DDL将所有的步骤串联,也将所有的人工操作排除在外,开发人员再也不需要做Repartition的方案设计,再也不需要半夜三更手动执行数据库的高危操作了。


分布式MDL死锁检测


MySQL在5.7版本引入Online DDL能力后,使得DDL能够更好地与读写事务并行,相比于之前的版本有了很大的改善。Online DDL的基本原理是只在关键的时刻获取MDL锁,而不是在整个DDL阶段都持有MDL锁。PolarDB-X在执行Repartition时也会分多个阶段获取多次MDL锁,从而允许事务达到更高的并发度。但MDL锁有个危险的特性是:它是一个公平锁,并且可能造成MDL死锁。


多次获取MDL锁提高了性能的同时,实际上增加了MDL死锁产生的概率,而MDL死锁一旦发生,会导致后续所有的读写事务都被阻塞,MySQL的MDL默认超时时间1年,危害远大于普通的数据死锁。因此PolarDB-X也提供了一个分布式MDL死锁检测模块,用于在关键时刻解除分布式MDL死锁。



总结


灵活的拆分规则变更能力是分布式数据库的重要特性。PolarDB-X中有3种表类型:单表、广播表、分库分表。使用拆分规则变更能力,用户可以将数据表进行任意的表类型转换,从而更好地适应业务发展。PolarDB-X在提供拆分规则变更能力的同时,保证了数据的强一致、高可用、对业务透明、并且使用起来非常方便。本文简单阐述了PolarDB-X实现拆分规则变更过程中使用到的各项技术点,如读者可见,我们之所以将拆分规则变更能力集成到数据库内核中,是因为很多数据一致性问题非此不可解决。这也是分布式数据库区别于分布式数据库中间件的重要特性之一。


拆分规则变更能力只是PolarDB-X诸多特性中的一个。想要了解更多内容,欢迎关注公众号内的其他文章。



参考文献


  1. Asymmetric-Partition Replication for Highly Scalable Distributed Transaction Processing in Practice
  2. Online, Asynchronous Schema Change in F1
  3. What’s Really New with NewSQL
  4. https://dev.mysql.com/doc/refman/5.6/en/innodb-online-ddl.html
  5. https://dev.mysql.com/doc/refman/5.6/en/metadata-locking.html
  6. https://zhuanlan.zhihu.com/p/289870241
  7. https://zhuanlan.zhihu.com/p/329978215
  8. https://zhuanlan.zhihu.com/p/341685541
  9. https://zhuanlan.zhihu.com/p/346026906



【相关阅读】

子查询漫谈

探索 | PolarDB-X:实现高效灵活的分区管理

分布式数据库如何实现 Join?

PolarDB-X 让“Online DDL”更Online

上一篇:爬虫与Python:(四)爬虫进阶二之数据存储(数据库存储)——8.PostgreSQL存储


下一篇:postgresql之分区表