作者:姑胥
背景
PolarDB-X作为一款云原生分布式关系型数据库[6],支持通过拆分规则将一张逻辑表的数据分布在多个数据节点中。同时,也支持变更拆分规则,将数据进行重新分布[9]。数据重新分布(Repartition)的能力是分布式数据库的核心能力之一。当业务规模扩大时,它可以将数据分布到更多的节点,以达到水平扩展(ScaleOut)的目的。当业务规则剧烈变化时,它也可以将数据以新的拆分规则分布,从而提高查询的性能,更好地适应新的业务规则。下图展示如何将一张单表,通过一条简单的DDL语句online地变更为一个分库分表。
数据如何重新分布?
其实早在分布式数据库中间件大行其道的年代,开发人员就尝试过各种方式来解决数据Repartition的问题。但是无一例外,这些方案都非常复杂和危险,开发人员通常只能在半夜流量低峰期做Repartition,前前后后需要准备好详细的设计、论证、回滚方案。并且基于分布式的特点,在新老数据切换的那一段时间,几乎总是得在数据一致性和可用性之间做一个痛苦的选择。所以很多方案会有一个停写阶段或数据比对阶段,或两者皆有。
PolarDB-X充分论证了分布式数据库Repartition的各项技术细节后,能够做到Repartition过程中数据强一致、高可用、对业务透明、一条简单DDL语句搞定。
数据Repartition的关键问题
本文讨论的主要是表级别的数据Repartition。比如:将一张拆分表的数据按照新的规则重新分布到不同的数据节点。但是重新分布的过程需要时间,这段时间内又会有新的增量数据进入系统。当所有的数据都重新分布完成后,要将新老数据进行切换,最后停止双写并删掉所有的老数据。仔细研究上述过程的细节后,可以将其概括为这3个关键子问题,后续我们将按照这3个子问题评估各个实现方案:
- 如何进行存量、增量的数据同步?
- 比如:应用层双写、数据同步中间件、数据库触发器等。
- 比如:应用层双写、数据同步中间件、数据库触发器等。
- 如何进行流量切换?
- 主要考虑读写流量在新老数据中的迁移。
- 主要考虑读写流量在新老数据中的迁移。
- 如何控制Repartition的整体流程?
- 主要考虑如何组合各个步骤,比如什么时候开启/停止增量数据同步,什么时候进行流量切换。
- 如果故障了怎么恢复或回滚等。
- 主要考虑如何组合各个步骤,比如什么时候开启/停止增量数据同步,什么时候进行流量切换。
传统的Repartition方案
一个传统的Repartition流程大概是这样的:
- 应用集群初始状态的读写流量都在老的数据表上。
- 增加新表并开启双写。双写方式可能是应用层双写,或基于binlog数据同步,或数据库触发器实现双写等。但这个步骤,如果没有事务保证的话就很容易造成数据不一致。
- 启动存量数据同步。
- 可能会启动一个数据比对服务,如果有数据不一致的话,也需要人工介入处理。
- 当存量数据同步完成,增量数据追平后,将读流量切到新表,保持双写一段时间。
- 撤走老表的写流量,完成Repartition
但是如果我们仔细推敲一下,发现很多步骤根本就没办法保障数据一致性:
- 上述第2步,开启双写时,分布式系统无法保证所有节点都一起开启双写。所以一定有一段时间只有部分节点开启了双写。那就会出现Orphan Data Anomaly的问题(后文将说明)[2][8],造成新老表的数据不一致。传统的Repartition方案要解决这个问题的话,只能停写。
- 同理,上述第6步撤走老表的写流量时,也会出现数据不一致问题。
- 上述第2步,增量数据的双写一致性难以保障。
除了数据一致性问题外,依然存在很多繁琐但依然十分重要的问题:
- 整个流程基本由人工串联,多个环节需要人工介入。所以前期需要学习和理解成本,过程中也需要参与成本。
- 传统方案如果要避免数据不一致问题,在关键节点需要停写,这会对业务造成干扰。
- 整个流程十分脆弱地粘合在了一起,容错性非常差。
PolarDB-X的Repartition方案
PolarDB-X 是一款存储与计算分离的分布式数据库,所以在架构中会分为计算节点(CN)和存储节点(DN),以及一个元数据库(GMS)。CN负责SQL的parse、optimize、execute;DN负责数据存储;GMS负责存储元数据。为了性能考虑,每个CN节点都会缓存一份元数据。
如何进行存量、增量数据同步
增量数据双写
在分布式的增量数据双写场景中,双写的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节点开启了增量双写,而另一些没有。于是会出现这样的情况:
- 计算节点CN0已开启双写,并在老表和新表分别写入了3条数据,如下图
- 计算节点CN1未开启双写,执行了delete语句,删除了id为3的一条数据,但是未删除新表的数据
- 新表和老表出现了数据不一致
这一问题在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以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诸多特性中的一个。想要了解更多内容,欢迎关注公众号内的其他文章。
参考文献
- Asymmetric-Partition Replication for Highly Scalable Distributed Transaction Processing in Practice
- Online, Asynchronous Schema Change in F1
- What’s Really New with NewSQL
-
https://dev.mysql.com/doc/refman/5.6/en/innodb-online-ddl.html
-
https://dev.mysql.com/doc/refman/5.6/en/metadata-locking.html
-
https://zhuanlan.zhihu.com/p/289870241
-
https://zhuanlan.zhihu.com/p/329978215
-
https://zhuanlan.zhihu.com/p/341685541
- https://zhuanlan.zhihu.com/p/346026906
【相关阅读】