一力搜索
作者一力搜索2021-06-10 16:19
数据库架构师, 股份制银行

中间件式分布式数据库发展趋势

字数 8961阅读 2234评论 1赞 6

通过本文,你将领略到goldenDB和TDSQL的关键技术设计,并能明白其中设计优劣权衡。本文还以个人观点看看未来数据库中间件的发展。因涉及机密,已对一些关键数据详细指标进行抽象化描述。

一. 分布式数据库关键技术剖析

1.1 对 mysql 的半同步异步化改造 ( 较 mysql 5.6)

异步化改造之后的半同步,TDSQL跨DC官方测试数据TPS由半同步的2200提升到了9500。

我们goldenDB的实现类似,当然,我们还实现了一个高低水位的判断,并不会盲目的像mysql那样退化为异步(只有一个主也能提供服务)。

明显的,Mysql官方也意识到了这个问题。在5.7版本中也做了类似的优化。

dump thread过程分析:

mysql5.6 和之前版本:

  1. master dump thread 发送binlog events 给 slave 的IO thread,等待 slave 的ack反馈

  2. slave 接受binlog events 写入relay log ,返回 ack 消息给master dump thread

  3. master dump thread 收到ack消息,给session返回commit ok,然后继续发送写一个事务的binlog。

mysql5.7新增dump ack线程:

  1. master dump thread 发送binlog events 给 slave 的IO thread,开启ack线程等待 slave 的ack反馈,dump 线程继续向slaveIO thread发送下一个事务的binlog(和我们和TDSQL所做的异步化改造一样)。

  2. slave 接受binlog events 写入relay log ,返回 ack 消息给master ack线程,然后给session返回commit ok。

(5.6到5.7 after_commit 到after_sync避免异常情况下可能数据不一致的问题不在这里展开了。 当半同步复制发生超时时(由rpl_semi_sync_master_timeout参数控制,单位是毫秒,默认为10000,即10s),会暂时关闭半同步复制,转而使用异步复制。当master dump线程发送完一个事务的所有事件之后,如果在rpl_semi_sync_master_timeout内,收到了从库的响应,则主从又重新恢复为半同步复制。)

MySQL 5.7极大的提升了半同步复制的性能。

5.6版本的半同步复制,dump thread 承担了两份不同且又十分频繁的任务:传送binlog 给slave ,还需要等待slave反馈信息,而且这两个任务是串行的,dump thread 必须等待 slave 返回之后才会传送下一个 events 事务。dump thread 已然成为整个半同步提高性能的瓶颈。在高并发业务场景下,这样的机制会影响数据库整体的TPS 。

5.7版本的半同步复制中,独立出一个 ack collector thread ,专门用于接收slave 的反馈信息。这样master 上有两个线程独立工作,可以同时发送binlog 到slave ,和接收slave的反馈。

1.2 goldenDB分布式事务处理

聚合运算这些操作可以用多种办法把性能和内存开销做到极致,但是我觉得这不是重点,所以不在这里展开。

我们做过测试,SSD, 2U12C的硬件配置下,mysql在2000个select for update时,就已经举步维艰了(我们还需要测试排查看是不是select for update只命中一行数据却锁多行的问题造成)。 我们update直接拆分的形式,起码对现在的mysql5.7而言,对并发能力是个考验。

全局死锁处理:

如:A,B 互相转账

因为update会拆分出来select for update,所以GT1等009用户释放锁,而GT2又在等002释放锁。

我们并没有全局死锁检测机制,需要应用配置超时时间或断开后回滚。当然proxy故障这种情况下,其他proxy会通过活跃GTID接替回滚操作。

1.3 TDSQL分布式事务

1.3.1 简易逻辑架构

TProxy:识别DDL操作,以任务形式保存进ZK; 识别DML操作,对sql进行解析分发到对于DB, 收集各DB节点应答,组合处理后返回应用;watch zk,拉取路由

Agent:监控DB实例状态并上报zk.;监控表状态上报ZK;从ZK拉去所要执行的任务执行;参与主备切换。

