【.com原稿】行业背景银行业从最初的手工记账,到会计电算化,再到财务电算化,再到现在的金融科技。金融与科技的结合越来越紧密。人工智能、大数据、物联网、区块链等新兴技术改变了金融交易方式,为金融业创新提供了源源不断的动力。同时,互联网金融的兴起是一把双刃剑,既带来机遇,也带来挑战。普惠金融降低了金融门槛,更多普通民众参与金融活动,金融信息系统压力越来越大。所以我们可以看到,大型商业银行、保险公司、证券公司、交易所的核心交易系统都在进行分布式改造。其中,数据库作为一个有状态的应用,成为了信息系统中唯一的单点,承担着从上面施加的所有压力。随着数据库瓶颈的出现,分布式改造迫在眉睫。数据库分布式改造的方法数据库的分布式改造主要有三种方法:分布式访问客户端、分布式访问中间件和分布式数据库。由于其分布式能力是在不同层次(应用层、中间层、数据库层)实现的,因此对应用的入侵程度不同。其中,分布式访问客户端对应用的侵入性最大,改造难度最大,而分布式数据库方案对应用的侵入性最小,但架构设计和开发难度最大。分布式数据库的整体架构事实上,目前市面上的分布式数据库的整体架构都大同小异,由三个必不可少的组成部分组成:访问节点、数据节点和全局事务管理器。整体结构如下。接入节点负责sql解析、生成分布式执行计划、sql转发、数据聚合等;数据节点负责数据的存储和计算;全局事务管理器负责全局事务编号的生成,保证事务的全局一致性。这种架构或多或少受到了GoogleSpannerF1论文的影响。本文主要分析了这些组件实现的难点以及如何设计架构。二阶段提交的问题我们知道二阶段提交是一个阻塞协议,这也是它最大的问题。下图以pgxc架构下的两阶段提交为例,主要分为几个阶段:①:CNprepare->②:AllDNprepare->③:CNcommit->④:AllDNcommit想象一下如果你commitincn阶段发生cn/dn宕机会怎样?如果宕机发生在cn发送cncommit命令后,dn收到commit命令后会commit,但返回commitok时发生cn宕机,事务进入阻塞状态。如果cn发送commit后某个dn宕机,会导致部分dncommit成功,部分dncommit失败,造成不一致。但是如果dn重启了,会继续去cn获取事务提交信息,发现是commit状态,就会继续执行commit操作,提交之前的事务。在这个地方,我们可以讨论一个更极端的情况。如果此时cn也down了,那么失败的dn重启后,去cn获取状态,发现获取不到。此时失败dn上的事务处于pending状态,不知道是该提交还是回滚。这时候应该有一个进程可以从其他dn发现状态并报告给故障dn,通知它提交。这个角色就是pgxc_clean进程。其实前面案例中事务的回滚也是这个过程的功劳。那么再深入一点,如果dn是唯一参与事务的,那么此时pgxc_clean无法从其他dn和cn获取状态,dn确实是pending。为了解决两阶段提交的阻塞问题,出现了三阶段提交。三阶段提交在提交之前引入了cancommit的过程,同时加入了超时机制。因为如果协调器宕机了,参与者无法知道协调器是发送了提交还是中止。三阶段的cancommit过程是通知参与者我发送了commit或者abort命令。如果此时协调器出现故障,参与协调器可以在等待超时时间后选举出新的协调器,协调器知道应该下达什么命令。三阶段提交虽然解决了阻塞问题,但是并不能解决性能问题。为了保证分布式系统中事务的一致性,需要与每个参与者进行通信。一个事务的提交和参与需要分布式系统中各个节点的参与。带来了延迟,但是在10G、infiniband、roce高速网络的支持下,这已经不是问题了。CAP和BASE之间的选择我们知道,分布式系统无法战胜CAP。那么设计分布式系统时如何选择呢?首先必须保证P(partitionfaulttolerance),因为分布式系统必须有多个节点(partition)通过网络互连,而网络是不可靠的。分布式系统是为了避免单点故障。如果由于网络问题或部分节点故障导致整个系统不可用,则不符合分布式系统的设计初衷。如果保证A(availability),那么当网络出现故障时,网络隔离的不同区域会继续提供服务,会造成不同分区数据不一致(裂脑);如果保证C(一致性),那么当网络出现故障时,需要等待不同网络分区的节点完成同步数据。如果网络不断出现故障,系统将因为无法同步而保持不可用状态。2PC是牺牲可用性来保证一致性的典型例子,而BASE(基本可用,软状态,最终一致性)是牺牲一致性来保证可用性的例子,因为牺牲实时强一致性代价太大,它允许某些时间窗口的数据不一致,通过在窗口中记录每个临时状态日志,当系统出现故障时,通过日志继续完成未完成的工作或取消已完成的工作并返回到初始状态,这种方式实现了最终一致性有保证。BASE偏离了传统的ACID理论。符合BASE理论的交易也称为灵活交易。当故障发生时,需要相应的补偿机制,与业务耦合度高。其实我并不认同BASE的做法,因为它已经背离了数据库最基本的设计理念。raft的优点,不管是上面的XA还是BASE,都不能完全解决一致性问题。真正意义上的强一致性必须基于强共识协议。Paxos和Raft是目前主流的两种共识算法。Paxos诞生于学院派。它是一种基于分布式环境中消息传递的共识算法。最初设计时考虑的是一个通用模型,并没有过多考虑实际应用,而paxos考虑的是多个节点的同时写入。这使得paxos的状态机极其复杂,难以理解。不同的人可能理解不同的意思。这受到了批评。比如MGR引入了writeset的概念来处理多点写冲突。问题,在热点数据高并发的场景下是不能接受的。因为Paxos比较难理解,斯坦福的两个大学生设计了Raft算法。相比之下,Raft是一种工业风格。同时只有一个leader,follower通过日志复制实现一致性。与Paxos相比,Raft的状态机更加先进。易于理解和实现,因此在分布式环境中有广泛的应用,如TiDB、RadonDB、etcd、kubernetes等。Raft协议将共识问题分解为三个子问题分别解决:领导人选举、日志复制和安全。Leader选举:服务器节点有三种状态:Leader、Follower和Candidate。正常情况下,系统中只有一个leader,其他节点都是follower。领导者处理所有客户端请求,追随者不会主动发送任何请求,而只是响应领导者或候选人的请求。如果follower没有收到消息(选举超时),那么他就成为候选人,发起选举。在集群中获得最多选票的候选人成为领导者,而领导者仍然是领导者,直到它死亡。Raft算法将时间划分为任意长度的任期,每个任期开始于选举,其中一个或多个候选人试图成为领导者。Ifacandidatewinstheelection,thenheactsasleaderforthatterm.follower要开始一个选举过程,首先要增加自己的当前任期数,并切换到candidate状态,然后他会并行向集群中的其他server节点发送RPC请求投票给自己,candidate会继续投票maintainThecurrentstateismaintaineduntiloneofthefollowingthreethingshappens:(a)hewinstheelection,(b)anotherserverbecomestheleader,and(c)nocandidatewinstheelection.Acandidatewinstheelectionandbecomestheleaderwhenitgetsthevotesofthemajorityofnodesintheclusterforthesametermnumber.然后它向其他服务器发送心跳消息以建立其权限并防止创建新的领导者。下图是三个角色的过渡状态机。日志复制:当领导者被选举出来后,他作为一个服务器来处理客户端的请求。来自客户端的每个请求都被视为需要由复制状态机执行的指令。领导者将该命令作为新的日志条目追加到日志中,然后向其他服务器并行发出追加条目RPC以复制日志条目。当日志条目被安全复制后,领导者将日志条目应用到其状态机并将执行结果返回给客户端。如果跟随者崩溃或网络丢包,领导者将反复尝试附加日志条目RPC(尽管回复客户端)直到所有跟随者最终存储所有日志条目。下图显示了复制状态机模型。安全性:安全性是指每个复制状态机都需要按照相同的顺序执行相同的指令,以保证每个服务器上数据的一致性。假设某个follower在一段时间内不可用,之后可能会被选举为leader,从而导致之前的日志被覆盖。Raft算法通过增加一些对领导者选举的限制来避免这个问题。此限制可确保对于给定的任期编号,所有领导者都拥有上一任期的所有已提交日志条目。日志条目只会从leader传递给follower,follower将不需要将日志传输给leader,因为新的leader缺少日志,leader永远不会覆盖本地日志中已经存在的条目。Raft算法导致选民在投票时拒绝那些没有自己的日志的新投票请求,从而阻止候选人赢得选票。CN的设计接入节点的设计看似简单,但有些地方还是有些玄机。设计cn的关键考虑因素是cn应该是重的还是轻的。这是一把双刃剑,主要表现在以下两个方面。1、如何实现sql语法兼容?接入节点主要负责sql的解析,执行计划的生成和传递,这些东西其实都是sqlparser来完成的,我们可以直接使用mysql或者pgparser甚至server层来进行sql的解析和执行计划的生成,而它天然兼容MySQL或者pg的语法。2、如何处理元数据的问题?上面的方案看起来很完美,但是有一个问题:如果直接把mysql或者pg的server层搬过来,metadata怎么办?是否把元数据放在cn上?如果不放元数据,那么就需要一个统一的地方来存储和管理元数据。我建在cn上的表需要去一个固定的地方去更新元数据信息,查询也是一样的。如果元数据存储在cn上,那么需要在cns之间同步元数据的更新。如果cn出现故障,任何ddl操作都将挂起。这时候就需要有一种机制:当cn超时,没有响应后从集群中移除cn。DN设计数据节点的设计主要考虑以下几个方面。1、如何让数据节点高可用?数据库数据当然是最宝贵的。任何数据库都必须有数据冗余方案。数据节点必须是高可用的。在保证rpo=0的基础上,尽量缩短rto。仔细想想,其实每个dn都是一个数据库实例。这里我们以MySQL或者pg为例。MySQL和pg都有高可用方案。无论是基于主从半同步还是流复制,都可以作为dn级别的数据库实例。数据冗余和交换方案。当然,有些数据库在dn层面引入了paxos、raft、quorum等强一致性方案,这也是分布式数据库中很常见的设计。2、如何实现线上扩容?在线扩展是分布式数据库的一个巨大优势,数据节点的扩展必然涉及到数据向新节点的迁移。目前市面上的分布式数据库基本实现了数据的自动重载。分散式。但是,仅仅自动重新分配数据库是不够的。如何只迁移一小部分数据来降低服务器的IO压力成为了一个关键问题。传统的哈希方法是根据分区键的哈希值对分区数进行取模运算,结果就是数据应该落在哪个分区。但是这种分布方式在增删节点时会造成大量的数据重新分布。一致性哈希的核心思想是每个分区不再对应一个数字,而是对应一个范围,计算出的哈希值与范围匹配。大致思路是将数据节点的hash值和key映射到0~2^32圈,然后从映射值的位置开始顺时针查找,将数据保存到找到的第一个节点。如果超过2^32都找不到服务节点,则保存到第一个节点。一致性哈希最大程度解决了数据重新分布的问题,但是可能会造成节点数据分布不均的问题。当然这个问题也有一些改进,比如增加虚拟节点。GTM的设计GTM,顾名思义,是一个全球性的概念。分布式数据库最初旨在实现可扩展性、提高性能和降低全球风险。然而,GTM打破了这一切。1、为什么需要GTM?简单总结就是:GTM是保证全局读一致性,而两阶段提交是保证写一致性。这里我们可能有误会。如果没有GTM,会不会造成数据不一致?是的,但这只是在某个时间点的阅读不一致。这种不一致也是暂时的,但不会造成数据写入不一致。写入的一致性通过两阶段提交来保证。我们知道postgresql是通过快照来实现MVCC和事务可见性判断的。对于读提交隔离级别,要求每个事务中的查询只能看到事务开始前已经提交的更改,以及当前事务中查询之前所做的更改,必须通过快照来实现。快照数据结构中会包含事务xmin(插入元组的事务编号)、xmax(更新或删除事务的事务编号)、运行事务列表等相关信息。pg的每个元组头信息中也记录了交易的xmin和xmax信息。Pg获取到快照后,会判断事务可见性。对于所有id小于xmin的元组,对当前快照可见,同时,id大于xmax的元组对当前事务可见。目前扩展到分布式集群后,每台机器上都有pg实例。为了保证全局读一致性,需要一个全局组件负责快照的分配,使得快照信息在各个节点之间共享。这是GTM的工作。2、GTM高可用的问题?GTM是唯一分配全局快照和事务ID的组件。只能有一个GTM。当然GTM可以主备高可用,但是同时工作的只有一个GTM。gxid信息在主备之间这会产生问题。虽然其他节点是分布式的,但GTM始终是一个单点。当出现单点故障时,就会涉及到切换。切换过程影响整个世界。为了保证切换后的gxid信息不丢失,必须在GTM之间同步gxid。针对高可用的问题,可以剥离GTM的交易号存储信息,将交易号信息存储在第三方存储中。例如,etcd就是一个不错的选择。etcd是一个分布式存储集群,具有强一致性和高可用性。与etcd相比,etcd轻量级,适合存储交易号信息,同时保证高可用和强一致性。此时GTM不需要同步master和backup之间的gxid。如果发生主备切换,新的masterGTM只需要去slave的etcd中获取最新的事务号,写入事务号也是如此。主GTM会将交易号信息写入主etcd节点,通过etcd自身的raft复制协议保证一致性。这样的设计为GTM减轻了很多压力。3.GTM性能问题?GTM是大多数分布式数据库的性能瓶颈,这使得集群的整体性能比单机还要差。也很容易理解,任何交易开启时,首先要从cn获取交易号和快照信息给GTM,然后分析结果发送给dn执行,cn再汇总返回给应用.路径明显更长了,那么效率肯定会变低。目前的优势是可以利用多台机器的组合能力进行计算,计算资源得到了扩展。当然,GTM的瓶颈问题是有解决办法的。比如华为GaussDB提出了GTM-Free和GTM-Lite。gtm-free是在对强一致性读要求不高的场景下禁用GTM的功能,一切都不使用GTM。在这种情况下,性能基本上可以线性提升。这个功能已经实现;提升也很明显,这个功能还在研发阶段。分布式数据库如何实现PITR数据库的PITR一般是通过基础备份加上连续wal归档来实现的。这个基本备份可以在线,因为它不需要数据库当时处于一致状态。可重现性可以通过重播重做来实现,因此基本备份可以是文件系统tar命令,而不需要文件系统级快照。PITR可以通过基本备份加重做日志的方式恢复到任意时间点。不同的数据库对这个时间点有不同的定义。它可能是lsn、快照或时间戳。在Postgresql数据库中,任何时间戳都可以基于redo来恢复。从理论上讲,分布式数据库的PITR与单个服务器的PITR没有太大区别。每个节点备份自己的基础数据。这个数据不需要一致性,但是必须考虑分布式事务的问题。在做基础备份的时候,一定要保证之前的分布。事务(如果存在)已经完全完成,因为分布式事务是两阶段提交协议,在2PC提交阶段不同机器的提交之间肯定存在时间差。如果你在这个时间差做备份,你会发现上一台机器有这个事务,另一台没有,所以恢复会造成数据不一致。这个问题可以通过pg中barrier的概念来实现。分布式事务结束后,创建屏障获取一致性点,然后进行基础备份。对于redo前滚,只需要将所有节点的redo前滚到一个一致的点即可。作者介绍:张晓海,就职于大型商业银行,目前主要负责数据库管理和新技术研究,PostgreSQL技术推广,个人公众号:数据库架构之美。【原创稿件,合作网站转载请注明原作者和出处为.com】
