GoldenDB
作者GoldenDB·2020-03-15 17:36
产品经理·中兴通讯

陆天炜: GoldenDB 事务一致性处理机制优化历程

字数 8947阅读 9399评论 1赞 1

前言:GoldenDB 是中兴通讯推出的一款自研的金融级交易型分布式数据。针对金融行业关注的数据库事务一致性问题,中兴通讯 GoldenDB 分布式数据库架构师陆天炜,在 DTCC2019 数据库大会上做了干货分享,重点介绍了 GoldenDB 的解决方案和对应的优化实践。


▲中兴通讯 GoldenDB 分布式数据库架构师 陆天炜

众所周知,中兴通讯的主航道产业是 5G 通信,但是各行各业都会使用数据库,中兴通讯提交给客户的通讯设备上面也都使用了大量的数据库,为了给客户提供更好的交付体验,大部分都是自研的产品。

从 2002 年开始,中兴通讯推出 EBASE 文件数据库,2007 年推出 EBASE-MEM 内存数据库,做到 2011 年的 DHSS 分布式数据库,开始有了一个雏形。2014 年的时候,中兴通讯开始研发了 GoldenDB 这个金融级的数据库,2015 年的时候已经有第一个商业版本 GoldenDB 1.0,并在中信银行的北京营业厅的冠字号业务上开始使用。到 2016 年的时候,已经在多个银行不同业务等级的生产系统上做了商用投产。2017 年,中兴通讯做了另外一件事情,和中信银行一起在总行的账务系统上做了一个核心下移的测试验证,并且验证通过了,性能超过 40000TPS。当时客户就决定要在 2019 年的时候,把核心业务的数据库,从基于小机的 DB2 上迁移到基于 X86 平台的 GoldenDB 上。

GoldenDB 分布式数据库总体架构是什么?

GoldenDB 分布式数据库总体架构分成四个重要部分:

第一部分,是计算节点集群,是多点读写模式,因为计算节点没有状态,所以可以做到横向扩容。

第二部分,是数据节点集群。在一套 GoldenDB 里面,可以有多个数据节点集群去承载多个不同的业务,每个数据节点集群是可以做到物理隔离的。每个集群内部根据对应的业务流量,还有存储压力,去做分片,做负载均衡。每个数据分片都是一主多备的结构,每个备机可以做容灾和读负载能力的扩展。

第三部分,全局事务管理节点,用来管理全局分布式事物的一个生命周期。它跟计算节点做交互,提供全局事务 ID 的分配、回收,以及全局活跃事物列表的查询功能。

第四部分,是管理节点,主要包含几个重要功能:第一,提供了一个 Web 控制台页面,在控制台上可以做自动的安装、更新,集群的组建,主备切换,备份恢复等各种与运维相关的操作;第二,管理节点里面还包含了元数据管理器,源数据管理器存了两部分信息,第一部分信息是 GoldenDB 拓扑的组网信息,包括各个集群的设备信息,主备信息、IP 信息等,第二部分就是业务数据库的源数据,包括 DDL、库表结构、分布状态、分布规则;第三,管理节点还提供了监控,可视化告警等与管理相关的功能。

这就是 GoldenDB 数据库的整体架构,对外连接业务,提供的是 MySQL 的标准协议。

GoldenDB 现在正在做的一个事情是,在 2019 年底的时候要把中信银行的 DB2 给替换掉。下面总结了 GoldenDB 数据库满足金融核心功能的一些关键需求。

第一,实时一致的分布式事务控制;在做分布式事务控制的时候,能够做到实时一致,并且事务处理不影响业务原来的业务逻辑,业务原来的代码迁到 GoldenDB 上的时候是不需要做逻辑变更。他们原来的代码是 RPG 的代码,直接通过工具改成 Java 代码,业务开发的工作量非常少。