Scheduler:调度决策集群;从zk拉取DDL任务,在DB上执行; 扩容控制;控制DB主备切换;

Zk: 元数据管理集群

GTM:在二阶段提交实现上,在begin的时候从GTM获取全局递增的事务ID,然后在参与事务的各个子节点通过这个事务ID开启事务,进行各种DML操作,提交的时候先对各个子节点执行prepare。当prepare成功之后,再更新全局事务ID的事务状态,同时获取到一个新的事务ID作为提交的事务ID对各个子事务进行异步并行化的提交,提供更好的事务操作性能。

当前GTM以一主两从的方式运行,主从节点底层通过 raft 协议进行数据的同步以及主从切换,内部交互以及对外通讯均基于 grpc 协议。当前TDSQL的GTM组件性能完全可以满足金融业务需求:

GTID格式为:‘网关id’-‘网关随机值’-‘序列号’-‘时间戳’-‘分区号’,例如 c46535fe-b6-dd-595db6b8-25

GtidLog: 为mysql中的一张表,,用于记录全局事务信息,一个全局事务只会存储在一个分片上。

SELECT gtid_state(“gtid”),可以获取“gtid”的状态,可能的结果有:

a)“COMMIT”,标识该事务已经或者最终会被提交

b)“ABORT”,标识该事务最终会被回滚

c) 空,由于事务的状态会在一个小时之后清楚,因此有以下两种可能:

1) 一个小时之后查询,标识事务状态已经清除

2) 一个小时以内查询,标识事务最终会被回滚

1.3.2 分布式事务流程

(1)、网关在执行一个事务的insert/update/delete语句时,会记录这个语句修改了哪个SET;

(2)、SET时会发送一个XA START在这个SET上面启动事务分支;(注:XA事务开始时,并不确认事务将以哪种提交方式执行,因此总是以xa start来开启一个事务);

(3)、检测是否影响SET个数≤1,若是,则直接做一阶段提交(xa commit one phase)。

(4)、影响SET个数≥2,则改为做两阶段提交:

1) 网关首先发送xa prepare‘gtid’ 给参与的SET(大于等于2个SET);

2) SET接受到xa prepare应答ok(表示成功确认);

3) 收到成功确认后,写入XA对应的commit log,再发送xa commit‘gtid’参与SET;

4) 如果有SET返回了错误,或者写入commit log失败,那么网关发送 xa rollback‘gtid’给相关SET,这样这个全局事务就实现了回滚。

腾讯云DCDB的commit log是在SET中存储,这个步骤是批量完成的——网关后台线程会汇集正在提交的分布式事务然后在独立的连接和事务中完成对每个SET的写入,并且每个事务的commit log只写入一个SET中,因而这个开销并没有显著增加事务的提交耗时或者降低TPS。而且,依赖腾讯云DCDB已有的强同步和容灾特性,只要XA成功写入了commit log,就意味着数据已经写入从机。

虽然绝大多数的XA事务可以正常执行。但极少数的异常情况还是会影响整个集群稳定性,因此,腾讯云设计了agent(监控模块),在故障后继续协助完成本地MySQL上面prepared事务的提交,即agent会解析commit log,并根据异常处理本地仍然处于prepared的事务数据;如果commit log上面没有事务的提交决定的话,agent也会回滚超时未被提交的prepared本地事务。

TDSQL XA的全局事务可以达到read committed, repeatable read, 或者serializable隔离级别。总的来说,全局事务的本地事务分支的依赖关系也是TDSQL的全局事务的依赖关系。 即对TDSQL XA来说,其本地mysql事务在read-committed/repeatable-read隔离级别下运行时,TDSQL XA的全局事务也是在read-committed/repeatable-read隔离级别下运行。TDSQL可以做到全局事务的可串行化,但代价同样是巨大的。

那么TDSQL是如何做到全局事务的可串行化的?

1). 独立的后端连接

