Raft角色

一个Raft集群包含若干节点,Raft将这些节点分为三种状态:Leader、Follower、Candidate,每种状态负责的任务也不一样。正常情况下,集群中的节点只存在Leader与follower两种状态。

  • Leader(领导者):负责日志的同步管理,处理来自客户端的请求,与follower保持心跳
  • Follower(追随者):响应Leader的日志同步请求,响应Candidate的邀票请求,以及将客户端请求到Follower的事务转发给Leader
  • Candidate(侯选者):负责选举投票,集群刚启动或者Leader宕机时,状态为Follower的节点将转为Candidate并发起选举,选举胜出后,从Candidate转为Leader状态。
Raft三个子问题

Raft实现了和Paxos相同的功能,它将一致性分解为多个子问题:Leader选举、日志同步、安全性、日志压缩、成员变更。
这里重点看下选举、日志同步与安全性:

  • 选举:当Leader宕机或集群启动时,一个新的Leader需要被选举出来
  • 日志同步:Leader接收来自客户端的请求并将其以日志条目的形式同步到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致
  • 安全性:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它节点不能在同一个日志索引位置应用一个不同的指令。

1. 选举原理

根据Raft协议,一个应用Raft协议的集群在刚启动时,所有节点的状态都是Follower。由于没有Leader,Follower无法与Leader保持心跳,因此,Follower会认为Leader已经下线,进而转为Candidate状态。然后,Candidate将向集群中其它节点请求投票,同意自己升级为Leader。如果Candidate收到超过半数节点的投票(N/2+1),它将成为Leader。

第一阶段:所有节点都是Follower
在准备选举时,所有节点状态都是Follower,并且初始任期为0,同时启动选举定时器,每个节点的选举定时器超时时间都在100-500毫秒之间且不一致,避免同时发起选举。

第二阶段:Follower转为Candidate并发起投票
节点启动后在一个选举定时器周期内未收到心跳和投票请求,则状态转为候选者Candidate状态,且Term自增,并向集群中所有节点发送投票请求并且重置选举定时器。每个节点的选举定时器超时时间都不一致,所以最先转为Candidate的节点最有可能成为Leader。

第三阶段:投票阶段
节点收到投票请求后会根据以下情况决定是否接受投票请求:

  1. 请求节点的Term大于自己的Term,且自己还未投票给其它节点,则接受请求,将票投给它
  2. 请求节点的Term小于自己的Term,且自己琮未投票,则拒绝请求,将票投给自己

第四阶段:Candidate转为Leader
一轮选举过后,正常情况下会有一个Candidate收到超过半数节点(N/2+1)的投票,它将胜出并升级为Leader。然后定时发送心跳给其它节点,其它节点会转为Follower并与Leader保持同步,至此,选举结束;如果未过半数会进行下一轮选举。

2. Raft日志同步

在一个Raft集群中,只有Leader节点能够处理客户端请求(如果请求到了Follower,它会将请求转发到Leader),客户端的每个请求都包含一条被复制状态机执行的指令。Leader将这条指令作为一条新的日志条目(Entry)附加到日志中去,然后并行地将附加条目发送给各个Followers,让它们复制这条上场条目。
当这条日志条目被各个Followers安全复制,Leader会将这条上场条目应用到它的状态机中,然后将执行结果返回给客户端。如果Follower崩溃或者运行缓慢,或者网络丢包,Leader会不断尝试附加日志条目直到所有的Follower都最终存储所有的日志条目,确保强一致性。

第一阶段:客户端请求提交到Leader
Leader在收到客户端请求后会将其作为日志条目(Entry)写入本地日志中,需要注意的是该Entry的状态是未提交(uncommitted),Leader并不会更新本地数据,因此它是不可读的。

