腾讯金融级分布式数据库TDSQL的前世今生

  • 来源:csdn
  • 更新日期:2020-04-30

摘要:作者:潘安群,腾讯TEG/计费平台部技术总监,2007年毕业于华中科技大学,随即加入腾讯,一直负责计费领域相关系统建设工作,先后主导账户平台、风控平台、HOLD

作者:潘安群,腾讯TEG/计费平台部技术总监,2007年毕业于华中科技大学,随即加入腾讯,一直负责计费领域相关系统建设工作,先后主导账户平台、风控平台、HOLD及TDSQL、计费集群等平台建设。

本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅2017年《程序员》

TDSQL(Tencent Distributed MySQL,腾讯分布式MySQL)是由腾讯技术工程事业群计费平台部针对金融联机交易场景开发的高一致性数据库集群产品。该产品第一个版本诞生于2012年,当时主要承载公司数字支付相关业务,包括Q币Q点、包月、各类点券等核心支付数据的托管;2014年被WeBank选中,作为其核心系统的数据库解决方案;2015年,在腾讯金融云上正式推出,作为唯一一款金融级数据库产品,为金融、政企机构提供数据库的公有云以及私有云服务。

TDSQ作为我们计费平台第四代数据库产品,是计费平台2014年以来,对金融类存储产品的思考沉淀之作,同时作为一款定位于金融的数据库,其产品质量也必须符合金融行业标准:数据多备份高一致、零丢失、高可用。正因如此,本文试着从计费平台(TBOSS)四代存储系统的发展过程,来谈谈TDSQL在存储平台数据一致性、可用性和易用性等方面的一些考量。

何为一致性

谈到一致性,业界常提的有传统关系型数据库ACID理论的C,以及分布式系统中CAP理论的C,虽翻译过来都叫“一致性”,但从狭义角度来看,此C非彼C:ACID中的C通常指事务一致性,CAP中的C通常指数据复制一致性。但从广义角度来看,二者又是相通的。

【ACID中的C】

ACID理论中的C,是在事务执行过程中,对外界应看到系统何种状态的约束(相对的,原子性是对系统如何执行这个事务的约束:要么全部成功,要么全部不执行)。严格意义来说,一致性要求在事务执行过程中,系统不能看到该事务执行过程中的中间状态。举个例子:假如银行里一共有100个个人账户,每人有10块钱余额,彼此之间可以转账,但无论同时发生多少次转账,我们任何时候看到的银行总余额应该始终是1000块,不多不少。

【CAP中的C】

分布式中的C,一般是指数据多副本情况下的复制一致性,即数据虽然多副本,但需要保证任意时刻,从系统中读到的数据都是一致的。例如某人的银行账户需扣款100元,在分布式系统中,执行该操作后,可能部分副本数据已更新为扣除后余额,但是部分副本可能数据还没有更新,那这就是一个中间状态,强一致性则要求这个中间状态不可见。用户在查询个人余额时,不能时而查询到扣除前的余额,时而又查询到扣除后的余额。初看起来,它与ACID中的C是两个概念,但如果把分布式系统中的一个写入操作当作一个事务(这个事务由多个写操作组成,因为需要在不同的服务器写多个副本),那这两个C概念又是相通的。

【一致性、隔离性与锁】

谈了一致性,就不能不谈隔离性。ACID中四种不同的隔离级别,理解起来比较晦涩。我们先抛开具体的隔离级别,而从下面的角度来看,就容易理解得多:系统要实现最严格的事务一致性,最简单的办法就是用一把全局锁,这样就实现了可序列化隔离级别,但这种情况下,系统的吞吐量极差,于是我们考虑将锁粒度缩小,再加入读写锁等机制以提升系统吞吐量。锁的粒度越小,系统的一致性就越弱,于是各种隔离级别应运而生,不同的隔离级别就是系统在吞吐量与一致性之间的不同平衡程度。

备注:在各种数据库实现中,通常为了进一步提升吞吐量,并不是简单地用锁控制并发,往往会结合一些更为复杂的实现,例如MVCC等。

但在分布式系统下,如果继续使用锁机制来实现一致性,会因为各种因素,例如网络时延、网络故障、机器故障等情况,导致这个锁实现异常复杂,需要考虑的情况太多。

【Paxos、ZAB、Raft】

在分布式系统中确保各个节点数据一致性,Paxos是业界指定的唯一算法,经过严格数学证明,但是很不好理解,而且工程实现难度很高,所以后面出现了一些变种,例如ZooKeeper的ZAB协议,以及现在比较火的Raft协议,均是Paxos协议变种,但是从工程角度来看,无论可理解性,还是可实现性,均有大幅提升。

