分布式协调机制导论
|0x00 分布式系统缘何兴起
什么是分布式?简单来讲,是将相同或者相关的程序,运行到多台服务器上,实现特定目标的一种方式。从外部的视角来看,一组服务器,展现给用户的是一个统一的整体,使用起来就像单机系统一样。因此,不论是数据的并行计算,或者是任务的并行调度,都是分布式的一种形态,而我们对分布式发展的最主要驱动力量,则来自于对“性能、可用性和可扩展性”的不懈追求。
从发展过程上讲,分布式系统经历了单机 - 并行 - 分布式三个阶段,以“火车订票系统”为例,我们能够明显感知到这种发展的过程。
单机阶段:
1.png
并行阶段:
2.png
分布式阶段:
3.png
分布式并不仅仅指数据集群,也指业务系统发展的终极形态。现在“架构师”、“领域建模”这些概念之所以兴起,与分布式系统逐步成为开发标准,是密不可分的。试想,如果领域拆分不清楚,那么“高内聚、低耦合”肯定就搞不清楚,后期再维护的成本可就太高了。
本文讲解一下分布式协调服务需要考虑的三大块内容:事务、锁与互斥、选举机制。这一部分是分布式技术的基础,也是产出了大量论文的地方,就像是管理团队一样,管理几个人、十几个人,我们可以用“集中式”的方法来做,但管理几千人、几万人的团队时,就需要考虑协调各个子团队之间的关系了,而这正是分布式协调服务关注的。事实上,很多算法的设计,也是参考了管理学的经验。
|0x01 分布式事务
分布式事务,是在分布式系统的基础上,实现事务的方式。我们首先了解一下“事务”的基本概念:事务是“一系列不可分割的执行单元”集合,例如在电商系统中,在订单支付成功之后,就需要修改库存、物流下单、扣除优惠券等各种操作,这些都需要绑定在一起执行。只有所有操作都被正确执行的情况下,事务才能被提交,任何一个阶段的失败都需要整个事务的回滚,可以说:“All or Nothing”。
关系型数据库的是面向事务的,遵循“ACID”原则,但在分布式系统中,则扩展为了“CAP”或者“BASE”理论,以应对更为复杂的场景。感兴趣的可以点击这里扩展阅读。
那我们如何实现分布式事务?首先看一个经典的“两将军问题”。
网络领域有一个经典的问题:“两将军问题”,与“拜占庭将军问题”的区别在于,两将军问题探讨的是不可靠信道下两方的通信准确性问题,而拜占庭将军问题探讨的是多方通信结果一致性和决策正确性的问题,事务需要参考的是“两将军问题”。
4.png“两将军问题”如图所示,白军驻扎在沟渠里,蓝军则分散在沟渠两边。白军比任何一支蓝军都更为强大,但是蓝军若能同时合力进攻则能够打败白军。他们不能够远程的沟通,只能派遣通信兵穿过沟渠去通知对方蓝军协商进攻时间。问:是否存在一个能使蓝军必胜的通信协议?这就是两军问题。
类似的问题在分布式中太常见了,例如在电商系统中,当用户下单后,同步进行扣款操作时,如果网络故障导致两将军问题出现,扣款请求重复发送,产生重复扣款的结果,显然就是P级故障了。因此在系统设计的时候,就需要保证一次下单中,扣款请求无论被发送多少次,接收方有且只执行一次扣款动作。
分布式事务大体上有两种解决思路,二阶段/三阶段提交方法,以及基于消息最终一致性的方法,目前并没有一种简单的、能够应对所有场景的方法,都需要我们灵活应用。
“二阶段”指“基于XA的二阶段提交协议方法”,XA是指“分布式事务协议”,规定了分布式事务处理模型。如果简单理解,就是XA把分布式事务的处理,分成了两个阶段,第一个是投票阶段(Voting),第二个是提交阶段(Commit)。
在第一阶段中,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不会修改数据库中的数据),待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。
当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令。
这里有个问题,即二阶段提交方法,是阻塞线程机制的,因此很容易发生单点故障。
因此,实际应用中,大家会把二阶段提交改成三阶段提交的方法,即在CanCommit和DoCommit之间,增加一个PreCommit阶段,即准备阶段,以应对响应超时的问题。这样在业务发生时,顺序就变成了:CanCommit:询问是否可以提交事务;PreCommit:预提交,如果超时则回滚;DoCommit:正式提交。
TCC的设计与二阶段或者三阶段不同,它是一种补偿型事务机制,TCC设计了三个阶段,分别是:Try:在业务逻辑阶段把数据操作更新到中间表并记录操作痕迹。Confirm:把所有中间步骤更新到原表操作。Cancel:回滚。
TCC要求每个服务都提供Try、Confirm和Cancel三个接口,核心思想是通过对资源的预留(提供中间态),尽早释放对资源的加锁,如果事务可以提交,则完成对预留资源的确认,如果事务要回滚,则释放预留的资源。因此可以理解为,TCC是业务的分布式事务,最终一致性,不会出现长事务的锁风险。
但以上三种方法,均是以“集中式”的思路来实现分布式事务,存在的共同问题是:同步性能差、数据不一致。为了解决这两个问题,基于消息最终一致性的方法,便诞生了。
基于消息最终一致性的方法,核心思想是:将需要分布式处理的事务,通过消息队列或者日志的方式,异步的进行执行,再通过业务规则进行失败重试。例如在设计电商系统时,下单后,这条消息便可以发送给消息中间件,并进行持久化操作,随后再通知支付系统、物流系统等进行后续的操作。如果执行过程中发生了异常,那么MQ会再发送取消执行的消息。
基于分布式消息的最终一致性方案采用消息传递机制,并使用异步通信的方式,避免了通信阻塞,从而增加系统的吞吐量。同时,这种方案还可以屏蔽不同系统的协议规范,使其可以直接交互。
|0x02 分布式锁与互斥
锁与互斥,对于业务系统而言,是很常见的。在分布式系统里,排他性的资源访问方式,叫作“分布式互斥”,而这种被互斥访问的共享资源就叫作“临界资源”。什么是“锁”?指同一个临街资源,同一时间只能被同一资源访问。为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是“分布式锁”。
在电商场景下,如何在高并发场景下,应对订单分配问题,就是基于“分布式锁与互斥”来做设计。
分布式锁大体上有三种实现方法,分别是:基于数据库、基于缓存系统以及基于Zookeeper。
分布式锁实现的最简单、直接的方法,是通过数据库来做,通过创建一张记录共享资源信息的表,将需要锁住的资源写进来,当多个请求同时访问数据库时,数据库会保证只有一个操作可以成功。这种方式虽然简单,但因为需要频繁读取数据库,且数据需要落到磁盘上,因此在高并发场景下,并不适用。
因此,我们把数据库换成缓存系统,是可以减少IO读写、提升并发量的,缓存系统可以通过Redis来实现,Redis自身可以实现扩展性、便捷性、可操作性的平衡,算是另一种形式的数据库应用了。
最后讲一下Zookeeper,基于树形结构来实现分布式锁,ZooKeeper可以解决数据库方案存在的一些问题,比如单点故障、不可重入、死锁等问题。但该方法实现较复杂,且需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。
论性能,缓存 > Zookeeper > 数据库,论可靠性,Zookeeper > 缓存 > 数据库。
其实在工程设计上,还存在一些技巧,来规避锁的场景,以应对“秒杀”等场景。例如我们可以在CDN节点上部署一个小程序,根据在线人数和库存数量,来预估一个概率值。例如有100万人在线,抢100台手机,那么概率大概是0.01%,我们增加一点,0.02%,那么CDN在收到秒杀请求时,先进行一次过滤,放过去0.02%的人,也就是200人,最后由这200人来访问数据库,也就 200 TPS,压力一下子可以降下来。
接下来讲一下分布式互斥,与锁不同的是,互斥是一个持续性的过程,比如分配服务器的资源,当一个资源释放后,可以立刻承接下一个任务,因此对访问压力不太敏感,但对于最大化合理利用资源敏感。但锁应对的是一些单点场景,比如秒杀、库存,因此设计的重心,主要在访问压力上。从这一点上讲,分布式互斥,其实用Zookeeper来实现锁是一种标准,因为更加可靠,但同时性能又可以接受。
分布式互斥大概也有三种实现思路,即集中式算法、分布式算法和令牌环算法。
集中式算法比较容易理解,即选择一个中央服务器,也可以称之为协调者程序,每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
分布式算法要复杂一些,当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。这种方式,在分布式领域中,我们称之为分布式算法,或者使用组播和逻辑时钟的算法,Hadoop系统中的HDFS就是采用此方法来做的。客观的评价,分布式算法根据“先到先得”以及“全票通过”的机制,可以使程序公平顺序的访问资源,理解简单、实现成本低,但如果系统规模扩大,消息数量会呈指数级增加,导致“消息爆炸”。
令牌环算法是华为原创,适用在通信这种“令牌环方式”的分布式系统,实现方式为:所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。
|0xFF 分布式选举
对于分布式事务、分布式锁与互斥,不论是设计一套业务系统,还是应用到大数据集群中,都会用到,这里再讲一个集群管理中用到的概念:“分布式选举”。简单说,大数据集群一般是由多个服务器组成,每个服务器都是一个节点,那么“一个集群、多个节点”,应该如何协同和管理呢,这里就用到了分布式选举的方法。我们常用的ZooKeeper,简介为“分布式的,开放源码的分布式应用程序协调服务”,其实就要用到相关的知识。
分布式选举,目的是选出“主”节点,来管理整个或者是局部的机器,以保障集群的有序运行和节点间数据的一致性。分布式选举也是论文扎堆的地方,这里我们就简单讲解一下常见的各种算法,包括:Bully算法、Raft算法、Paxos算法、ZAB算法。
Bully 算法是一种霸道的集群选主算法,为什么说是霸道呢?因为它的选举原则是“长者”为大,即在所有活着的节点中,选取 ID 最大的节点作为主节点。在 Bully 算法中,节点的角色有两种:普通节点和主节点。初始化时,所有节点都是平等的,都是普通节点,并且都有成为主的权利。但是,当选主成功后,有且仅有一个节点成为主节点,其他所有节点都是普通节点。当且仅当主节点故障或与其他节点失去联系后,才会重新选主。
Paxos算法是分布式共识算法的“王者”,当前最常用的一批共识算法都是基于它改进的,如接下来提到的Raft 算法、ZAB选举算法等,是Lamport大神于1990年提出的一种基于消息传递的一致性算法。Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。Paxos算法本身没有提出实现细节,衍生算法有自己的实现方法。
Raft 算法是典型的多数派投票选举算法,其选举机制与我们日常生活中的民主投票机制类似,核心思想是“少数服从多数”。也就是说,Raft 算法中,获得投票最多的节点成为主。
ZAB选举算法是为 ZooKeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制,ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主,节点 ID 和数据 ID 越大,表示数据越新,优先成为主。相比较于 Raft 算法,ZAB 算法尽可能保证数据的最新性。所以,ZAB 算法可以说是对 Raft 算法的改进。