第二阶段:Leader将Entry发送到其它Follower
Leader与Follower之间保持着心跳联系,随心跳Leader将追加的Entry(AppendEntries)并行地发送给其它的Follower,并让它们复制这条上场条目,这一过程称为复制(Replicate)。

  1. Leader向Follower发送的Entry是AppendEntries. 在一个心跳周期内,Leader可能会接收到多条客户端的请求,因此,Leader发送给Follower也会是多个Entry也就是AppendEntries
  2. Leader在向Follower发送追加的Entry外,还会将上一条日志条目的索引位置(prevLogIndex),Leader的任期号(Term)也一并发送。如果Follower在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目,出现这样的情况说明Follower与Leader数据不一致了。
  3. 处理Leader与Follower数据不一致的问题。由于某些原因Leader上数据可能比Follower上的数据多也可能少。要使二者数据保持相同,Leader会找到最后两者达成一致的地方,然后将那个点之后的日志条目删除并发送自己的日志给Follower,所有这些操作都在进行附加日志的一致性检查时完成。
    Leader还会为每个Follower维护一个nextIndex,它表示下一个需要发送给Follower日志条目的索引地址。当一个Leader刚被选举出来的时候,它会初始化所有的nextIndex值,并给自己最后一条日志的index加1。如果一个Follower的日志和Leader不一致,那么在下一次附加日志一致性检查会失败。在被Follower拒绝之后,Leader会减小该Follower对应的nextIndex值并进行重试。最终nextIndex会在某个位置使得Leader和Follower的日志达成一致,这时会将Follower冲突的日志条目全部删除并再加上Leader的日志,这样Follower的日志就会和Leader保持一致。

第三阶段:Leader等待Follower回应
Follower接收到Leader发来的复制请求后,可能会有两种回应:

  1. 写入本地日志中,返回Success;
  2. 一致性检查失败,拒绝写入,返回False(解决方式见第二阶段)

需要注意的是,此时该Entry的状态在Leader中仍是未提交状态,当Leader收到大多数Follower Success回应后,会将第一阶段写入的Entry标记为提交状态,并此日志条目应用到它的状态机中。

第四阶段:Leader回应客户端
完成前三个阶段后,Leader会向客户端回应OK,表示写操作成功。

第五阶段:Leader通知Followers Entry已提交
Leader回应客户端后,将随着下一个心跳通知各Follower, 各Folloer收到通知后会将Entry标记为提交状态,至此,Raft集群超过半数节点已经达到一致状态,可以确保强一致性。由于网络等原因可能会有部分Follower在某个时间点内与Leader不一致,但最终还是会一致。

3. Raft算法之安全性
Raft如何保证对于给定的任期号(Term),Leader都拥有之前任期的所有被提交的日志条目(也就是Leader的完整性)呢?
1) 为了保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的Leader中,Raft采用的是日志条目单向传递,只从Leader传给Follower,并且Leader从不会覆盖自身本地日志中已经存在的条目。
2) 一个Candidate只有包含了最新的已提交的log entry,并且获得了集群中大部分节点的认可才有可能赢得选举,这也意味着每一个已经提交的日志条目肯定存在于至少一个服务器节点上。
Candidate在发送RequestVote RPC时,要带上自己的最后一条日志的term和log index,其他节点收到消息时,如果发现自己的日志比请求中携带的更新,则拒绝投票。日志比较的原则是,如果本地的最后一条log entry的term更大,则term大的更新,如果term一样大,则log index更大的更新。
3) Leader可以复制前面任期的日志,但是不会主动提交前面任期的日志,而是通过提交当前任期的日志来间接地提交前面任期内的日志(这一点有些不太理解)

最后再说明下,Raft是Paxos的简化版,是基于Multi-Paxos实现的,而ZAB并不是基于Paxos的实现的,它与Paxos是两种不同的协议,不过ZAB的实现有参考Paxos。

参考的文章:
分布式算法 - Raft算法
Raft协议安全性保证
Paxos、Raft、ZAB算法


步履不停
38 声望13 粉丝

好走的都是下坡路