TDSQL水平扩容背后的设计原理

设计原理:分区键选择如何兼顾兼容性与性能
TDSQL水平扩容背后的设计原理
文章图片

首先我们刚才提到,水平扩容第一个问题是数据如何进行拆分。因为数据拆分是第一步,这个会影响到后续整个使用过程。对TDSQL来说,数据拆分的逻辑放到一个创建表的语法里面。需要业务去指定 shardkey“等于某个字段”——业务在设计表结构时需要选择一个字段作为分区键,这样的话TDSQL会根据这个分区键做数据的拆分,而访问的话会根据分区键做数据的聚合。我们是希望业务在设计表结构的时候能够参与进来,指定一个字段作为shardkey。这样一来,兼容性与性能都能做到很好的平衡。
其实我们也可以做到用户创建表的时候不指定shardkey,由我们底层这边随机选择一个键做数据的拆分,但这个会影响后续的使用效率,比如不能特别好地发挥分布式数据库的使用性能。我们认为,业务层如果在设计表结构时能有少量参与的话,可以带来非常大的性能优势,让兼容性和性能得到平衡。除此之外,如果由业务来选择shardkey——分区键,在业务设计表结构的时候,我们可以看到多个表,可以选择相关的那一列作为shardkey,这样可以保证数据拆分时,相关的数据是放在同一个节点上的,这样可以避免很多分布式情况下的跨节点的数据交互。
我们在创建表的时候,分区表是我们最常用的,它把数据拆分到各个节点上。此外,其实我们提供了另外两种——总共会提供三种类型的表,背后的主要思考是为了性能,就是说通过将global表这类数据是全量在各个节点上的表——一开始大家会看到,数据全量在各个节点上,就相当于是没有分布式的特性,没有水平拆分的特性,但其实这种表,我们一般会用在数据量比较小、改动比较少的一些配置表中,通过数据的冗余来保证后续访问,特别是在操作的时候能够尽量避免跨节点的数据交互。其他方面,shardkey来说,我们会根据user做一个Hash,这个好处是我们的数据会比较均衡地分布在各个节点上,来保证数据不会有热点。
设计原理:扩容中的高可用和高可靠性
TDSQL水平扩容背后的设计原理
文章图片

刚才也提到,因为整个扩容过程的流程会比较复杂,那么整个扩容过程能否保证高可用或者高可靠性,以及对业务的感知是怎么样的,TDSQL是怎么做的呢?
数据同步
第一步是数据同步阶段。假设我们现在有两个Set,然后我们发现其中一个SET现在磁盘容量已经比较危险了,比如可能达到80%以上了,这个时候要对它进行扩容,我们首先会新建一个实例,通过拷贝镜像,新建实例,新建同步关系。建立同步的过程对业务无感知,而这个过程是实时的同步。
数据校验
第二阶段,则是持续地追平数据,同时持续地进行数据校验。这个过程可能会持续一段时间,对于两个同步之间的延时差无限接近时——比如我们定一个5秒的阈值,当我们发现已经追到5秒之内时,这个时候我们会进入第三个阶段——路由更新阶段。
路由更新
路由更新阶段当中,首先我们会冻结写请求,这个时候如果业务有写过来的话,我们会拒掉,让业务过两秒钟再重试,这个时候对业务其实是有秒级的影响。但是这个时间会非常短,冻结写请求之后,第三个实例同步的时候很快就会发现数据全部追上来,并且校验也没问题,这个时候我们会修改路由,同时进行相关原子操作,在底层做到存储层分区屏蔽,这样就能保证SQL接入层在假如路由来不及更新的时数据也不会写错。因为底层做了改变,分区已经屏蔽了。这样就可以保证数据的一致性。路由一旦更新好以后,第三个SET就可以接收用户的请求,这个时候大家可以发现,第一个SET和第三个SET因为建立了同步,所以它们两个是拥有全量数据的。
删除冗余数据
最后一步则需要把这些冗余数据删掉。删冗余数据用的是延迟删除,保证删除过程中可以慢慢删,也不会造成比较大的IO波动,影响现网的业务。整个删除过程中,我们做了分区屏蔽,同时会在SQL引擎层会做SQL的改写,来保证当我们在底层虽然有冗余数据,但用户来查的时候即使是一个全扫描,我们也能保证不会多一些数据。
可以看到整个扩容流程,数据同步,还有校验和删除冗余这几个阶段,时间耗费相对来说会比较长,因为要建同步的话,如果数据量比较大,整个拷贝镜像或者是追binlog这段时间相对比较长。但是这几个阶段对业务其实没有任何影响,业务根本就没感知到现在新加了一个同步关系。那么假如在建立同步关系时发现有问题,或者新建备机时出问题了,也完全可以再换一个备机,或者是经过重试,这个对业务没有影响。路由更新阶段,理论上对业务写请求难以避免会造成秒级的影响,但我们会将这个影响时间窗口期控制在非常短,因为本身冻结写请求是需要保证同步已经在5秒之内这样一个比较小的阈值,同步到这个阶段以后,我们才能发起路由更新操作。同时,我们对存储层做了分区屏蔽来保证多个模块之间,如果有更新不同时也不会有数据错乱的问题。这是一个我们如何保证扩容中的高可用跟高可靠性的,整个扩容对业务影响非常小的原理过程。
设计原理:分布式事务
TDSQL水平扩容背后的设计原理
文章图片

