深入剖析分布式一致性共识算法

一、共识算法 -- 拜占庭问题

两忠一叛问题:

 

如上图所示,将军A、B、C约定同时进攻或者撤退,假如将军C叛变了,被中间人截取消息并发送进攻给A、撤退给B,当所有将军消息都收到后结果如下:
A:2票进攻1票撤退;
B:2票撤退1票进攻;
导致最终A独自去攻打敌军,B撤退,最终会任务失败。

 

口信消息型拜占庭问题之解

如上图所示,经过2轮协商可以解决上述的两忠一叛问题;第一轮协商由leader发起,向其余3个将军发送进攻的指令消息,如果未收到消息则默认撤退指令;第二轮协商为3个将军之间的互通消息,假如将军C叛变为干扰信息向将军A、B发送撤退消息,则最终结果:
A:2进攻1撤退;
B:2进攻1撤退;
最终会执行进攻指令,这样解决了两忠一叛问题。

如果叛将人数为m,将军总人数不能少于3m + 1 。叛将数m决定递归循环的次数(将军们要进行多少轮作战信息协商),即m+1轮,n位将军,最多能容忍(n - 1) / 3位叛将。

  

二、分布式一致性算法前奏之Quorum NWR算法

Quorum选举算法
在N个副本中,一次更新成功的如果有W个,那么在读取数据时是要从大于N-W个副本中读取,这样就能至少读到一个更新的数据了。
如:维护了10个副本,一次成功更新了三个,那么至少需要读取八个副本的数据,可以保证读到最新的数据。

 

WARO算法(Write All Read one)
只有当所有的副本都更新成功之后,这次写操作才算成功,否则视为失败。WARO 优先保证读服务,因为所有的副本更新成功,才能视为更新成功,从而保证了所有的副本一致,这样的话,只需要读任何一个副本上的数据即可。写服务的可用性较低,因为只要有一个副本更新失败,此次写操作就视为失败了。假设有 N 个副本,N-1 个都宕机了,剩下的那个副本仍能提供读服务;但是只要有一个副本宕机了,写服务就不会成功。WARO 牺牲了更新服务的可用性,最大程度地增强了读服务的可用性,而 Quorum 就是在更新服务和读服务之间进行的一个折衷。

 

Quorum的应用
Quorum机制无法保证强一致性,也就是无法实现任何时刻任何用户或节点都可以读到最近一次成功提交的副本数据。
Quorum机制的使用需要配合一个获取最新成功提交的版本号的metadata服务,这样可以确定最新已经成功提交的版本号,然后从已经读到的数据中就可以确认最新写入的数据。
Quorum是分布式系统中常用的一种机制,用来保证数据冗余和最终一致性的投票算法,在Paxos、Raft和ZooKeeper的Zab等算法中,都可以看到Quorum机制的应用。

 

如上图所示,DATA-1有2个副本,DATA-2有3个副本,DATA-3 有1个副本,副本的数量即表示N;对DATA-2执行写操作时,完成了2个副本的更新(节点B、C),才完成写操作,即W此时为2;对DATA-2执行读操作,客户端读取DATA-2的数据时,需要读取2个副本中的数据,然后返回最新的那份数据,即读副本R为2。

无论客户端如何执行读操作,即使访问写操作未强制更新副本节点B,执行读操作时,因为要读2份数据副本,所以除了节点B上的DATA-2,还会读取节点C上的DATA-2,而节点C的DATA-2数据副本是强制更新成功的,这个时候返回给客户端肯定是最新的那份数据。

对于Quorum:

当W+R>N的时候,对于客户端来讲,整个系统能保证强一致性,一定能返回更新后的那份数据。
当W+R<=N的时候,对于客户端来讲,整个系统只能保证最终一致性,可能会返回旧数据。

 

三、paxos算法

paxos算法由Proposer、Acceptor和Learner组成:

提议者(Proposer):提议一个值,用于投票表决。在绝大多数场景中,集群中收到客户端请求的节点,才是提议者。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑,就可以像使用数据库一样访问后端的数据。
接受者(Acceptor):对每个提议的值进行投票,并存储接受的值,比如A、B、C三个节点。 一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
学习者(Learner):被告知投票的结果,接受达成共识的值,存储保存,不参与投票的过程。一般来说,学习者是数据备份节点,比如“Master-Slave”模型中的Slave,被动地接受数据,容灾备份。
 

 

Basic Paxos

在Basic Paxos中,使用提案代表一个提议,在提案中除了提案编号,还包含了提议值。使用[n, v]表示一个提案,其中n为提案编号,v为提议值。

 