第二,支持同城异地容灾和一致的备份恢复,符合异地监管的一致性。GoldenDB 契合金融的两个三中心架构,能够做到同城 RPO 为 0。无论是一致的备份恢复,还是异地接管的恢复,在做备份的时候各个节点单独备份,但恢复的时候能够能恢复到全局一致的状态。所以,异地做接管的时候,虽然会有数据丢失,但是接管后的数据也是一个一致的状态,更加方便业务进行数据回补。

第三,线性横向扩展和联机数据重分布,这包含两部分内容:一个是性能的横向扩展;一个是容量的横向扩展。关于性能的横向扩展,DBProxy 能做到百分之百的横向扩展,底层数据节点是瓶颈的时候,数据节点能做到 95% 的横向扩展。支持在线数据重分布,并且通过追增量和冻结日志的方式,能够把真正对联机交易的影响降低到秒级,一般默认的阈值配置都在 5 秒以下。

第四,丰富的监控体系和完善的运维能力。传统核心基于 DB2 或者 Oracle,只有一两台设备,运维人员和 DBA 人员只要监控一两台设备就行,换到这个 X86 的分布式架构下面,可能是十几台或者几十台,这对运维体系的易用性,对监控体系的完备性要求就更高。所以,中兴通讯的 GoldenDB 在 2018 年一整年都在做这么个事情,那就是丰富监控运维体系,跟很多第三方的平台和第三方工具做对接。

如何看待 GoldenDB 事务处理机制的各种问题?

1、事务理论的分布式延伸

关于 GoldenDB 的分布式事务处理机制,我们先看下事务处理在分布式架构有哪些延伸。我们理解的事务的一致性,其实是在事务内的原子性 ( 原子性 -A ) 和事物间的隔离性 ( 隔离性 -I ) ,以及故障时的持久性 ( 持久性 -D ) ,只有三者组合到一起才能保证数据的一致性 ( C ) 。

在分布式架构下,原子性的要求,是在多个数据分片上的多次操作,要么一起成功,要么一起失败;而在单机架构下,仅是一台机器上多条记录的多次操作;隔离性方面,要求多个计算节点上的不同连接,不会相互访问到在多个数据分片内未提交事务的数据;而单机数据库的隔离性,是指不同连接 ( 处理线程或进程 ) 不会相互访问到当前机器上未提交事务的数据;另外,从持久性角度看,分布式数据库的事务提交前必须将日志在分片主、 从节点都得到复制,主节点故障时,能在从节点上找回数据,继续完成事务;单机数据库的事务要求是,提交前必须先将日志落盘,机器宕机恢复后不丢失数据。

2、分布式事务的难点

那么,要实现分布式事务的实时一致性(保证 ACID ),难点在哪?其实包括两个部分:一是部分 DB 提交失败,如何保证全局事务的原子性 ( A ) ;另一部分是,并发访问时,每个事务都不知道其他事务的状态,如何保证事务之间的隔离性 ( I ) 。以转账交易为例 : 交易前 2 个账户资金余额各 100,事务 T1 从账户 1 转账 50 到账 户 2。需解决问题:在事务 T1 提交期间,由于 DB1 和 DB2 提交时间有空隙,若此时事务 T2 读取 2 个账户的余额,会发现余额之和是 50+100=150;存在事务 T1 对账户 1 上扣钱成功,给账户 2 加钱失败的情况。

3、如何保障原子性?

在业内,通常都用 2PC 协议保障原子性,在 MySQL 中就能看到很多场景,一个是 MySQL 里跨存储引擎的事务,二有存储引擎里 bin log 一致性的保障,都通过 2PC 实现。2PC 有自己的优势,参与者在投赞成票之前可以单方面取消事务,但是也有一些局限性。第一个问题,它本身是一个阻塞式的算法,进入 Prepare 以后一定要成功;第二个问题是,协同者的状态其实是一个单点,协调者挂了以后,参与者没有办法继续进行下去,参与者和协调者在某些状态会等待消息,需要启动定时监控;第三问题是,正常的提交流程,日志写入数量比较大,为 2N+3 ,消息数也多,为 4N ,其中 N 为参与节点的数量。