【CAP】

这里补充说明一下,CAP理论作者Eric Brewer,在2012年在《Computer》杂志发表新文章《CAP Twelve Years Later: How the“Rules”Have Changed》,中文版《CAP理论十二年回顾:“规则”变了》:

“首先,由于分区很少发生,那么在系统不存在分区的情况下没什么理由牺牲C或A。其次,C与A之间的取舍可以在同一系统内以非常细小的粒度反复发生,而每一次的决策可能因为具体的操作,乃至因为牵涉到特定的数据或用户而有所不同。最后,这三种性质都可以在程度上衡量,并不是非黑即白的有或无。可用性显然是在0%到100%之间连续变化的,一致性分很多级别,连分区也可以细分为不同含义,如系统内的不同部分对于是否存在分区可以有不一样的认知。”

在文章后面,我们也会来看看我们对CAP的理解。

何为可用性?

对于存储系统来说,可用性一般要处理下面这两种情况:

出现故障时,如何处理请求。分布式系统中,故障的情况有很多,例如网络故障、服务器故障、软件故障等; 出现性能或容量瓶颈时,导致的系统吞吐量降低,时耗增加等不可用问题。

何为易用性?

对于一套存储系统,易用性主要包含两个方面:

提供给业务开发人员的API接口。例如常见的存储系统接口有:标准SQL、KV等。一般来说SQL比KV功能更丰富,易用性更高。此外是更强的一致性,应用程序无需考虑数据不一致的情况,易用性也更高。 提供给DBA运维人员的运维接口。例如监控体系、发布体系、故障定位及处理体系、备份系统等。一般来说周边配套设施越完善,易用性越高。

TBOSS存储平台的历程

下面,我们将从TBOSS存储平台从1.0到4.0的发展历程,来谈谈TDSQL在数据一致性、可用性及易用性等方面的考虑。

【2002-2008:1.0,MySQL热备】

1.0属于起始阶段,数据的备份容灾,主要依赖于MySQL的异步复制机制,主MySQL提供服务,备MySQL Standby,按天冷备数据到异地。该架构的优势是简单粗暴,但数据备份依赖于MySQL的异步复制机制,在主备数据同步有延迟时,如果主机发生故障,系统会有数据丢失,业务系统不能直接切换到备机。所以通常情况下,如果主机发生了故障,对于账户类数据,需要先利用业务日志来修复备机数据后,再切换到备机。

图1  主备切换

一致性与可用性:从CAP角度来看,在P发生时,备机数据恢复之前,直接让系统此时不可用,也就是完全忽略A,保障C。

易用性:系统足够简单,开发、运营易用性均比较高。

【2008-2013:2.0,TBOSS 7*24容灾】

1.0架构在故障(P)时,要么忽略A保障C,要么忽略C保障A的机制,明显控制粒度过大。我们其实知道在故障发生时,主备MySQL没有同步的数据只是极少数,其他绝大部分数据都已同步,是一致的。有没有可能出现故障时,只让未完成同步的数据不可用,而大部分已经一致的数据继续可用呢?这也就达到了更细粒度的控制:在P发生时,仅仅牺牲少量可用性,来保证一致性。

于是我们在2.0架构里,引入了一个中间模块,叫一致性控制中心(CC)。如果从ACID事务角度来看一致性,那CC可以看到是一个事务管理器;如果从锁机制来看一致性,那它就可以视作一个锁管理器。应用层在每次更新数据之前,先告之CC,即将更新某数据,麻烦先将该数据加“锁”。“锁”的释放由CC来负责,CC会根据版本号不断比对主备数据之间的一致性,一旦数据一致,则释放锁,否则继续持有。大部分情况下,这把锁会很快被释放。一旦故障发生,CC中会持有全部未同步数据的锁(黑名单),此时如果应用层需要切换到备机,需要先到CC拉取这份黑名单,将这些数据置为不可用状态,而其他数据则可以直接访问备机。此外,需要注意的是,这把锁是异步的,在故障未发生的情况下,它释放与否,并不影响正常的数据访问及更新。

图2  访问控制架构

一致性与可用性:引入CC机制后,故障发生时,在C与A之间,我们确实做到了一个不错的平衡。

易用性:但系统在易用性上存在很大问题:在容灾逻辑上,应用层与CC、数据层耦合太紧,例如故障发生时,需要应用层去拉取未同步数据的黑名单,以及应用层决定何时切换到备机模式,每次数据访问需要先上报CC等,这些都使得应用层程序嵌入太多容灾逻辑,开发难度增高。

