分布式算法
01 分布式基本理论
CAP理论
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值,即写操作之后的读操作,必须返回该值。(分为弱一致性、强一致性和最终一致性)
- 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
共识算法
对于一个分布式系统来说,保障集群中所有节点的数据完全相同(即一致性)是很重要的,随着多节点的引入,这影响的是整个分布式系统对外服务的表象一致性。也就是说,一个分布式系统想要做到完全的一致性,需要对外表现为顺序一致性,即各个节点上的操作顺序都一致。
而在现实运行情况下,节点可能故障,可能增加,甚至可能被篡改,这就给分布式一致性带来了挑战。在这种情况的干扰下,分布式系统需要通过某些机制,来就一些事情达成一致的看法,也就是共识。
但要注意的是,共识算法并不能一次性解决所有分布式的不一致问题。不同的算法能解决不同异常情况下的问题,所以共识算法也有分类:
崩溃容错算法(Crash Fault Tolerance):典型代表是Paxos、Raft、ZAB等。
拜占庭容错算法(Byzantine Fault Tolerance):FBFT为代表的确定性算法,PoW为代表的的概率算法等。
拜占庭将军问题:Lamport曾经提出过在分布式网络下的一种节点作恶场景。描述的是拜占庭需要通过信使来在守卫边境的多个将军间传递消息,以达成一致的决定。如果此时出现叛徒,故意发送不同的消息来干扰共识的达成,应该如何让忠诚的将军们保持行动一致。映射到分布式系统中就是多个节点就消息达成共识时如果出现错误节点传递了错误的消息该怎么办。
对于拜占庭容错,往往都需要通过其他方面的激励或惩罚,来让“诚实”表达的节点利益最大化,本文描述的Paxos算法不解决拜占庭的问题,只解决崩溃容错算法条件下达成分布式共识的问题。
02 Paxos算法
背景
有一种说法,说所有共识算法都是Paxos。这种说法的来源,一方面是由于Paxos的第一次提出非常的早,另一方面则是因为,Paxos解决的其实是在分布式环境下,所有服务达成一次某个值的共识的过程,而这一过程,可以说每种共识算法都绕不开。
最早在1990年,Paxos的作者Leslie Lamport就提交了关于Paxos的论文《The Part-Time Parliament》,但直到2001年Lamport第三次发表简化版的相关论文,且2006年Google使用Paxos的理念实现了分布式系统,该算法才被大众所理解和追捧。
1990年的论文中,Lamport描述了一个名为Paxos的希腊城邦(算法得名于此),这个城邦是按照民主的议会制度来进行选举的,所有的居民进行提议和投票来选出决议。但是居民们不想花时间一直在选举上,大家都不定时地来提议、了解提议、投票、看进展等等,而Paxos算法的目标就是通过少数服从多数的方式来达成最终的一致意见。但是审稿人觉得这个背景太复杂了,要求删去,Lamport不乐意就没继续投稿了(真大师)。八年后的1998年,Lamport再次整理算法进行投稿,但是大家还是不太理解,因此也没有引出多少水花。又过了三年,2001年,Lamport再次进行了简化,发表了《Paxos Made Simple》,去掉了希腊城邦的背景,里面甚至没有一个公式。但依然直到2006年被Google实现后才开始吸引大家的眼球,而作者Lamport也才获得了2013年图灵奖。
算法过程
少数服从多数
首先要认识到,这是一个分布式系统下的共识算法,要解决的问题,简化一点,就是一堆机器,每一台都可能会收到客户端的一条消息,那需要将自己收到的消息,告诉其他的机器,让所有分布式系统中的机器,达到最终的一致,这就是达到共识。
Paxos采取了一个我们非常熟悉的达成共识的方法:少数服从多数。只要有超过一半的机器认可某一个消息,那么最终就所有机器都接受这条消息并将它作为本次的结论。而竞选失败的少数派消息,就会被拒绝,并由第一个从客户端处接收到该消息的机器,向客户端发送失败结果,由客户端进行重试,去尝试在下一轮竞选中胜出。
少数服从多数,说来简单,如果是一群人的话,大家碰个头一起举手表决就好了。但是放到一个分布式系统中就变复杂了。机器之间怎么传递提议,怎么表决,怎么统计多数,网络传输需要时间,在表决过程中,其他机器收到了新的消息怎么办,都需要一整套机制来解决。
下面就来逐步讲解Paxos的过程,但在讲解过程之前,先说Paxos中最常见的两种角色:
Proposer:提案者。也就是在选举中提出提案的人,放到分布式系统里,就是接收到客户端写操作的人。一切行为都由Proposer提出提案开始,Paxos会将提案想要进行的操作,抽象为一个“value”,去在多台机器中传递,最后被所有机器接受。
Acceptor:批准者。Acceptor 从含义上来说就是除了当前Proposer以外的其他机器,他们之间完全平等和独立,Proposer需要争取超过半数(N/2+1)的 Acceptor 批准后,其提案才能通过,它倡导的“value”操作才能被所有机器所接受。
除了以上两种角色,实际上Paxos还会提到Learner,即学习者这个角色,该角色是在达成决议时,对结论的学习者,也即是从其他节点“学习”最终提案内容,比较简单。需要注意,这些角色只是在不同时间下,逻辑上的划分,实际上任何一台机器都可以充当这三个角色之一。
一个简单的提案
先描述最简单的情况,假设现在有四台机器,其中一台收到了来自客户端的写操作请求,需要同步给其他机器。
此时这台收到请求的机器,我们称它为Proposer,因为它将要开始将收到的请求,作为一个提案,提给其他的机器。这里为了方便,我们假设这个请求是要将一个地址设置为“深圳”,那么如下图所示:
此时,其他的Acceptor都闲着呢,也没其他人找,所以当它们收到Proposer的提案时,就直接投票了,说可以可以,我是空的,赞成提案(同意提议):
到这里,就还是一个简单的同步的故事,但需要注意的是,这里Proposer实际上是经历了两步的。
在这个简单的提案过程中,Proposer其实也经历了两个阶段:
Prepare阶段:Proposer告诉所有其他机器,我这里有一个提案(操作),想要你们投投票支持一下,想听听大家的意见。Acceptor看自己是NULL,也就是目前还没有接受过其他的提案,就说我肯定支持。
Accept阶段:Proposer收到其他机器的回复,说他们都是空的,也就是都可以支持接受Proposer的提案(操作),于是正式通知大家这个提案被集体通过了,可以生效了,操作就会被同步到所有机器正式生效。
03 ZAB
ZAB全称是Zookeeper Atomic Broadcast,也就是Zookeeper的原子广播,顾名思义是用于Zookeeper的。
ZAB理解起来很简单,在协议中有两种角色:
\1. Leader节点:有任期的领导节点,负责作为与客户端的连接点接受读写操作,然后将其广播到其他节点去。
\2. Follower节点:主要是跟随领导节点的广播消息进行同步,并关注领导节点的健康状态,好随时取而代之。
既然有Leader节点,就必然有Leader的选举过程,ZAB的选举,会先看各个节点所记录的消息的时间戳(数据ID),时间戳(数据ID)越大,节点上的数据越新,就会优先被投票,如果数据ID比较不出来,就再看事先定义的节点的优先级(节点ID)。当大家根据上述优先级投票,超过半数去支持一个节点时,该节点就成为Leader节点了。
通过心跳算法可以共同检查Leader节点的健康度,如果出现问题(比如机器下线、网络分区、延迟过高等),就会考虑重新选举。
可以看出,这种选举方式相对Paxos是比较方便高效的,而且选出Leader节点后,就可以直接通过Leader节点接受消息进行广播,而不需要进行两阶段提交。
其实ZAB就很像选出了Leader的Multi-Paxos,两者的差异主要在选Leader的流程上。
04 Raft
Raft的应用比Paxos要多,有人认为Raft是Multi-Paxos的改进,因为Raft的作者也曾研究过Paxos。既然Paxos是前辈,为什么应用的反而要少呢?这是因为Basic-Paxos相对比较耗时,而Multi-Paxos,作者并没有给出具体的实现细节,这虽然给了开发者发挥的空间,但同样可能会在实现的过程中由于开发者不同的实现方式带来不同的问题,对于一个分布式共识算法,谁也不知道潜在的问题会不会就影响到一致性了。而Raft算法给出了大量实现细节,简单说就是,实现起来更不容易出错。
Raft协议同样是需要选举出Leader的,从这里也能看到,共识算法大都会走向选举出一个Leader的方向,来提升效率和稳定性。不同之处可能只在于选举的方式,以及消息同步的方式。
Raft的选举,会在上一任Leader失去联系时发起,每个Follower便有机会成为Candidate,参与选举。之所以说有机会,是因为每个Follower都会先等一会,看是否有其他候选人过来拉票,避免人人都跑去凑热闹参与选举浪费通信,这个等待的时间是在一个范围内随机的。
候选者参与选举时会产生一个term概念,每个候选者会先投自己一票,然后带着自己的term和自己的日志信息(代表着数据的新旧)去拉票,其他的Follower先看候选者的term是否大于等于当前自己的term,再看其日志信息是否比自己新,如果都满足就会投票。候选者收到超过半数的投票的话,就会成为新的Leader了。
在这个过程中投票的Follower也会更新自己的term为自己投票的候选者的term,这样就可以拒绝低于它的term的候选者了。而候选者如果被拒绝,也会回去更新自己的term以获得支持。
选出Leader后,Leader会把自己的日志发给大家做同步,以保持大家和自己的日志是一样的,然后就进行后续的接收客户端请求的环节。
可以看到Raft和Multi-Paxos也都要选举出一个Leader节点来,不同之处在于,Raft选举的Leader节点上的日志信息是最新最全的,这一方面可以不丢失日志信息的顺序,另一方面也可以让选举过程简化(日志信息的顺序总是好比较的),而Multi-Paxos选Leader的过程偏随机,就是看谁先拉拢更多节点的支持并快速落定,这一方面会使其日志不连续,另一方面也会使得其实现变得复杂和相对不可控。
但实际上不连续也不完全是缺点,它也可以提高写入的并发性能,所以虽然Raft实现相对更简单,但微信的PaxosStore还是选择了Paxos,甚至它都没有选择Multi-Paxos,而是Basic-Paxos,就是为了进一步避免单点依赖和切换Leader时的拒绝服务,来提高可用性。
可以看到,共识算法基本都需要解决两个基本问题:
如何提出一个需要达成共识的提案(选举Leader、随机投票…)
了Paxos,甚至它都没有选择Multi-Paxos,而是Basic-Paxos,就是为了进一步避免单点依赖和切换Leader时的拒绝服务,来提高可用性。