对于连接到网关的每个客户端连接,网关会向这个连接当中的语句访问的每一个后端DB发起一个独立的连接。并且每一个变量设置会传播到后端的所有连接中。比如,如果你在客户端设置了set tx_isolation=”serializable”; 那么这个设置会被网关设置到你的客户端连接对应的每一个网关与后端DB的连接当中。

2). 事务串行执行

假设有并发执行的全局事务GT1和GT2,它们的隔离级别都被设置为serializable,根据#1,网关与后端的连接上面隔离级别也都设置为serializable了。GT1和GT2在set1上面更新同一行,并且在set2上各自插入不同的插入一行,事务分支: GT1 {T11, T12},GT2{T21, T22}。

由于T11与T21并发更新同一行,如果T11先更新,那么T11会拿到pk=1的那行(标注为R1)的事务锁直到GT1结束提交才释放,然后T21才能拿到R1的行事务锁开始执行,然后T22执行。所以执行顺序就是 GT1->GT2(T11->T12->T21->T22);类似地,假如是T21先拿到了R1的行锁,那么执行顺序就将是 GT2->GT1(T21->T22->T11->T12)。也就是说,运行在每个MySQL实例上的事务锁调度机制可以确保全局事务的串行执行。

我们可以得出这个推论:全局事务的本地事务分支的依赖关系也是全局事务的依赖关系。这里的依赖关系就是事务锁的等待关系。按照上例来说,T11先拿到R1的事务锁,那么T21就依赖于T11,于是也能够导致GT2依赖于GT1。也就是说这点是成立的:

T11-> T21 ==> GT1->GT2 (1)

根据数据库事务处理的基本理论,如果某个并发事务调度机制可以让具有依赖关系的事务构成一个有向无环图(DAG),那么这个调度就是可串行化的调度。由于每个后端DB都在使用serializable隔离级别,所以每个后端DB上面并发执行的事务分支构成的依赖关系图一定是DAG。使用上面的推论(1),每个后端DB上面并发执行的事务分支的依赖关系图通过图的合并操作就自然形成了TDSQL XA所处理的并发执行的全局事务的依赖关系图GTG;如果这个图GTG是一个有向无环图,那么这些全局事务一定是在可串行化隔离级别下运行的;如果GTG有环,那么在serializable隔离级别下一定则会发生死锁,并且很可能是全局死锁(比如A,B用户(分在不同库)的同时相互转账),那么innodb死锁处理机制和TDSQL XA的全局死锁处理机制就需要解除这些死锁。

如果一个事务在read-committed隔离级别下运行,它的读锁在当前select语句结束后就释放;在repeatable-read隔离级别下,其读锁在事务结束时刻才释放。根据上面的推论(1),可以轻易得出:对TDSQL XA来说,其本地mysql事务在read-committed/repeatable-read隔离级别下运行时,TDSQL XA的全局事务也是在read-committed/repeatable-read隔离级别下运行。

1.3.3 假想基于MVCC的全局可串行化机制

使用serializable隔离级别时innodb就不再使用MVCC做查询了,而是基于锁,即使你不在select语句中加上for updates/lock in share mode。如果要基于MVCC实现TDSQL XA 的可串行化隔离级别是有巨大代价的,这个代价主要是集群的性能损失以及可靠性损失。因此TDSQL并没有这样做。如果一定要做,似乎道路也不多。

为了实现可串行化隔离级别,我们就需要一个全局事务id分发机制或者队列产生全局顺序。比如PGXL的GTM就是全局事务ID生成器,而这个GTM实例就是一个单点故障源。即使再为它实现容灾,它也仍然是一个性能和可扩展性瓶颈。

同时,由于mysql innodb使用MVCC做select(除了serializable和for update/lock in share mode子句),还需要将这个全局事务id给予innodb做事务id,同时,还需要TDSQL XA集群的多个set的innodb 共享各自的本地事务状态给所有其他innodb(这也是PGXL 所做的),任何一个innodb的本地事务的启动,prepare,commit,abort都需要通知给所有其他innodb实例。只有这样做,集群中的每个innodb实例才能够建立全局完全有一致的、当前集群中正在处理的所有事务的状态,以便做多版本并发控制。这本身都会造成极大的性能开销,并且导致set之间的严重依赖,降低系统可靠性。这些都是TDSQL在极力避免的。