【2010-至今:3.0,厚德(HOLD)存储平台】

厚德平台试图将CC与应用层剥离,让应用层不再关注容灾逻辑,把容灾逻辑下沉到存储层,让存储系统更加通用。

图3  厚德存储平台

延续2.0的思路,那首先要解决的问题就是如何锁定未同步数据。在2.0时代,由应用层来负责上锁,例如账户系统可以直接按照QQ或OpenID来锁定。但如果让数据层来负责上锁,且继续使用MySQL做存储层,并为应用层提供SQL接口,那CC需要关心每条SQL所对应的PrimaryKey(PK)是什么,以便将数据锁定,但这块难度很大。举几个简单例子:

建表时,没有指定PK字段,这种除非深入存储引擎层,否则拿不到SQL对应数据的PK。 Where条件中没有明确PK值的SQL语句,如update tbl_name set status=0 where mod_time>11111。

如上面的例子,如果试图去锁定这些SQL对应的数据,无法不在存储引擎层进行。

所以我们换一个思路,自研一套KV系统来做存储引擎,这里有几个好处:

KV系统要求每次访问都通过Key来访问,如此一来,CC就能很方便锁定Key,并在主备实现同步后,将Key对应的锁释放。 出现故障时,拉取黑名单也不再需要业务层处理,即将接管服务的Cache自动从CC拉取,如果业务继续访问黑名单中的Key,直接报“数据不可用”错误即可。 KV使用全内存存储,性能极高,轻松应对业务高峰。

此外,HOLD实现了在线自动扩容机制,能实现在系统容量不够的情况下,一键扩容,尤其是在业务快速发展期,这个功能尤其重要。举个例子,2013年我们HOLD系统扩容才20+次,到2014年,随着手游业务井喷,HOLD实现扩容496次,这在2.0时代,几乎是不可能完成的事。

但是自研KV系统,也引来了一个新问题——迁移问题。计费作为公司公共服务,要求7*24提供服务,历史数据在线迁移,所以从2.0架构迁移HOLD架构,有诸多困难:

异构数据平滑迁移:对于每一个应用,我们可能都得要写一个SO来实现数据的在线迁移、比对等工作。 功能接口变化大:对于现有系统来说,需要从SQL接口切换到KV接口,相当于重写一遍应用程序。 KV提供的功能不够丰富:对于应用程序而言,之前使用SQL做的一些逻辑,需要应用层自己解决,例如HOLD系统不支持范围查询,不支持事务等功能。

当然我们的HOLD系统,提供了一个插件机制,可以在一定程度上扩展系统功能,实现类似Cassandra的Lightweight transactions。例如要实现V=V-1,普通KV系统需要两次请求:get k, v; v1 = v - 1; set k, v1;在HOLD系统中,你可以写一个SO插件,让HOLD提供一个除get和set命令外的扩展命令,例如dec k。

一致性与可用性:HOLD在继续保持2.0时代CA优势的同时,通过自动扩容、全内存存储等机制,解决了系统容量与性能问题,进一步提升了可用性。

易用性:应用程序不再需要关心数据层容灾逻辑,开发及运营体验更佳。对于新建系统,且数据结构简单,性能要求高的使用场景,HOLD对于应用层开发和运营都是友好的。但是对于之前已经使用MySQL,且使用了一些复杂SQL逻辑的应用,则迁移成本较大,不太友好。

【2012-至今:4.0,TDSQL】

TDSQL的诞生,除了要解决HOLD系统所面临的老系统割接困难的问题,还有如下几个契机:

公司内开始大规模使用SSD,IOPS相对于机械硬盘有质的变化,能满足大部分系统的性能需求。利用好SSD优势,能极大简化存储系统的架构体系。

以KV为代表的NoSQL无法形成业界标准及规范,反观以Oracle(商业)、MySQL(开源)为代表的传统关系型数据库生态完善,使用范围也更广,更容易被开发人员接受。

MySQL在5.5版本引入半同步复制,5.6版本引入GTID等机制,而且经过5.5,5.6,5.7等几个版本,开源的MySQL在性能、数据强一致性方面有了质的提升。

于是我们思考,能否基于MySQL(我们实际上用的是MariaDB),开发一套存储系统,既能保留原有优势:

强一致性 高可用 自动扩容

这里似乎就有两个思路:

为HOLD增加一个SQL解析层:

如果坚持这种思路不断优化下去,最终做出来应该就是一个MySQL解析层,外加一个分布式存储引擎。但这将会消耗极大的人力投入,且时间周期会比较长,也难跟上社区的节奏。

