Raft协议及伪码解析
跟着Martin大神学习Raft协议,带上讲解和伪码确实给人深入浅出的感觉,英音听起来十分优雅,也是一种享受了~
视频地址:Distributed Systems 6.2: Raft
整篇主要包括了十张Slide:
节点的状态转换
首先需要明确,节点只有三种状态:
- follower
- candidate
- leader
follower
当一个节点刚启动的时候,或者刚从崩溃中恢复,它只会变成follower
,等待来自其他节点的消息
candidate
当follower
有一段时间没有接收到来自leader
或者candidate
的消息,它会开始怀疑leader
已经掉线,于是准备开始发起选举,推举自己变成leader
。
这段时间对于每个节点是随机设置的,否则所有节点刚启动的时候都会在同一时间发起选举。
如何选举?
概括一下:
candidate
会增加自己的term
,然后邀请其他节点进行投票。- 如果
candidate
接收到比自己更高的term
(可能来自于leader
或者其他candidate
),那么它会重新变成follower
。 - 如果
candidate
收到足够多的投票,那么它会变成leader
。
下文都会使用
quorum
(合法的法定人数)来表示足够多的投票,表示过半数以上的节点。
- 如果
candidate
没有在计时器(timer)时间范围内获得quorum
票数的话,选举超时。它会再次增加term
,发起投票邀请,再次进入选举。
leader
当选了leader
后,一般情况下会一直保持这个状态。除非:
leader
掉线了,那么它再次上线回重新变成follower
。- 它接受到了来自其他节点发送的消息,而他们的
term
比它更高,那么它也会变成follower
。
这种情况可能是由于网络分区导致其他节点无法与它连接,认为它已经掉线而重新进行了选举。
伪码部分
节点初始化(Initialazation)
初始化中需要注意的变量有:
需要持久化的四个变量(他们的值不能因为节点崩溃而丢失)
currentTerm
:节点当前处在的term
值voteFor
:节点把票投给的节点ID
值log
:可以认为是一个数组,每一个值entry
包含了msg
和term
值。msg
表示需要向其他节点广播复制的消息,而term
表示该条消息所处的term
的值。log
通过追加新entry
的方式进行增长,如果某条entry
已经被quorum
数量的节点复制成功,那么这条entry
可以被commit
,即被提交。commitLength
:已经被提交的长度
其他变量可以因为节点崩溃在恢复时被重置。同时假设,每个节点拥有唯一的ID
,nodes
变量保存了所有节点的ID
,系统中的节点数量发生变化不在讨论的范畴之内。
前面提到,当follower
发现不能与leader
进行通信时,会使自己的currentTerm
+1,然后将自己设置为candidate
,发起选举。与此同时,它会将票投给自己,因此votedFor
的值设置为自己的ID
,votesReceived
集合中也添加了自己的ID
,并且初始化lastTerm
。
lastTerm
代表节点本地log
的最后一条entry
的term
值。
candidate
将构造好的投票信息msg
(由nodeId
, currentTerm
, log.length
, lastTerm
组成)发送给其他节点,开启计时器,进行选举。
选举时其他节点的视角
当其他节点收到了投票请求后,会首先对自身的currentTerm
与candidate
发送过来的currentTerm
(用cTerm
替代)进行比较:
- 如果自身
currentTerm
小于cTerm
,那么自己变成follower
,更改自己的currentTerm
变成cTerm
。 - 如果自身存在
log
,取出自己最新接收到的entry
的term
值作为自己的lastTerm
。 logOk
的值当candidate
发来的lastTerm
(用cLogTerm
代替) > lastTerm(candidate
的最新的log
的term
比自己大)或者cLogTerm=lastTerm
但是candidate.log.length >= log.length
(candidate
虽然与当前node
中的最新log
的term
值相同,但是candidate
拥有很多msg
)为true
。
由3可见,
logOk
为true
的前提是,candidate
拥有更新更多的log
。
- 当且仅当
currentTerm
接受了candidate
的cTerm
,并且candidate
拥有最新的log
(logOk
为true
),且没有给其他candidate
投票的情况下,可以将自身的voteFor
的值设置为candidate
的ID
值。这种情况下,表示准备将票投给candidate
,发送一条granted
为true
的VoteResponse
(由当前节点ID
-nodeId
,currentTerm
, 是否投票给它的granted
)消息返回给它。否则,返回一条granted
为false
的VoteResponse
消息。
回到candidate
选举时的视角
当candidate
收到来自其他节点的回复时,判断:
- 如果自己仍然还是
candidate
,并且其他节点的term
与自己的一致,也同意投票给自己,将这个节点添加至自己的votesReceived
集合中(发起投票时,里面最初只有自己投给自己)。 - 判断
votesReceived
中节点的数量是否已经过半,如果已经过半了,那么将自己设置为leader
,currentLeader
的值为自己的节点ID
,并且取消计时器。遍历nodes
集合中的所有node
,更改sentLength
和ackLength
字段,执行ReplicateLog
函数。
sentLength
和ackLength
都可以看做是一个map
,key
为follower
的nodeId
,value
是一个数值,代表长度。前者代表leader
已经发送给某个follower
的log
长度,后者代表该follower
已经确认收到的长度。显然,当sentLength
的值设置为log.length
,表明leader
假设follower
已经拥有和leader
一样多的log
,虽然这个假设可能是错的,但是我们会在后面进行修正。
ReplicateLog
也会在后面进行解释。
- 如果节点的
term
大于自身的currentTerm
,表明有比自己更高term
的节点存在,那么自己重新变成follower
,并且更改自己的currentTerm
为那个更高的term
,取消定时器,终止选举。
消息如何广播复制
当应用发送消息给集群时,消息是如何被保存提交的?
- 如果
leader
接收了消息,那么它可以将消息直接保存到自己的log
中,然后修改自己的ackedLength
,再遍历follower
调用ReplicatedLog
进行复制。 - 如果是
follower
接收到了消息,那么它需要通过FIFO
队列,将这个请求发送给leader
进行处理。
与此同时,leader
会周期性地向follower
复制消息,即使没有新的消息需要进行广播,这样不仅可以充当与其他节点之间的心跳链接,也可以当做复制给某个follower
失败时的重传。
重要的反复出现的ReplicateLog
在这里,sentLength
派上了用场。利用sentLength
将log
分割成两个部分:
prefixLen
:已经发送给follower
的log
长度suffix
: 需要发送给follower
的log
的entry
列表。
如果prefixLen = log.length
,那么suffix
为空,代表没有新数据要发送给follower
。
leader
构造了一个LogRequest
,包括自身的ID
, currentTerm
,prefixLen
, prefixTerm
(已经发送给follower
的log
的最后一个值log[prefixLen-1]的term
), commitLength
, suffix
发送给follower
。
节点收到了LogRequest
这时有两种情况:
- 节点可能正在处于
candidate
状态,如果接收到的term
值大于currentTerm
,那么它会取消选举,将自己变成follower
,并设置leader
,否则的话,返回一条接收失败的LogResponse
给发送者。 - 节点是个普通的
follower
。
如果节点的状态是follower
,那么需要判断logOk
,这个值当且仅当当前节点的log
长度大于等于prefixLen
(当前节点的log
不能小于leader
认为的已经发送的长度,否则存在log
的丢失),以及如果存在prefixLen
,那么leader
端的prefixTerm
应当与当前节点的最新log
的term
一致。
Raft
保证,如果两个节点的log
在同样的index
包含同样的term
,那么他们在该index
以及之前的log
都是相同的。
如果节点的term
与leader
一致,并且logOk
为true
,那么节点将会执行Appendentries
,并且更改自己的ack
为prefixLen+suffix.length
,作为LogResponse
的一部分返回。
节点如何追加log
,Appendentries
follower
需要判断的是,是否已经包含了这个消息。
- 如果
follower
的log
长度大于prefixLen
,并且suffix
的长度不为空,意味着follower
中的log
可能包含了以前leader
的消息,于是需要找到他们重叠位置的index
。
根据这个index
的值,判断当前log
的term
和suffix
对应的term
,如果不相等,意味着需要对当前的log
进行截断,即丢弃prefixLen
之后的log
。
为什么能在这个地方截断?因为进入
Appendentries
的前提是logOk
已经为true
,此时已经保证prefixLen
之前的term
已经一致。
- 将
suffix
的值追加到log
中 - 如果
commitLength
小于leader
的commit
的值,那么将自己没有提交的log
发送给应用,然后增加自己的commitLength
。
再次回到leader
, 如何处理LogResponse
- 如果
leader
发现返回的term
大于自身的term
,那么说明有新的leader
,所以自身变为follower
。 - 否则,它会检查其他节点是否已经成功接收消息并且
ack
长度大于原来所记载的长度。
2.1 如果是,那么更新sentLength
和ackLength
,并且执行Commitlogentries
。
2.2success
为false
,表明sentLength
需要进行修正。然后重新执行Replicatelog
。
logOk
不为true
,可能是因为prefixLen >= log.length
,也可能是因为prefixTerm != log[prefixLen-1].term
。
leader
提交log
,Commitlogentries
leader
遍历所有的log
,将有超过半数以上被接收的entry
,认为是已经ready
的log
。
找到最大下标的log
,如果term
与当前一直,意味着已经可以被认为commit
的log
长度增加了。并且这些消息已经被过半节点存储,可以发送给应用了。
总结:
- 每一个节点在收到比自己高的
term
时,会变成follower
。 leader
复制到其他节点时,发送LogRequest
,其他节点返回LogResponse
,用来标识是否已经成功复制。leader
根据是否有过半的成功LogResponse
来判断消息是否能够最终commit