另外,还有很多通过增加消息中间件,或者使用 TCC 分布式事务框架,来实现事务原子性的方案。通过业务改造保障原子性,会有额外的问题:首先,是对业务的侵入性较大,所有业务都要增加反向操作逻辑 / 增加额外中间件;其次,处理过程中数据存在短暂不一致或导致更新延后,原子性不能 100% 保障;其三,无论 2PC、MQ 还是 TCC,都存在的一个问题,就是在 2 阶段提交 Commit 阶段,在 MQ 不断重试的阶段,在 TCC 的 Confirm 阶段如果出现了异常,没有办法解决,没有补救措施,必须人工干预。

至于,GoldenDB 事务原子性机制如何实现?总结起来就两句话:第一句是一阶段提交。GoldenDB 有一个全局事务,整体的提交是一阶段。把一个全局事务拆成由若干个子系统单独提交的一个个子事务,把这些子事务由计算节点交给对应 DB 去执行。执行之前,我们先在 GTM 那边把事务标记成活跃的状态;如果所有的 DB 都执行成功了,在 GTM 那边把事务标记为不活跃,全局事务就结束了;第二句是自动回滚补偿。遇到像转账失败的流程后,子事务在单机上会自动回滚,反馈给计算节点后,不是全部成功的,计算节点需要向数据节点下发回滚的消息。回滚补偿就是把下发回滚的操作做到数据库层面,业务层不需要做补偿交易。这种模式的优点,一是应用层无需增加额外补偿逻辑;二是,失败回滚是少数情况,整体性能高于两阶段提交,只需要一次交互就可以了,大大节省了单机资源。

4、已提交事务回滚实现和优化方式

在这一过程中,会涉及已提交事务回滚的实现和优化方式。整个过程要经历定位 - 遍历 - 生成 - 执行这样一个流程。通过全局事务 ID 所对应的 Binlog 提升回滚性能,数据行里会存有全局事务的 ID, 当你 Commit 的时候,全局事务 ID 会随着 Binlog 一起进入文件里。通过 Binlog 里面的 GTID 找到这个 GTID 所对应的所有 Binlog 的语句块,然后解析 Binlog,生成 SQL,立刻组成新的事务,重新执行一遍。对整个操作过程,GoldenDB 做了很多优化,包括表定义缓存、预分析并行查找、共享内存、key 值利用、SQL 格式,未来还要把正向 Binlog 直接生成反向 Binlog,不走 SQL 解析,直接走 MySQL 并行回放机制,直接应用到数据库。

5、事务隔离性的各种异常

我们如何处理事务隔离性的各种异常?首先,事务隔离性的各种问题都是并行调度处理不好导致,如果写写冲突,处理不当,会导致 ‘脏写’。比如: T1w、T2w、T1a,基于被回滚的数据做了 update,造成了丢失回滚。再比如,写读冲突处理不当,导致‘脏读’,如果第一读,读到了未提交的数据,那就是脏读,如果多次读并读了不同版本的数据,那是不可重复读,由于事务并发导致前后读出的多个数据间不满足原有约束,那是读偏序,由于事务并发导致满足条件的结果集,变多或者变少,那是幻读。此外,还有读写冲突,处理不当导致 ‘脏写’ ,比如:T1r、T2w、T1w,T2w 被覆盖,会导致丢失更新。最后是写偏序,这是一种违反语义的异常,在数据库层面看正常,但是达不到你要求的效果,就像黑白球,我有一黑一白两个球,事务一要把黑球改成白球,事务二要把白球改成黑球,当在快照级别隔离下,两个事务一起并发操作时,会导致最终事务一改了一个球,事务二改了一个球,一黑一白变成了一白一黑,这种情况是错误的,不满足于任意一种串行处理结果。以上这些异常都是并发调度没有处理好的结果。

