1 历史背景
讲Raft前,先提一下Paxos算法,Paxos算法是Leslie Lamport于1990年提出的基于消息传递的一致性算法。然而,由于算法难以理解,刚开始并没有得到很多人的重视。其后,作者在八年后,也就是1998年在ACM上正式发表,然而由于算法难以理解还是没有得到重视。而作者之后用更容易接受的方法重新发表了一篇论文《Paxos Made Simple》。可见,Paxos算法是有多难理解,即便现在放到很多高校,依然很多学生、教授都反馈Paxos算法难以理解。同时,Paxos算法在实际应用实现的时候也是比较困难的。这也是为什么会有后来Raft算法的提出。
2 概念
Raft是实现分布式共识的一种算法,主要用来管理日志复制的一致性。它和Paxos的功能是一样,但是相比于Paxos,Raft算法更容易理解、也更容易应用到实际的系统当中。而Raft算法也是联盟链采用比较多的共识算法。
它保证了在一个由N个节点构成的系统中有(N+1)/2个节点正常工作的情况下的系统的一致性,比如在一个5个节点的系统中允许2个节点出现非拜占庭错误,如节点宕机、网络分区、消息延时。Raft相比于Paxos更容易理解,且被证明可以提供与Paxos相同的容错性以及性能,其详细介绍可见官网及动态演示。
3 节点类型
在Raft算法中,每个网络节点只能如下三种身份之一:Leader、Follower以及Candidate,其中:
- Leader:主要负责与外界交互,由Follower节点选举而来,在每一次共识过程中有且仅有一个Leader节点,由Leader全权负责从交易池中取出交易、打包交易组成区块并将区块上链;
- Follower:以Leader节点为准进行同步,并在Leader节点失效时举行选举以选出新的Leader节点;
- Candidate:Follower节点在竞选Leader时拥有的临时身份。
4 任期
Raft算法将时间划分为不定长度的任期Terms,Terms为连续的数字。每个Term以选举开始,如果选举成功,则由当前leader负责出块,如果选举失败,并没有选举出新的单一Leader,则会开启新的Term,重新开始选举。
每个leader可以理解为都是有自己的任期(term)的,每一期起始于选举阶段,直到因节点失效等原因任期结束。每一期选举期间,每个follower节点只能投票一次。图中t3可能是因为没有获得超半数票等造成选举失败,须进行下一轮选举,此时follower可以再次对最先到达的candidate发出的RequestVote请求投票(先到先得)。
每个leader在当选期间都有一个自己的任期编号,它是全局单调递增的数字。每个节点都存储这当前的leader的任期编号,当处于candidate阶段的时候,发起投票的时候会把当前任期编号加一。
而且当一个节点接受到比自己任期高的请求时,会将自己的任期编号更新为高的任期编号,如果当前角色是leader,会从leader转换为follower角色。当接受到任期编号比自己小的请求时,节点会直接拒绝这个请求。
一个节点在一个任期只能投一票,如果A、B节点都请求C节点投票,C节点如果先投给A之后、就会拒绝B的投票请求。
5 消息类型
在Raft算法中,每个网络节点间通过发送消息进行通讯,当前Raft模块包括四种消息:VoteReq、VoteResp、Heartbeat、HeartbeatResp,其中:
- VoteReq:投票请求,由Candidate节点主动发出,用于向网络中其他节点请求投票以竞选Leader;
- VoteResp:投票响应,在节点收到投票请求后,用于对投票请求进行响应,响应内容为同意或拒绝该投票请求;
- Heartbeat:心跳,由Leader节点主动周期发出,其作用有两个:(1) 用于维护Leader节点身份,只要Leader能够一直正常发送心跳且被其他节点响应,Leader身份就不会发生变化;(2) 区块数据复制,当Leader节点成功打包一个区块后,会将区块数据编码至心跳中以将区块进行广播,其他节点在收到该心跳后会解码出区块数据并将区块放入自己的缓冲区中;
- HeartbeatResp:心跳响应,在节点收到心跳后,用于对心跳进行响应,特别的,当收到一个包含区块数据的心跳时,该心跳的响应中会带上该区块的哈希;
6 节点状态转换
-
Follower -> Candiddate
当节点启动时,节点自动成为Follower且将Term置0。只要Follower从Leader或者Candidate收到有效的Heartbeat或RequestVote消息,其就会保持在Follower状态,如果Follower在一段时间内(这段时间称为 Election Timeout)没收到上述消息,则它会假设系统当前的Leader已经失活,然后增加自己的Term并转换为Candidiate,开启新一轮的Leader选举流程,为了避免选举冲突,这个时间是一个随机数(一般为150~300ms)。 -
Candiddate-> Leader
超时成为Candidate后,增加自己的Term值,并将票投给自己,广播RequestVote到其他节点请求投票,假设有2N+1个节点,收到N+1个节点以上的同意回应,即被选举为Leader节点,且同时立即向其他所有Follower发送心跳包;(发送心跳包是为了宣告天下,保持自己的权威地位) -
Candiddate-> Follower
在等待投票期间,Candidate 可能会收到另一个声称自己是 Leader 的服务器节点发来的 AppendEntries RPC 。如果这个 Leader 的任期号(包含在RPC中)不小于 Candidate 当前的任期号,那么 Candidate 会承认该 Leader 的合法地位并回到 Follower 状态。 -
Leader-> Follower
如果Leader崩溃时间较长,在网络中已有新的Leader选举产生后恢复,由于旧的Leader任期号将小于新的Leader,在旧的Leader接收到新的Leader发送的心跳消息后则会变为Follower状态。
7 日志复制
leader选举成功后,将进入有效工作阶段,即日志复制阶段,其中日志复制过程会分记录日志和提交数据两个阶段。整个过程如下:
- 首先client向leader发出command指令;(每一次command指令都可以认为是一个entry)
- leader收到client的command指令后,将这个command entry追加到本地日志中,此时这个command是uncommitted状态,因此并没有更新节点的当前状态;
- 之后,leader向所有follower发送这条entry,follower接收到后追加到日志中,并回应leader;
- leader收到大多数follower的确认回应后,此entry在leader节点由uncommitted变为committed状态,此时按这条command更新leader状态;
- 在下一心跳中,leader会通知所有follower更新确认的entry,follower收到后,更新状态,这样,所有节点都完成client指定command的状态更新。
可以看到client每次提交command指令,服务节点都先将该指令entry追加记录到日志中,等leader确认大多数节点已追加记录此条日志后,在进行提交确认,更新节点状态。
8 思考
1:是否存在选不出Leader的情况?
Raft采取随机超时时间,Raft系统有一个选举超时配置项,Follower和Candidate的计时器超时时间每次重新计算,随机选取配置时间的1倍到2倍之间。即使所有节点同时启动,由于随机超时时间的设置,各个节点一般不会同时转为Candidate,先转为Candidate的节点会先发起投票,从而获得多数票。因而在每个任期内,多个节点同时请求投票并且都只获得少数票的几率很小,连续多次发生这种情况几率更小,
2:如何选择election timeout的长短?
- 至少需要长于几个心跳定时发送的时间。因为有些心跳信息可能因为网络问题没有到达节点,至少要允许重发的心跳可以到达,这样避免了无意义的重新选举提高了效率。
- 随机的election timeout时间区间的下限,要足够一个candidate成功竞选。
- 随机的election timeout时间区间的上限,要短到能够及时发现错误,避免长时间停顿。
3:网络状态良好的情况下,Leader会一直出块吗?
由于Leader没有任期时间限制,如果Leader不出现宕机,其他Follower能一直收到心跳,则Leader会一直出块,感觉有点违背去中心化共识,但Raft是基于CFT容错,可以不考虑恶意节点的情况。
4:最多允许多少故障节点?
Raft 算法只支持容错故障节点,不支持拜占庭容错节点,假设集群总节点数为n,故障节点为 f ,根据小数服从多数的原则,集群里正常节点只需要比 f 个节点再多一个节点,即 f+1 个节点,正确节点的数量就会比故障节点数量多,那么集群就能达成共识。因此 Raft 算法支持的最大容错节点数量是(n-1)/2。