raft算法基本原理

Raft算法是一种通过日志复制实现数据一致性的算法。

0. 关键概念

1. 节点状态

主要有三种角色,分别是Leader、Follower和Candidate(其实大部分时间只有Leader和Follower角色,因为Candidate只是选举的一个过渡角色)。
  • Follower:主要负责处理Leader节点发起的请求,比如添加日志、心跳检测等操作。集群启动时, 每个raft节点都以Follower角色运行。各自维护一个超时字段(ElectionTick属性),如果在超时时间内都没有收到来自Leader的消息,转换为Candidate角色,发起选举。
  • Candidate:如果选举成功,则转为Leader角色;如果选举失败,就转为Follower角色。选举过程中可能会有多个Candidate节点竞争领导人角色,并且获得同样多的选票,这种情况都不会转为Leader角色,而是等待一段时间重新发起选举,保证一次选举最多只有一个leader角色。这里面的优化机制是,每个候选人重新发起选举的间隔时间是不一样的,可以降低多次选票相同重新选举的问题。
  • Leader:主要负责集群数据的管理,包括给Follower发送添加日志、心跳检测(HeartbeatTick属性)等命令。在Raft节点运行过程中Leader节点可能会宕机,这时候其他节点会重新选举出一个新Leader,并运行一段时间后,宕机的老Leader节点恢复了,新的Leader会给老的Leader发送消息,这时老Leader发现消息里面的任期(Term字段)比自己当前的Term更大,老Leader就知道自己已经不是最新的领导人了,就会自动转换为Follower角色。

2. 领导人选举

  • Follower检测到选举超时了,使自己转变为Candidate角色,并发的对集群所有节点发送选举的请求。
  • 收到消息的Follower节点会对Candidate的请求做出响应。
  • Candidate统计投票回应,如果Candidate收到超过半数节点的同意投票响应,就会成为新一任的Leader。
  • 如果Candidate没有当选Leader,就使自己转变为Follower角色,听从Leader的指令。

假设集群中有5个节点,分别是节点A、B、C、D和E,如果这时出现网络错误,使得集群中的节点(A、B、C、D、E五个节点)被分成两部分,节点A、B、C和节点D、E。这时D、E没收到Leader节点的消息会重新发起选取,但是由于不能获得多数节点的选票,因此Term会不断增大。当网络回复的时候,D、E节点的Term已经比正常集群的Term更大,因此节点D或节点E将当选Leader,导致节点A、B、C在节点D、E宕机时期添加的正常数据被清除,从而破环集群的历史数据。网络错误如下图所示:

Raft通过PreVote算法解决了这个问题,PreVote算法在Candidate发起选举之前,会首先向其他节点发送一个预投票信息,这时Term不变, 如果超过半数节点同意了选举请求,Candidate才设置Term+1,真正发起领导人选举投票。在上述环境中D、E节点是没办法获得超过半数节点的选票,因此节点只能反复Prevote, Term却没有机会增加。当网络恢复的时候当收到Node A的消息,立刻使自己变成Follower继续为集群服务,此时节点A会把网络分区时候添加的日志复制到节点D、E上,使得所有节点数据时一样的。

Follower节点在收到投票消息的时候肯定不能所有的选票都同意,Follower节点会对Candidate的消息进行检测,主要体现在Candidate的日志必须比自己更新。只有拥有最新日志的节点才有可能当选Leader,这样保证了集群中历史数据不会丢失。这部分主要检测Candidate日志的Term和index是否比自己的更新,注意这个Term是日志的Term,每个日志都有一个Term标识这条日志是在哪个任期被加上的;Index表示日志的索引。如果Term相同,则比较消息的index是否比自己日志在相同任期下更大,更大说明更新,只有检验通过才投票。

3. 日志复制

Raft集群中与客户端的交互都是通过Leader节点进行的,即使Follower 节点收到请求也会把请求转发到Leader 节点。集群中所有操作都是以日志复制的形式体现,达到一定规则,再把日志应用到状态机中。假设客户端发起一个SET X=5的指令,raft集群中相关节点的操作步骤可以概括如下:

  • 客户端给Leader节点发送一个 SET X = 5 的请求。
  • Leader节点收到客户端请求,首先把SET X = 5的指令写到日志中,然后把请求并发发送到所有Follower节点。
  • Follower节点收到Leader请求,也把SET X = 5 写到日志中,然后响应Leader 节点。
  • 当Leader节点收到大部分Follower节点写入成功后,把 SET X = 5 这个指令 commit并且应用到状态复制机,这时Leader 节点上的X 才真正等于5,并发送commit 成功给所有Follower节点。
  • Follower节点收到Leader节点commit成功的信息,也commit并应用到状态复制机,这个时候就实现了Raft集群数据的一致性

    上述过程所有节点都需要记录两个属性,commitIndex和lastApplied,这两个属性分别表示节点已经提交的日志索引和已经应用到状态机的索引。在分布式环境中每个节点日志索引可能会因为网络异常或者节点宕机造成与Leader节点的commitIndex、lastApplied不一致,这时就需要Leader节点有相应属性记录每个节点的commitIndex和lastApplied,这正是nextIndex[]和matchIndex[]的作用。Leader每次给其他节点复制日志之前都会查看节点对应的nextIndex值,并根据节点的回应修改nextIndex[]和matchIndex[]对应的值。

    考虑一种情况假设集群中有三个节点,节点A、B和C。一开始Node A是Leader节点,节点A和节点B的提交的记录是一样的,节点C由于某些因素(宕机或网络延迟等)导致数据未更新到最新状态。集群一开始都没有发生过Leader切换,因此Node A始终知道nextIndex[]和matchIndex[]的确定值。这时节点A宕机,节点B当选Leader,节点B一开始时不知道nextIndex[]和matchIndex[]的,那它是怎么获取nextIndex[]和matchIndex[]的值的呢?如下图所示:

    如上图所示,当切换Leader节点时,新的Leader节点会把所有节点的nextIndex设置为自己已提交日志的最大索引,matchIndex重置为0,既然能够当选,那么这个节点必定已经拥有完整的数据。当Leader节点向其他节点复制日志的时候,只需要根据nextIndex索引复制。以上图为例,当Leader节点复制日志给Node A时,由于Node A的数据和Node B数据是一样的,Node A会返回数据已经存在的相应,会返回Reject=true,RejectIndex=3的信息。Leader收到消息消息就重置Node A对应的nextIndex[Node A’s index]=3和matchIndex[Node A’s index]=3;同理,当Node C收到Leader 复制日志的请求时,也会返回Reject=true, RejectIndex=1的信息。Leader收到消息消息就重置Node C对应的nextIndex[Node C’s index]=2和matchIndex[Node C’s index]=1。这样Node B 就获取到了所有节点nextIndex和matchIndex的值,新Leader就能正常服务了。

你可能感兴趣的:(raft算法基本原理)