1.3.4 select 语句的一致性

在使用分布式事务的情况下,使用MVCC是有问题的。

这是因为innodb 的MVCC只针对同一个mysqld进程内的事务有效,innodb并不能知道一个本地事务分支所属的全局事务在其他innodb实例当中的事务分支的状态(active, committed, prepared, etc),因此有可能查询到一个未完全提交的全局事务的改动---只有本地事务分支完成了提交,其他mysql实例上面还没有完成提交。归根到底的原因是无法得到全局一致性快照,但是如上节所述,全局一致性快照的维护代价极其昂贵,并不适合OLTP系统。

假如dId=001有1000元,dId=002的有500元,则GT2查询出来的结果为:dId=001的为900元,dId=002的有500元。 也就是说GT2的select语句只是读取到了GT1的一部分更新,但是分片2的更新因为GT1尚未提交而没有读取到。

本例中如果GT1的select语句本来也只会访问到set1上面的数据,那么尽管GT2.T22并未完成提交,那么对于TDSQL XA来说也不算是一致性问题,这是因为在TDSQL XA中,只要commit log中记录好了要提交的事务就一定会完成提交,也就是说GT1读取到的是完整的稳定的可靠的结果.这是没有问题的。

在使用MVCC做select查询的情况下,做两阶段提交的全局事务只能做到最终一致性,MVCC有可能读取到没有完全提交(i.e.在所有参与的set上都完成提交)的GT的部分改动。一般情况下,这个不一致的时间窗口很小;需要agent提交时会时间窗口会比较长。对一致性要求高的话,就是用serializable隔离级别。

TDSql认为,XA事务做select就不能使用MVCC,要读取全局一致性数据,就需要在网关与后端的连接当中设置隔离级别是serializable,这样所有的select自动都是加共享锁的。或者客户端对每个select语句都显示使用加锁子句: select ... lock in share mode/for update也可以。

可选隔离级别的配置,其实在中信卡中心的现有设计中就体现得淋漓尽致,只不过我们通过业务梳理和DBProxy隔离的方式来做到的性能大幅提升(如授权业务的全体无分布式性)。

可以这么理解,默认情况下goldenDB的查询就是TDSQL中隔离级别最高的查询,不会查询出全局的中间状态(比如A给B转账,A所在库commit了,B所在库尚未commit),当然我们也可以通过SQL语句加上ur或者dbproxy配置UR模式来做到不校验活跃GTID,这也是类似TDSQL默认的做法。

goldenDB没有对select进行select for update这样的拆分,通过判断GTID是否在活跃列表中和没有比最大非活跃GTID大这种乐观的思想来做的全局一致性查询,在最严格隔离级别下的查询中,设计和性能上还是要比TDSQL的思想要优的。 TDSQL中因为允许查询出全局中间状态,所以分布式的update是不能允许做反向SQL的回滚的,它务必使其他分片也要commit成功。

1.4 容灾切换

原则:

  1. 不可多主

  2. 数据一致

  3. 快速提供写能力

  1. 主DB降级为备机(kill掉当前所有session,设置只读, 如因为当机原因没有收到下次重新启动也会执行这个流程),同时推送proxy暂时没有主节点的路由。

  2. 参与选举的备机回放完relay log后,上报最新的binlog中gtid值(mysql的,非全局GTID)。

  3. CM收到各备机binlog中最大GTID值后,选择出GTID最大的节点(可能同时有多个,依据配置规则选举出来,如:本地机房备节点优先升主)为主机;

  4. 重建主备关系,并保存进元数据库中。

5. 新路由推送给所有proxy(PM从MDS库中读取)。

这一块goldenDB和TDSQL没有本质上的区别,只是采用的元数据库载体不一样而已。