在《A Critique of ANSI SQL Isolation Levels》论文中,总结了隔离级别及对应的现象。首先,R R 级别的隔离解决不了幻读;其次,SI 隔离解决不了写偏序。只有可串行化的隔离级别才能解决所有问题。有人说 MySQL 级别的 R R 隔离能解决幻读,其实 MySQL 级别的 R R 隔离并非论文里指出的 R R 级别 , 它实际上是 SI 的隔离级别。

那么,实现可串行化隔离级别的方式有哪些呢?第一种方式是严格按照串行顺序执行,这种方式肯定能保证隔离性。像 Voltdb,号称是最 fast 的内存数据库,通过串行保证隔离性,通过在存储过程里执行保证了原子性。Redis 是单线程跑,所以隔离性也没有问题,但是 Redis 只支持单语句事务,只能保证单语句的原子性。如果你需要 Redis 去做多行的事务,就需要自己去保证它的原子性。第二种,是运用 SS2PL 技术,加锁解决幻读 。比如:MySQL、SQL Server、Informix 的隔离级别,实际上都是通过 SS2PL 技术做到可串行化的隔离级别。第三种,可串行化的快照隔离 SSI,可解决写偏序问题。像 PostgreSQL、FoundationDB,都是这种模式的隔离。这三个都是正确的隔离,而其他的隔离都是对性能的妥协,都是不正确的。

6、如何通过 2PL 进行事务并发控制?

这里主要看下如何通过 2PL 进行事务并发控制。Eswaran 等人已经证明:遵守 2PL 算法的并发调度一定是可串行化的。什么是 2PL?是指在数据库事务处理中的两阶段锁定,前一个阶段只能加锁,后一个阶段只能释放锁。如果所有的事务都能遵循这样一个原则去处理,并发起来的最终结果就等同于某一种串行化的并发调度结果了。不考虑事务 Commit 时的失败,那 2PL 这就够了,已经能保证事务的一致性了。但是这个可串行化的可能太多了,而且 LockPoint 比较难找,所以大部分厂商的实现方式都是 SS2PL,都是把读锁和写锁的释放放到 Commit 阶段一起去执行。

问题是,从 2PL 到 SS2PL 到底有哪些限制?第一个是到 S2PL,它做了第一个限制,先把写锁放到 Commit,当一个事务对某个数据做了修改,但是没提交之前,所修改的数据的后像是不能被其他的事务读或者写的。SS2PL 则在 S2PL 基础上,把读写也放到了最后。如果一个事务做了修改,那么所做修改的数据的前像是没有被其他事务给读过的,SS2PL 最终达到的效果是,在所有的可串行化的并发调度里最终选择了某一种或者某几种,来保障事务的特性。

GoldenDB 是怎么做事务的并发控制的?GoldenDB 引入了全局事务管理器(GTM:Global Transaction Manager)实现全局事务的隔离性,并协助实现全局事务的原子性。

与 SS2PL 的比较,GTM 封锁是在事务提交之前,所有的 GTID 相关的数据都不能被读取和修改。活跃事务列表里的 GTID,相当于写锁。保障了写写和写读冲突下的数据一致性。另外,通过对读操作显式加锁,达到严格的 SS2PL 效果,保障了读写冲突下的数据一致性。而 SS2PL 只在事务提交的的时候才释放读锁和写锁。

GoldenDB 还有一个预锁机制,用来解决如下问题:DBProxy 查询到的活跃事务列表是旧的;DBProxy 基于活跃事务列表做的判断是不准确的。GoldenDB 的解决办法是,将所有的可能性冲突都认为是冲突。在返回活跃事务列表中,增加 Max_GITD,Proxy 判断如果 GTID 大于 Max_GITD,也认为该事物是存在冲突的。