备注:其实MySQL官方提供了MySQL Cluster这一个产品,使用NDB这种全内存的存储引擎,但是我们分析后,发现该产品实用价值不大。

直接基于MySQL做一套全新系统:

这种思路,紧贴社区,充分利用MySQL强大的社区力量,并在此基础之上,加上集群管理功能,来实现故障容灾切换、强同步、自动分库分表等。但在这种思路下,我们原来的黑名单机制已经无法继续使用,必须寻找新思路。

在一致性方面,我们想到MySQL Binlog的严格有序性以及强同步复制机制,再结合Raft协议思想(Raft协议核心两点就是Leader选举、日志复制),我们最终实现了TDSQL的强一致性以及数据零丢失,大体架构图如图4所示。

图4  强一致架构

2-1. 强同步机制:基于MySQL半同步复制优化,对于进入集群的每笔更新操作,都将发到对应Set(每一个Set包含3个MySQL实例:一主两备)的主机上,主机会将Binlog发往两个备机,且收到其中任意一个备机应答后(仅仅是备机收到了Binlog,但可能还没有执行该Binlog),然后才本地提交,并返回给客户端应答,这就能确保数据零丢失。此外,强同步机制势必会影响系统吞吐量,所以我们也优化了MariaDB的线程池实现,以提升并发性能。

2-2. 可用性:在这种强同步机制下,建议是存3个数据副本,且分别分布在3个IDC,这样在任一IDC故障情况下,剩下两个IDC依然能够提供服务。如果仅存2个副本,在某中一个副本故障情况下,系统会变为不可用。

Leader选举: MySQL节点本身无法直接参与选举,于是我们由Scheduler模块来负责这个事,如果主机发生故障,Scheduler会从两个备机中,选择一个数据较新的备机(因为MySQL Binlog是严格有序的,所以谁同步主机Binlog的偏移量更大,谁的数据就更新。当然也可以基于GTID来判断)提升为主机。

图5  TDSQL分支版本比较

自动扩容:目前TDSQL有两个分支版本,一个是No-Sharding版本,一个是Group-Sharding版本,NS版本不支持自动扩容,GS版本支持自动扩容,但是该版本对SQL有比较大的限制。

Group-Sharding虽然不支持跨节点事务和JOIN,但是在一个Group内,可以支持JOIN和事务。举个例子,假设有两张表:用户信息表与用户账户表,我们可以将这两张表组成一个逻辑Group,且这两张表都按用户ID字段Sharding,后续Group内任意一张表需要Re-Sharding时,该组内所有表都同时进行Re-Sharding(相当于按Group进行Sharding),这样单个用户相关数据一定会落在一个Shard上(即同一个MySQL实例),于是在单个用户条件下,这些表之间可以做JOIN和关联。

易用性: TDSQL相对于前面3代系统,已经有了大幅提升,但是其分布式OLTP数据库定位,跟传统的关系型数据相比,在JOIN和事务方面,依然有很大差距,这也是我们后续研究的重点。

应用场景:目前TDSQL承担公司计费平台100%的数据存储,也是腾讯金融云的数据库解决方案,为WeBank的核心交易系统提供数据库服务。共计部署服务器数量达1500+。

TDSQL的未来

TDSQL在未来一段时间,将聚焦以下几个方面:

周边配套工具的完善,例如监控体系、备份机制优化等 分布式事务 回馈社区

总结

回顾从主备模式到TDSQL,虽然其间做了很多细节,但核心思想可能就下面几点:

正确认识CAP理论:在P出现时,CA不是0与1关系,需要根据业务场景在C与A之间做好平衡;在未出现P时,A没有问题,此时需要平衡的是C与延迟(Latency)。

提升系统易用性:不断将容灾逻辑下沉到存储系统,为开发和运维人员提供更好的交互接口。

及时更换硬件:充分利用新型硬件,可有效降低系统复杂度。不要一味追求水平扩展,有时垂直扩容效果也不错。

跟紧业界与社区:TDSQL发展已有2年多,目前来看该思路正确,我们的选型与设计,跟开源社区的发展基本一致。但回想2008年我们开始做TBOSS 2.0时,几乎是自己关起门来,逐个分析需求,逐步讨论并实现,每个细节每个坑都是自己趟出来的,虽最终思路跟业界也是一致的,但耗费的时间太长。尤其现在技术发展越来越快,借助社区的力量,明显更有效率。

关于回馈社区,目前我们做得还不够,未来不仅要贡献我们的思路,也希望能直接给社区贡献代码。

虚机02.jpg