刚才讲的是扩容阶段大概的流程,以及TDSQL是如何解决问题的。接下来我们再看扩容完成以后,如何解决刚才说的水平扩容以后带来的问题。首先是分布式事务。
原子性、去中心化、性能线性增长
扩容以后,数据是跨节点了,系统本来只有一个节点,现在跨节点的话,如何保证数据的原子性,这个我们基于两阶段提交,然后实现了分布式事务。整个处理逻辑对业务来说是完全屏蔽了背后的复杂性,对业务来说使用分布式数据库就跟使用单机MySQL一样。如果业务这条SQL只访问一个节点,那用普通的事务就可以;如果发现用户的一条SQL或者一个事务操作了多个节点,我们会用两阶段提交。到最后会通过记日志来保证整个分布式事务的原子性。同时我们对整个分布式事务在实现过程中做到完全去中心化,可以通过多个SQL来做TM,性能也可实现线性增长。除此之外,我们也做了大量的各种各样的异常验证机制,有非常健壮的异常处理和全局的试错机制,并且我们也通过了TPCC的标准验证。
设计原理:如何实现扩容中性能线性增长
对于水平扩容来说,数据拆分到多个节点后主要带来两个问题:一个是刚才说事务原子性的问题,这个通过分布式事务来解决;还有一个就是性能。
垂直扩容中一般是通过更换更好的CPU或者类似的方法,来实现性能线性增加。水平扩容而言,因为数据拆分到多个节点上去,如何才能很好地利用起拆分下去的各个节点,进行并行计算,,真正把水平分布式数据库的优势发挥出来,需要大量的操作、大量的优化措施。TDSQL做了这样一些优化措施。
TDSQL水平扩容背后的设计原理
文章图片

一是相关数据存在同一个节点上。建表结构的时候,我们希望业务能参与进来一部分,在设计表结构的时候指定相关的一些键作为shardkey,这样我们就能保证后端的相关数据是在一个节点上的。如果对这些数据进行联合查询就不需要跨节点。
同样,我们通过并行计算、流式聚合来实现性能提升——我们把SQL拆分分发到各个后台的节点,然后通过每个节点并行计算,计算好以后再通过SQL引擎来做二次聚合,然后返回给用户。而为了减少从后端把数据拉到SQL,减少数据的一个拉取的话,我们会做一些下推的查询——把更多的条件下推到DB上。此外我们也做了数据冗余,通过数据冗余保证尽量减少跨节点的数据交互。
我们简单看一个聚合——TDSQL是如何做到水平扩容以后,对业务基本无感知,使用方式跟使用单机MySQL一样的。对业务来说,假设有7条数据,业务不用管这个表具体数据是存在一个节点还是多个节点,只需要插7条数据。系统会根据传过来的SQL进行语法解析,并自动把这条数据进行改写。7条数据,系统会根据分区键计算,发现这4个要发到第一个节点,另外3个发到第二个节点,然后进行改写,改写好之后插入这些数据。对用户来说,就是执行了这么一条,但是跨节点了,我们这边会用到两阶段提交,从而变成多条SQL,进而保证一旦有问题两边会同时回滚。

![file](/img/bVcUL7R)

数据插录完以后,用户如果要做一些查询——事实上用户不知道数据是拆分的,对他来说就是一个完整的表,他用类似聚合函数等进行查询。同样,这条SQL也会进行改写,系统会把这条SQL发到两个节点上,同时加一些平均函数,进行相应的转换。到了各个节点,系统会先做数据聚合,到这边再一次做聚合。增加这个步骤的好处是,这边过来的话,我们可以通过做一个聚合,相当于在这里不需要缓存太多的数据,并且做到一个流式计算,避免出现一次性消耗太多内存的情况。
【TDSQL水平扩容背后的设计原理】对于比较复杂的一些SQL,比如多表或者是更多的子查询,大家有兴趣的话可以关注我们后面的分享——SQL引擎架构和引擎查询实战。

    推荐阅读