总结下来,对于事务处理中的写一致处理,涉及两个方面:一个是满足条件的记录上无锁, GTID 不活跃,就可以直接修改数据,如果 GTID 活跃,这个时候需要通过预锁机制重新等待 GTID 被释放;另外是满足条件的记录上有锁,只能先走单机锁冲突的事务处理机制,等到锁释放了,再去判断 GTID 是否活跃。

对于事务处理中读一致性的处理:在脏读的场景下,不做任何判断,直接读,但是不推荐这种隔离级别;读已提交,如果有排它锁,证明这条数据有事务正在修改,直接读它的前像。如果无排它锁,GTID 不活跃,没有单机事务,全局事务,直接读当前数据行。如果无排它锁,GTID 活跃,说明没有单机事务,但是有全局事务,仍然不能读单机上的数据,需要去读 数据,这个前像数据的 purge 操作需要全局进行;在读已提交的场景下,有排他锁和无排他锁、GTID 活跃场景的处理方式是一样的;在可重复读,有排它锁的情况下,会按照 GTID 版本号去找,找到快照开始时候的版本号。在可重复读的场景下,无排它锁,GTID 不活跃,看上去没有问题,但是当前数据行有可能已经被提交过了,不能直接读,需要筛选版本判断是否需要找前像数据。无排它锁,GTID 活跃,也是一样的,需要去找满足条件的 GTID 版本号的事务版本;可序列化级别也如此,加共享锁无冲突,GTID 不活跃,可以直接读数据,如果 GTID 活跃,还可以读数据,但是要把活跃的冲突的 GTID 号给带上去,DBProxy 根据预锁的机制,判断什么时候 GTID 能正常释放,如果正常释放,数据就可以读到。不能的话,需要把事务重启。在可序列化、加共享锁有冲突的场景下,仍然按照单机锁冲突的并发控制去处理。

事务处理模块优化有哪些实践经验?

在 GoldenDB 上有个 GTM,是一个主要的优化点。怎么解决性能的瓶颈?首先,GTM 被设计得很简单,只有三个流程,负责 GTID 的分配、回收,负责向计算节点提供活跃事务 ID 的查询的一个接口。GTID 的分配和回收需要实时落盘,GTID 相当于一个大号的 Binlog ,一次写盘大概 1 μ s,理论上限 100 万 TPS,能否突破?另外一个优化点是,Proxy 和 GTM 之间的消息交互,对网络资源消耗过大,理论瓶颈 1.25GB 带宽如何利用?

1、申请 / 释放 GTID 优化:Proxy 批量请求

在释放 GTID 的时候,做了批量释放,能够做到减少交互次数,降低交互的数据量。在申请 GTID 的时候也做了批量申请,但是这里有个问题,在 RC 和串行隔离下面,可以任意使用批量申请,但是在 RR 隔离下面,是不能肆无忌惮的去使用批量申请的,在 RR 隔离级别下,是基于 GTID 的数据快照,要求 GTID 是严格单调的,所以在 GTID 申请的时候,还是要保障 GTID 单调,先来的事务先获得 GTID。但实际上,由于系统的调度都是有误差的,比如 Proxy1 向 GTM 申请了 GTID1,要去修改数据 A, Proxy2 向 GTM 申请了 GTID2, 也要修改数据 A,虽然是 Proxy1 先申请,但是他不一定能最快地给数据 I 加上锁,所以靠分配 GTID 去严格保障时间顺序,有误差。也就是数据库调度会有问题,不能保证先来的请求一定会先被执行。所以,在一定误差范围内,分配的 GTID 可以不保证严格单调的,需要做额外处理。Proxy1 先申请了 1000 个 GTID,我需要他在很短的时间内把这 1000 个 GTID 给用掉,如果不用,需要有一个废弃的机制,相当于保证 GTID 申请的这些事务在误差内保证单调。这样可也减少交互次数,降低交互数据量。

2、查询 GTID 列表优化:Proxy 进行组提交