整个共识协商是分2个阶段进行的(二阶段提交)。假设客户端1的提案编号为1,客户端2的提案编号为5,节点A、B先收到来自客户端1的准备请求,节点C先收到来自客户端2的准备请求。
prepare阶段

客户端1、2作为提议者,分别向所有接受者发送包含提案编号的准备请求:
对于客户端1的提案,由于之前没有通过任何提案,所以节点A、B以后将不再响应提案编号小于等于1的prepare请求,即不会通过编号小于1的提案,节点C以后不再响应提案编号小于等于5的准备请求,即不会通过编号小于5的提案;
 
对于客户端2的提案,当节点A、B收到提案编号为5的准备请求的时候,因为提案编号5大于它们之前响应的准备请求的提案编号1,而且两个节点都没有通过任何提案,所以它将返回一个 “尚无提案”的响应,所以节点A、B将不再响应提案编号小于等于5的准备请求,即不会通过编号小于5的提案,当节点C收到提案编号为1的准备请求的时候,由于提案编号1小于它之前响应的准备请求的提案编号5,所以丢弃该准备请求不做响应;
 
Accept阶段
首先客户端1、2在收到大多数节点的准备响应之后,会分别发送接受请求:
当客户端1收到大多数的接受者(节点A、B)的准备响应后,根据响应中提案编号最大的提案的值设置接受请求中的值。因为节点A、B的准备响应中都为空,所以就把自己的提议值3作为提案的值,发送接受请求[1, 3];
当客户端2收到大多数的接受者的准备响应后(节点A、B和节点C),根据响应中提案编号最大的提案的值,来设置接受请求中的值。因为该值在来自节点A、B、C的准备响应中都为空,所以就把自己的提议值7作为提案的值,发送接受请求[5, 7];
 
当三个节点收到2个客户端的接受请求时:
当节点A、B、C收到接受请求[1, 3]的时候,由于提案的提案编号1小于三个节点承诺能通过的提案的最小提案编号5,所以提案[1, 3]将被拒绝;
当节点A、B、C收到接受请求[5, 7]的时候,由于提案的提案编号5不小于三个节点承诺能通过的提案的最小提案编号5,所以就通过提案[5, 7],也就是接受了值7,三个节点就X值为7达成了共识;

 

Multi-Paxos

Basic Paxos只能就单个值(Value)达成共识,Multi-Paxos是通过多个Basic Paxos实例实现一系列值的共识的算法。
Multi-Paxos通过引入Leader节点,将Leader节点作为唯一提议者,避免了多个提议者同时提交提案的情况,解决了提案冲突的问题,Leader节点是通过执行Basic Paxos算法,进行投票选举产生的。
优化Basic Paxos执行可以采用“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制。在Leader节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案,Leader可以独立指定提案中的值。Leader节点在提交命令时,可以省掉准备阶段,直接进入到接受阶段:
和重复执行Basic Paxos相比,Multi-Paxos引入领导者节点之后,因为只有领导者节点一个提议者,所以就不存在提案冲突。另外,当主节点处于稳定状态时,就省掉准备阶段,直接进入接受阶段,所以在很大程度上减少了往返的消息数,提升了性能,降低了延迟。
 

四、一致hash算法

使用哈希算法的问题?
通过哈希算法,每个key都可以寻址到对应的服务器,比如,查询key是key-01,计算公式为hash(key-01) %3 ,经过计算寻址到了编号为1的服务器节点A;如果服务器数量发生变化,基于新的服务器数量来执行哈希算法的时候,就会出现路由寻址失败的情况,无法找到之前寻址到的那个服务器节点;假如增加了一个节点,节点的数量从3变化为4,那么之前的hash(key-01) %3 = 1,就变成了hash(key-01) %4 =X,因为取模运算发生了变化,所以这个X大概率不是1,这时再查询就会找不到数据了,因为key-01对应的数据并非存储在节点X。

通过上图可以看出,当扩容增加一个节点时会出现hash寻址失败的情况;同理,如果需要下线1个服务器节点,也会存在类似的可能查询不到数据的问题。

 

一致哈希实现哈希寻址
一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对2^32进行取模运算。在一致哈希中,可以通过执行哈希算法将节点映射到哈希环上,如选择节点的主机名作为参数执行hash(),那么每个节点就能确定其在哈希环上的位置了。
当需要对指定key的值进行读写的时候,可以通过下面2步进行寻址:
首先,将key作为参数执行hash()计算哈希值,并确定此key在环上的位置;
然后,从这个位置沿着哈希环顺时针“行走”,遇到的第一节点就是key对应的节点。
 
