区块链快速入门(三)——CFT(非拜占庭容错)共识算法-创新互联
CFT(Crash Fault Tolerance),即故障容错,是非拜占庭问题的容错技术。
Paxos 问题是指分布式的系统中存在故障(crash fault),但不存在恶意(corrupt)节点的场景(即可能消息丢失或重复,但无错误消息)下的共识达成问题,是分布式共识领域最为常见的问题。最早由Leslie Lamport用 Paxon 岛的故事模型来进行描述而得以命名。解决Paxos问题的算法主要有Paxos系列算法和Raft算法,Paxos算法和Raft算法都属于强一致性算法。
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生任何异常,都不会破坏整个系统的一致性。
Paxos算法用于解决分布式系统的一致性问题。
1990年,Leslie Lamport 在论文《The Part-time Parliament》中提出了Paxos共识算法,在工程角度实现了一种大化保障分布式系统一致性(存在极小的概率无法实现一致)的机制。Leslie Lamport作为分布式系统领域的早期研究者,因为相关成果获得了2013年度图灵奖。
Leslie Lamport在论文中将Paxos问题表述如下:
希腊岛屿Paxon上的执法者在议会大厅中表决通过法律(一次Paxos过程),并通过服务员(proposer)传递纸条的方式交流信息,每个执法者会将通过的法律记录在自己的账目上。问题在于执法者和服务员都不可靠,他们随时会因为各种事情离开议会大厅(服务器拓机或网络断开),并随时可能有新的执法者进入议会大厅进行法律表决(新加入机器),使用何种方式能够使得表决过程正常进行,且通过的法律不发生矛盾(对一个值达成一致)。
Paxos过程中不存在拜占庭将军问题(消息不会被篡改)和两将军问题(信道可靠)。
Paxos是首个得到证明并被广泛应用的共识算法,其原理类似两阶段提交算法,进行了泛化和扩展,通过消息传递来逐步消除系统中的不确定状态。
作为后续很多共识算法(如 Raft、ZAB等)的基础,Paxos算法基本思想并不复杂,但最初论文中描述比较难懂,甚至连发表也几经波折。2001年,Leslie Lamport专门发表论文《Paxos Made Simple》进行重新解释,其对Paxos算法的描述如下:
Phase1
(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro-posal (if any) that it has accepted.
Phase 2
(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v , where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.
Paxos算法目前在Google的Chubby、MegaStore、Spanner等系统中得到了应用,Hadoop中的ZooKeeper也使用了Paxos算法,但使用的算法是原始Paxos算法的改进算法。通常以Paxos算法为基础,在实现过程中处理实际应用场景中的具体细节,可以得到一个Paxos改进算法。
Paxos算法的基本思路类似两阶段提交:多个提案者先要争取到提案的权利(得到大多数接受者的支持);成功的提案者发送提案给所有人进行确认,得到大部分人确认的提案成为批准的结案。
Paxos协议有三种角色:Proposer(提议者),Acceptor(决策者),Learner(决策学习者)。
Paxos 是一个两阶段的通信协议,Paxos算法的基本流程如下:
第一阶段Prepare:
A、Proposer生成一个全局唯一的提案编号N,然后向所有Acceptor发送编号为N的Prepare请求。
B、如果一个Acceptor收到一个编号为N的Prepare请求,且N大于本Acceptor已经响应过的所有Prepare请求的编号,那么本Acceptor就会将其已经接受过的编号大的提案(如果有的话)作为响应反馈给Proposer,同时本Acceptor承诺不再接受任何编号小于N的提案。
第二阶段 Accept
A、如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么Proposer就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。V就是收到的响应中编号大的提案的value,如果响应中不包含任何提案,那么V由Proposer自己决定。
B、如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,Acceptor就接受该提案。
Paxos 并不保证系统总处在一致的状态。但由于每次达成共识至少有超过一半的节点参与,最终整个系统都会获知共识结果。如果提案者在提案过程中出现故障,可以通过超时机制来缓解。
Paxos 能保证在超过一半的节点正常工作时,系统总能以较大概率达成共识。
对于提案ID的选择,《Paxos made simple》中提到的是让所有的Proposer都从不相交的数据集合中进行选择。
Google的Chubby论文中给出提案ID的生成算法如下:假设有n个Proposer,每个编号为
ir(0<=ir
,Proposal编号的任何值s都应该大于它已知的大值,并且满足:
s %n = ir => s = m*n + ir
Proposer已知的大值来自两部分:Proposer自己对编号自增后的值和接收到Acceptor的拒绝后所得到的值。
以3个Proposer P1、P2、P3为例,开始m=0,编号分别为0,1,2。
1) P1提交的时候发现了P2已经提交,P2编号为1 >P1的0,因此P1重新计算编号:new P1 = 1*3+1 = 4
;
2) P3以编号2提交,发现小于P1的4,因此P3重新编号:new P3 = 1*3+2 = 5
。
如果两个提案者恰好依次提出更新的提案,则导致活锁,系统会永远无法达成共识(实际发生概率很小)。活锁没有产生阻塞,但是一直无法达成一致。
活锁有三种解决方案:
A、在被打回第一阶段再次发起PrepareRequest请求前加入随机等待时间。
B、设置一个超时时间,到达超时时间后,不再接收PrepareRequest请求。
C、在Proposer中选举出一个leader,通过leader统一发出PrepareRequest和AcceptRequest。
Paxos算法在执行过程中会产生很多的异常情况:Proposer宕机,Acceptor在接收Proposal后宕机,Proposer接收消息后宕机,Acceptor在Accept后宕机,Learner宕机,存储失败等等。
为保证Paxos算法的正确性,Proposer、Aceptor、Learner都需要实现持久存储,以做到Server恢复后仍能正确参与Paxos处理。
Proposer存储已提交的大proposal编号、决议编号(instance id)。
Acceptor存储已承诺(promise)的大编号、已接受(Accept)的大编号和Value、决议编号。
Learner存储已学习过的决议和编号。
Paxos算法只有两种情况下服务不可用:一是超过半数的Proposer异常,二是出现活锁。前者可以通过增加Proposer的个数来降低由于Proposer异常影响服务的概率,后者本身发生的概率就极低。
Paxos是分布式系统一致性协议的基础,其它的协议(raft、zab等)都是Paxos协议的改进版本。Paxos侧重理论,实现Paxos非常困难。
微信后台生产级Paxos类库PhxPaxos实现:
https://github.com/Tencent/paxosstore
https://github.com/tencent-wechat/phxpaxos
基于Paxos算法的改进算法的资料集合:
https://github.com/dgryski/awesome-consensus
三军问题的描述如下:
1) 1支红军在山谷里扎营,在周围的山坡上驻扎着3支蓝军;
2) 红军比任意1支蓝军都要强大;如果1支蓝军单独作战,红军胜;如果2支或以上蓝军同时进攻,蓝军胜;
3) 三支蓝军需要同步他们的进攻时间;但他们惟一的通信媒介是派通信兵步行进入山谷,在那里他们可能被俘虏,从而将信息丢失;或者为了避免被俘虏,可能在山谷停留很长时间;
4) 每支军队有1个参谋负责提议进攻时间;每支军队也有1个将军批准参谋提出的进攻时间;很明显,1个参谋提出的进攻时间需要获得至少2个将军的批准才有意义;
5) 问题:是否存在一个协议,能够使得蓝军同步他们的进攻时间?
三军问题符合Paxos问题场景,参谋和将军需要遵循一些基本的规则:
1) 参谋以两阶段提交(prepare/commit)的方式来发起提议,在Prepare阶段需要给出一个提议编号;
2) 在Prepare阶段产生冲突,将军以提议编号大小来裁决,提议编号大的参谋胜出;
3) 参谋在Prepare阶段如果收到将军返回的已接受进攻时间,在Commit阶段必须使用返回的进攻时间;
A、参谋1发起提议,派通信兵带信给3个将军,内容为(编号1);
B、3个将军收到参谋1的提议,由于之前还没有保存任何编号,因此把(编号1)保存下来,避免遗忘;同时让通信兵带信回去,内容为(ok);
C、参谋1收到至少2个将军的回复,再次派通信兵带信给3个将军,内容为(编号1,进攻时间1);
D、3个将军收到参谋1的时间,把(编号1,进攻时间1)保存下来,避免遗忘;同时让通信兵带信回去,内容为(Accepted);
E、参谋1收到至少2个将军的(Accepted)内容,确认进攻时间已经被大家接收;
F、参谋2发起提议,派通信兵带信给3个将军,内容为(编号2);
G、3个将军收到参谋2的提议,由于(编号2)比(编号1)大,因此把(编号2)保存下来,避免遗忘;又由于之前已经接受参谋1的提议,因此让通信兵带信回去,内容为(编号1,进攻时间1);
H、参谋2收到至少2个将军的回复,由于回复中带来了已接受的参谋1的提议内容,参谋2因此不再提出新的进攻时间,接受参谋1提出的时间;
1) 参谋1发起提议,派通信兵带信给3个将军,内容为(编号1);
2) 3个将军的情况如下
A、将军1和将军2收到参谋1的提议,将军1和将军2把(编号1)记录下来,如果有其他参谋提出更小的编号,将被拒绝;同时让通信兵带信回去,内容为(ok);
B、负责通知将军3的通信兵被抓,因此将军3没收到参谋1的提议;
3) 参谋2在同一时间也发起了提议,派通信兵带信给3个将军,内容为(编号2);
4) 3个将军的情况如下
A、将军2和将军3收到参谋2的提议,将军2和将军3把(编号2)记录下来,如果有其他参谋提出更小的编号,将被拒绝;同时让通信兵带信回去,内容为(ok);
B、负责通知将军1的通信兵被抓,因此将军1没收到参谋2的提议;
5) 参谋1收到至少2个将军的回复,再次派通信兵带信给有答复的2个将军,内容为(编号1,进攻时间1);
6) 2个将军的情况如下
A、将军1收到了(编号1,进攻时间1),和自己保存的编号相同,因此把(编号1,进攻时间1)保存下来;同时让通信兵带信回去,内容为(Accepted);
B、将军2收到了(编号1,进攻时间1),由于(编号1)小于已经保存的(编号2),因此让通信兵带信回去,内容为(Rejected,编号2);
7) 参谋2收到至少2个将军的回复,再次派通信兵带信给有答复的2个将军,内容为(编号2,进攻时间2);
8) 将军2和将军3收到了(编号2,进攻时间2),和自己保存的编号相同,因此把(编号2,进攻时间2)保存下来,同时让通信兵带信回去,内容为(Accepted);
9) 参谋2收到至少2个将军的(Accepted)内容,确认进攻时间已经被多数派接受;
10) 参谋1只收到了1个将军的(Accepted)内容,同时收到一个(Rejected,编号2);参谋1重新发起提议,派通信兵带信给3个将军,内容为(编号3);
11) 3个将军的情况如下
A、将军1收到参谋1的提议,由于(编号3)大于之前保存的(编号1),因此把(编号3)保存下来;由于将军1已经接受参谋1前一次的提议,因此让通信兵带信回去,内容为(编号1,进攻时间1);
B、将军2收到参谋1的提议,由于(编号3)大于之前保存的(编号2),因此把(编号3)保存下来;由于将军2已经接受参谋2的提议,因此让通信兵带信回去,内容为(编号2,进攻时间2);
C、 负责通知将军3的通信兵被抓,因此将军3没收到参谋1的提议;
12) 参谋1收到了至少2个将军的回复,比较两个回复的编号大小,选择大编号对应的进攻时间作为最新的提议;参谋1再次派通信兵带信给有答复的2个将军,内容为(编号3,进攻时间2);
13) 将军1和将军2收到了(编号3,进攻时间2),和自己保存的编号相同,因此保存(编号3,进攻时间2),同时让通信兵带信回去,内容为(Accepted);
14) 参谋1收到了至少2个将军的(accepted)内容,确认进攻时间已经被多数派接受;
Paxos是对一个值达成一致,Multi-Paxos是连续多个Paxos instance来对多个值达成一致。Multi-Paxos协议中有一个Leader。Leader是系统中唯一的Proposal,在lease租约周期内所有提案都有相同的ProposalId,可以跳过prepare阶段,议案只有accept过程,一个ProposalId可以对应多个Value,所以称为Multi-Paxos。
Multi-Paxos协议是经典的Paxos协议的简化版本,将原来2-Phase过程简化为了1-Phase,从而加快了提交速度。Multi-Paxos要求在各个Proposer中有唯一的Leader,并由Leader唯一地提交value给各Acceptor进行表决,在系统中仅有一个Leader进行value提交的情况下,Prepare的过程就可以被跳过,而Leader的选举则可以由Paxos Lease来完成。
在Paxos算法中,如果能够选举出一个leader,那么有助于提高投票的成功率。另外leader在多个决议的选举中有很重要的作用(用于得到决议的连续id)。因此,如何通过某种方法得到一个leader就是PaxosLease所说明的。
Master选举的过程如下:从众多的Node中选择一个作为Master,如果该Master 一直 alive则无需选举,如果master crash,则其他的node进行选举下一个master。选择正确性的保证是:任何时刻最多只能有一个master。
逻辑上Master更像一把无形的锁,任何一个节点拿到这个锁,都可成为master,所以本质上Master选举是个分布式锁的问题,但完全靠锁来解决选举问题也是有风险的:如果一个Node拿到锁,然后crash,会导致锁无法释放,即死锁。一种可行的方案是给锁加个时间(Lease),拿到锁的Master只能在Lease有效期内访问锁定的资源,在Lease timeout后,其他Node可以继续竞争锁,从根本上避免了死锁。
Master在拿到锁后,如果一直alive,可以向其他node”续租“锁,从而避免频繁的选举活动。
Paxos 算法的设计并没有考虑到一些优化机制,同时论文中也没有给出太多实现细节,因此后来出现了不少性能更优化的算法和实现,包括Fast Paxos、Multi-Paxos 等。
Raft算法由斯坦福大学的Diego Ongaro和John Ousterhout 2013年在论文《In Search of an Understandable Consensus Algorithm》中提出。Raft算法面向对多个决策达成一致的问题,是对Multi-Paxos的重新简化设计和实现,分解了领导者选举、日志复制和安全方面的考虑,并通过约束减少了不确定性的状态空间。
Raft算法将一致性问题分解为领导选举(leader election)、日志复制(log replication)、安全性(safety)三部分。
Raft算法包括领导者(Leader)、候选者(Candidate)和跟随者(Follower)三种角色,决策前通过选举一个全局的领导者来简化后续的决策过程。领导者决定日志(log)的提交,日志只能由领导者向跟随者单向复制。
Leader:集群中只有一个处于Leader状态的服务器,负责响应所有客户端的请求。
Follower:刚启动时所有节点为Follower状态,响应Leader的日志同步请求,响应Candidate的请求。
Candidate:Follower状态服务器准备发起新的Leader选举前需要转换到的状态,是Follower和Leader的中间状态。
所有节点初始状态都是Follower角色
超时时间内没有收到Leader的请求则转换为Candidate进行选举
Candidate收到大多数节点的选票则转换为Leader;发现Leader或者收到更高任期的请求则转换为Follower
Leader在收到更高任期的请求后转换为Follower
Term(任期):每个Leader都有自己的任期,任期到期就需要开始新一轮选举,在每个任期内,可以没有leader,但是不能出现大于两个的leader。
Raft算法将整个系统执行时间划分为若干个不同时间间隔长度的Term(任期)构成的序列,以递增的数字来作为Term的编号;每个Term由Election开始,在任期内若干处于Candidate状态的服务器竞争产生新的Leader,如果一个候选人赢得了选举,就会在该任期的剩余时间担任领导人;在某些情况下,选票会被瓜分,有可能没有选出领导人,那么将会开始另一个任期,并且立刻开始下一次选举。Raft算法保证在给定的一个任期最多只有一个领导人。
Raft算法典型的过程包括两个主要阶段:
(1)Leader选举
当整个系统启动时,所有服务器都处于Follower状态;如果系统中存在Leader,Leader会周期性的发送心跳(AppendEntries RPC)来告诉其它服务器自己是Leader;如果Follower经过一段时间没有收到任何心跳信息,则可以认为Leader不存在,需要进行Leader选举。
在选举前,Follower增加其Term编号并改变状态为Candidate状态,然后向集群内的其它服务器发出RequestVote RPC,Candidate状态持续到发生下面三个中的任意事件:
A、赢得选举:Candidate接受了大多数服务器的投票,成为Leader,然后向其它服务器发送心跳(AppendEntries RPC)告诉其它服务器。
B、其它服务器获得选举:Candidate在等待的过程中接收到自称为Leader的服务器发送来的心跳(AppendEntries RPC),如果RPC的Term编号大于等于Candidate自身的Term编号,则Candidate承认Leader,自身状态变成Follower;否则拒绝承认Leader,状态依然为Candidate。
C、一段时间过后,如果没有新的Leader产生,则Term递增,重新发起选举(因为有可能同时有多个Follower转为Candidate状态,导致分流,都没有获得多数票)。
(2)日志复制
Log复制主要作用是用于保证节点的一致性,所做的操作是为了保证一致性与高可用性;当Leader选举出来后便开始负责客户端的请求,所有请求都必须先经过Leader处理。Leader接受到客户端请求后,将其追加到Log的尾部,然后向集群内其它服务器发出AppendEntries RPC,引发其它服务器复制新请求的操作,当大多数服务器复制完后,Leader将请求应用到内部状态机,并将执行结果返回给客户端。
每个Log中的项目包含2个内容:操作命令本身和Term编号;还有一个全局的Log Index来指示Log项目在Log中的顺序编号。当大多数服务器在Log中存储了Log项目,则可认为Log项目是可以提交的,上图中的Log Index为7前的Log项目都可以提交。
(3)安全性
安全性是用来保证每个节点都执行相同序列的安全机制,如当某个Follower在当前Leader提交命令时不可用,稍后可能该Follower又会被选举为Leader,这时新Leader可能会用新的Log覆盖先前已提交的Log,这就是导致节点执行不同序列;安全性就是用于保证选举出来的Leader一定包含先前已经提交Log的机制。
为了达到安全性,Raft算法增加了两个约束条件:
A、要求只有其Log包含了所有已经提交的操作命令的那些服务器才有权被选为Leader。
B、对于新Leader来说,只有它自己已经提交过当前Term的操作命令才被认为是真正的提交。
Raft算法已经被多种语言实现,如Go语言、C++、Python等主流开发语言。
Raft算法原理动画演示:
http://thesecretlivesofdata.com/raft/
Raft共识算法资源如下:
https://raft.github.io/
Raft算法的Go语言实现:
https://github.com/goraft/raft
文章名称:区块链快速入门(三)——CFT(非拜占庭容错)共识算法-创新互联
本文来源:http://hbruida.cn/article/cejeoh.html