1.5 TDSQL有意思的一些改进或注意点

1. 用户可以设置select_rows_limit/delete_rows_limit/update_rows_limit 3个动态的全局变量,来强制添加用户必须显示指定limit子句来做规模限制,例如设置比如 set global delete_rows_limit=10000; 这样的话,delete语句就不可以删除超过10000行代码,否则就失败了。当然,开启变量后,用户也可以显示指定NO_LIMIT关键字,来避免这个限制。

2. 为了防止计划外的超大事务导致锁表;或binlog量过大会导致强同步等待超时;或某些异常操作,比如用户误操作无条件删表等;我们通过参数max_binlog_write_threshold限制一个事务中写入的binlog总量,当达到配置的最大值时候,当前DML语句返回错误,并在数据库中回滚该事务。检查在DML执行时即做,而不是在事务提交时刻,这样可以有效提高效率。

  1. 字段增加之类的在线DDL通过pt在线工具实现。

  2. Swap如果要使用(建议关掉),也要在SSD上。

  3. Truncate或者drop实现改成rename,再定时慢慢删除。

二. 中信卡中心架构改造

经过上面的详细描述,相信大家对以中间件开展的分布式数据库有了一定的了解,那么针对信用卡业务,我们可以做哪些的改造,使得分布式数据库更稳定,性能更高?

2.1 中间件DBProxy分组带来业务的隔离。

排除不同项目组之间影响,降低包括proxy自身bug在内(如调度派发机制的缺陷)的风险。 例如,数据或误操作一个大查询也不会影响到授权。这种隔离也有助于问题的归类和定位。当然为了降低DBProxy单点故障影响,我们机器部署2-4个DBProxy。

2.2 全局隔离级别多样化

保障业务准确性的同时,大幅提高吞吐能力。通过业务梳理做到绝大部分业务不需要分布式查询,不需要分布式更新(并且不会有其他业务会去分布式更新影响)配置为UR,SW模式(无活跃GTID比对,update语句无select for update的拆分),时延提升40%(50ms -30ms)。

2.3 所有业务交易去管理节点(OMM,CM,PM, MDS)化(DDL之类除外)化(管理节点故障业务不受影响)。

授权业务和账户联机业务,数据服务联机查询去GTM化(DBProxy可配),减少交互流程,同时极大降低GTM单点风险,在前面的优化基础上,TPS再次直接提升了超过20%(16000TPS-20000TPS)。

2.4 DB磁盘隔离部署,日志单独部署,分流部分压力到备库,减轻主库压力,避免因压力导致主库故障。

而relay log, bin log的频繁写删会影响SSD磁盘寿命,因此单独隔离。采用4磁盘(MASTER RAID10) + 4磁盘(SLAVE RAID10) + 3磁盘(relay log +binlog RAID5)部署方式。 数据批量抽数(全表扫描)直接抽取备库。

2.5 后面我们还将要推动实现DBPROXY/用户级的读写分离。

这是联调使用中的心得,后面发现TDSQL中也支持这样的功能。 用户级的读写分离是支撑数据服务联机查询大幅切过来(数据服务联机查询对毫秒级别的延迟完全可以接受)而不影响在线交易的不二选择。

通过改造,授权业务TPS便可轻松突破20000(当然,同城时延的引入会带来一定下降,但proxy我们可以做到真正无状态扩展).

改造后业务和中间件逻辑关系如下:

当然,我们还有不少关键的架构需要调整,

如proxy的消息调度派发模型(会引起各种阻塞);

如Truncate table ;

如业务网络平面和管理平面的隔离;

如分布式调度处理消息的优先级划分(会导致雪崩效应);

如所有分片超长事务(锁或异常)的监控......

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

6

添加新评论1 条评论

匿名用户
2021-06-11 13:53
作者本人在银行有这样的技术,说实话很难的
Ctrl+Enter 发表

分布式关系型数据库选型优先顺序调查

发表您的选型观点,参与即得50金币。