根据一致哈希算法,key-01将寻址到节点A,key-02将寻址到节点B,key-03将寻址到节点C。假设现在节点C故障了,key-01和key-02不会受到影响,只有key-03的寻址被重定位到A。在一致哈希算法中,如果某个节点宕机不可用了,那么受影响的数据仅仅是会寻址到此节点和前一节点之间的数据。比如当节点C宕机了,受影响的数据是会寻址到节点B和节点C之间的数据(例如key-03),寻址到其他哈希环空间的数据不会受到影响。同理,如果集群扩容一个节点,在一致哈希算法中,如果增加一个节点,受影响的数据仅仅是会寻址到新节点和前一节点之间的数据,其它数据也不会受到影响。
在哈希寻址中常出现这样的问题:客户端访问请求集中在少数的节点上,出现了有些机器高负载,有些机器低负载的情况,在一致哈希中可以使用虚拟节点让数据访问分布的比较均匀。
使用虚拟节点解决冷热不均的问题:
对每一个服务器节点计算多个哈希值,在每个计算结果位置上,都放置一个虚拟节点,并将虚拟节点映射到实际节点。
比如,可以在主机名的后面增加编号,分别计算 “Node-A-01”“Node-A-02”“Node-B-01”“Node-B-02”“Node-C-01”“Node-C-02”的哈希值,于是形成6个虚拟节点;增加了节点后,节点在哈希环上的分布就相对均匀了。如果有访问请求寻址到“Node-A-01”这个虚拟节点,将被重定位到节点A。
因此,当节点数越多的时候,使用哈希算法时,需要迁移的数据就越多,使用一致哈希时,需要迁移的数据就越少。所以相比hash算法,一致哈希算法具有较好的容错性和可扩展性。

  

五、zab协议

Multi-Paxos解决的是一系列值如何达成共识的问题,不关心最终达成共识的值是什么,不关心各值的顺序,即它不关心操作的顺序性。
ZAB协议基于主备模式的原子广播,最终实现了操作的顺序性。Master-Slave的主备模型,主节点采用二阶段提交,向备份节点同步数据,如果主节点发生故障,数据最完备的节点将当选主节点;原子广播协议,广播一组消息,消息的顺序是固定的。
ZAB支持3种成员身份(领导者、跟随者、观察者)。
领导者(Leader): 作为主节点,在同一时间集群只会有一个领导者,所有的写请求都必须在领导者节点上执行。
跟随者(Follower):作为备份节点, 集群可以有多个跟随者,它们会响应领导者的心跳,并参与领导者选举和提案提交的投票,跟随者可以直接处理并响应来自客户端的读请求,但对于写请求,跟随者需要将它转发给领导者处理。
观察者(Observer):作为备份节点,类似跟随者,但是没有投票权,观察者不参与领导者选举和提案提交的投票。
ZAB在Multi-Paxos的基础上做了优化,为了实现分区容错能力,将数据复制到大多数节点后,领导者就会进入提交执行阶段,通知备份节点执行提交操作。
ZAB定义了4种成员状态:
LOOKING:选举状态,该状态下的节点认为当前集群中没有领导者,会发起领导者选举。
FOLLOWING :跟随者状态,意味着当前节点是跟随者。
LEADING :领导者状态,意味着当前节点是领导者。
OBSERVING: 观察者状态,意味着当前节点是观察者。
 
如上图所示,首先,当跟随者检测到连接领导者节点的读操作等待超时了,跟随者会变更节点状态,将自己的节点状态变更成LOOKING,然后发起领导者选举;接着,每个节点会创建一张选票,这张选票是投给自己的,然后各自将选票发送给集群中所有节点,一般而言,节点会先接收到自己发送给自己的选票(因为不需要跨节点通讯,传输更快);集群的各节点收到选票后,为了选举出数据最完整的节点,对于每一张接收到选票,节点都需要进行领导者PK,也就将选票提议的领导者和自己提议的领导者进行比较,找出更适合作为领导者的节点,约定的规则如下:
优先检查任期编号(Epoch),任期编号大的节点作为领导者;
如果任期编号相同,比较事务标识符的最大值,值大的节点作为领导者;
如果事务标识符的最大值相同,比较集群ID,集群ID大的节点作为领导者。
如果选票提议的领导者,比自己提议的领导者,更适合作为领导者,那么节点将调整选票内容,推荐选票提议的领导者作为领导者。
 