Proxy 北向是一个数据库连接池,跟应用的连接池做交互,有的执行线程是在做建链,有的是在做 SQL 解析,但是也有可能他们都在做 GTID 的查询,如果所有的线程都能直接和 GTID 去做交互,交互数据量会很大。所以,代理线程可汇总执行线程的请求,然后跟 GTM 做单独的交互,并且这个交互是单线程单工的,使用一份活跃事务列表应答。当在途请求没有返回时,代理线程不会发送新的查询请求。

3、查询 GTID 列表优化:GTM 进行组提交

GTM 和 Proxy 是一对多的,多个计算节点去访问同一个 GTM,GTM 在汇总多个 Proxy 请求以后,他也可以产生一个子线程出来。专门处理 GTM 返回请求的拼装,用同一份活跃事务列表应答不同 Proxy 发来的请求。 当子线程没有拼装完返回列表时,不会处理新的请求,这和 Proxy 处理机制类似。

4、组提交优化后对预锁机制的影响

有一个问题是,刚在 Proxy 上和 GTM 上都做了组提交,但对之前的预锁机制,对性能到底有没有影响。在优化之前的场景下,Proxy 上所有向 GTM 发的查询请求,都需要过 0.5ms(32k 包大小万兆下的双向时延)才能收到响应,Proxy 获取到的事务列表都落后 GTM0.25ms;优化之后,Proxy 上所有的请求,在 0~0.5ms 内会收到响应,Proxy 获取到的事务列表的时间没有变化,仍然是落后 GTM0.25ms,经过组提交优化后,并未增加单消息处理时延。可能有人会问,如果以后性能要求更高的情况下,如何处理带宽占用的问题?我们可以通过自适应算法精简请求,依据请求数、统计时间和当前负载情况,实时修改发送周期。另外,可以通过数据压缩技术,通过记录 GTID 起始值 ( 8Byte ) 和偏移量位图 ( 255Byte ) ,将数据包大小由 32k 降到 1K 以内,压缩后万兆的带宽允许提供 1200 个 Proxy。所以,Proxy 和 GTM 的带宽不会是性能瓶颈。

5、申请释放 GTID 优化:GTM 线程优化

这是之前踩的一个坑,之前是按照做 DB 的思路去做 GTM 上的持久化,专门弄了一个 Flush 线程去写日志,但是没有考虑到一个问题就是 GTM 上的数据结构非常简单,消耗的不是 I/O,消耗的是 IOPS,专门弄一个线程去做异步处理,处理得很快,每次收到的请求很少,导致 IOPS 非常高,而且线程切换非常严重。但是,把它变成串行化处理了以后,请求积攒了,IO 量变大了,但是 IOPS 会降下来,不用做 CPU 的线程的切换了,CPU 也减低了。

经过各种优化后,GoldenDB 在这个数据模型下,经过了严苛的技术验证,实现了 3 亿客户 15 亿账户转账交易、明细查询 40000 TPS 的支撑能力。在这一过程中,还模拟了软硬件故障的各种测试,能满足核心业务强一致性要求,正确率达到 100%。同时,在这个场景下,做了当 Proxy 是瓶颈的时候,去扩展 Proxy;在 DB 是瓶颈的时候,去扩展 DB 的一个测试。其中,计算节点扩展线性率达到 100%,数据节点扩展线性率 95%。在故障隔离方面,计算节点故障、不影响整体系统交易;数据节点故障的时候,不影响其他节点交易。

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

1

添加新评论1 条评论

im_opim_op运维百度
2020-06-11 17:38
请问一下,GTM容灾是怎么做的,如果GTM宕机对系统会不会有影响。另外数据节点主备之间是如何同步的,是否是强同步

GoldenDB@im_op GTM做了多节点高可用,所有数据都是实时同步,并定时落盘。 一台挂机心跳检测不到立即切换。

2021-11-17 10:59
Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广