zab故障恢复是由成员发现和数据同步两个阶段完成的,成员发现是通过跟随者和领导者交互来完成的,目标是确保大多数节点对领导者的领导关系没有异议,也就是确立领导者的领导地位:
成员发现,是为了建立跟随者和领导者之间的领导者关系,并通过任期编号来确认这个领导者是否为最合适的领导者。当跟随者和领导者设置ZAB状态为数据同步,它们也就是进入了数据同步阶段,数据同步也是通过跟随者和领导者交互来完成的,目标是确保跟随者节点上的数据与领导者节点上数据是一致的。
数据同步,是通过以领导者的数据为准的方式,来实现各节点数据副本的一致,需要你注意的是,基于“大多数”的提交原则和选举原则,能确保被复制到大多数节点并提交的提案,就不再改变。
对于zab处理写请求:
由于写请求只能在领导者节点上处理,所以ZooKeeper集群写性能约等于单机。而读请求是可以在所有的节点上处理的,所以读性能是能水平扩展的。可以通过分集群的方式来突破写性能的限制,并通过增加更多节点,来扩展集群的读性能。
首先,ZAB实现了主备模式,也就是所有的数据都以主节点为准;
其次,ZAB实现了FIFO队列,保证消息处理的顺序性。
另外,ZAB还实现了当主节点崩溃后,只有日志最完备的节点才能当选主节点,因为日志最完备的节点包含了所有已经提交的日志,所以这样就能保证提交的日志不会再改变。
 

六、raft算法

Raft算法是分布式系统开发首选的共识算法 ,从本质上说,Raft算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致。
Raft算法支持领导者(Leader)、跟随者(Follower)和候选人(Candidate)3种状态:
跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
候选人:候选人将向其他节点发送请求投票(RequestVote)RPC消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
领导者:主要工作内容就是3部分,处理写请求、管理日志复制和不断地发送心跳信息。

 选举领导者的过程:

 

首先,在初始状态下,集群中所有的节点都是跟随者的状态,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。集群中没有领导者,而节点A的等待超时时间最小(150ms),它会最先因为没有等到领导者的心跳信息而发生超时。节点A就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票RPC消息,请求它们选举自己为领导者。当候选人节点A在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。

 
节点A当选领导者后,将周期性地发送心跳消息,通知其他服务器以阻止跟随者发起新的选举。
Raft算法中约定的选举规则:
1. 领导者周期性地向所有跟随者发送心跳消息,阻止跟随者发起新的选举。
2. 如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。
3. 在一次选举中,赢得大多数选票的候选人,将晋升为领导者。
4. 在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其它节点发起一轮新的选举。
5. 在一次选举中,每一个服务器节点会按照“先来先服务”的原则进行投票。
6. 当任期编号相同时,日志完整性高的跟随者(最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点B、C的任期编号都是3,节点B的最后一条日志项对应的任期编号为3,而节点C为2,那么当节点C请求节点B投票给自己时,节点B将拒绝投票。
 
Raft算法日志复制流程:
Raft算法中,副本数据是以日志的形式存在的,领导者接收到来自客户端写请求后,处理写请求的过程就是一个复制和应用日志项到状态机的过程。
首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC消息,将日志项复制到集群其他节点上。
接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。
1. 接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
2. 领导者通过日志复制RPC,将新的日志项复制到其他的服务器。
3. 当领导者将日志项成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。
4. 领导者将执行的结果返回给客户端。
5. 当跟随者接收到心跳信息,或者新的日志复制RPC消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机中。
 

七、总结 

ZAB协议在Multi-Paxos达成共识的基础上实现了操作的顺序性。

Raft算法和Multi-Paxos不同之处:
1. 在Raft中,不是所有节点都能当选领导者,只有日志最完整的节点,才能当选领导者;
2. 日志必须是连续的;
 
Raft算法与ZAB协议的异同点:
1. Raft采用的是“先到先得”的自定义投票算法。Raft的领导者选举,需要通讯的消息数更少,选举也更快。
2. 对于日志复制,Raft和ZAB相同,都是以领导者的日志为准来实现日志一致,而且日志必须是连续的,也必须按照顺序提交。
3. 对于读操作和一致性,ZAB的设计目标是操作的顺序性,在ZooKeeper中默认实现的是最终一致性,读操作可以在任何节点上执行;而Raft的设计目标是强一致性(也就是线性一致性),所以Raft更灵活,Raft系统既可以提供强一致性,也可以提供最终一致性。
4. 对于写操作,Raft和ZAB相同,写操作都必须在领导者节点上处理。
5. 成员变更,ZAB不支持成员变更,当需要节点变更(比如扩容)时,必须重启整个ZooKeeper集群。Raft支持成员变更,不需要重启机器,集群是一直运行的,服务也不会中断。
相比ZAB,Raft的设计更为简洁,Raft没有引入类似ZAB的成员发现和数据同步阶段,而是当节点发起选举时,递增任期编号,在选举结束后,广播心跳,直接建立领导者关系,然后向各节点同步日志,来实现数据副本的一致性。

posted @ 2021-02-17 17:18  HarvardFly  阅读(969)  评论(0编辑  收藏  举报