Raft共识算法

Raft共识算法在分布式系统中是经常使用的共识算法之一,论文原文In Search of an Understandable Consensus Algorithm ,做者在论文中指出Poxas共识算法的两大问题,其一是难懂,其二是应用到实际系统存在困难。针对Paxos存在的问题,做者的目的是提出一个易懂的共识算法,论文中有Designing for understandability单独一小节,其中强调Raft必须是一个实用的、安全可用、有效易懂的共识算法。本文描述了Raft共识算法的细节,不少内容描述及引用图片均摘自论文原文。git

Raft概述

咱们主要分如下三部分对Raft进行讨论:github

  • Leader election——a new leader must be chosen when
    an existing leader fails. (领导人选举)
  • Log replication——the leader must accept log entries from clients and replicate them across the cluster,
    forcing the other logs to agree with its own.(日志复制)
  • Safety——the key safety property for Raft. (安全性)

正常工做过程当中,Raft分为两部分,首先是leader选举过程,而后在选举出来的leader基础上进行正常操做,好比日志复制操做等。算法

一个Raft集群一般包含\(2N+1\)个服务器,容许系统有\(N\)个故障服务器。每一个服务器处于3个状态之一:leaderfollowercandidate。正常操做状态下,仅有一个leader,其余的服务器均为follower。follower是被动的,不会对自身发出的请求而是对来自leader和candidate的请求作出响应。leader处理全部的client请求(若client联系follower,则该follower将转发给leader)。candidate状态用来选举leader。状态转换以下图所示:
安全

为了进行领导人选举和日志复制等,须要服务器节点存储以下状态信息:服务器

状态 全部服务器上持久存在的
currentTerm 服务器最后一次知道的任期号(初始化为 0,持续递增)
votedFor 在当前得到选票的候选人的 Id
log[] 日志条目集;每个条目包含一个用户状态机执行的指令,和收到时的任期号
状态 全部服务器上常常变的
commitIndex 已知的最大的已经被提交的日志条目的索引值
lastApplied 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增)
状态 在领导人里常常改变的 (选举后从新初始化)
nextIndex[] 对于每个服务器,须要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一)
matchIndex[] 对于每个服务器,已经复制给他的日志的最高索引值

Raft在任什么时候刻都知足以下特性:网络

  • Election Safety:在一个任期中只能有一个leader;
  • Leader Append-Only:leader不会覆盖或删除日志中的entry,只有添加entry(follower存在依据leader回滚日志的状况);
  • Log Matching:若是两个日志包含了一条具备相同index和term的entry,那么这两个日志在这个index以前的全部entry都相同;
  • Leader Completeness: 若是在某一任期一条entry被提交committed了,那么在更高任期的leader中这条entry必定存在;
  • State Machine Safety:若是一个节点将一条entry应用到状态机中,那么任何节点也不会再次将该index的entry应用到状态机里;

下面咱们详细讨论这几部分。分布式

Leader选举(Leader election)

一个节点初始状态为follower,当follower在选举超时时间内未收到leader的心跳消息,则转换为candidate状态。为了不选举冲突,这个超时时间是一个随机数(通常为150~300ms)。超时成为candidate后,向其余节点发出RequestVoteRPC请求,假设有\(2N+1\)个节点,收到\(N+1\)个节点以上的赞成回应,即被选举为leader节点,开始下一阶段的工做。若是在选举期间接收到eader发来的心跳信息,则candidate转为follower状态。动画

在选举期间,可能会出现多个candidate的状况,可能在一轮选举过程当中都没有收到多数的赞成票,此时再次随机超时,进入第二轮选举过程,直至选出leader或着从新收到leader心跳信息,转为follower状态。spa

正常状态下,leader会不断的广播心跳信息,follower收到leader的心跳信息后会重置超时。当leader崩溃或者出现异常离线,此时网络中follower节点接收不到心跳信息,超时再次进入选举流程,选举出一个leader。3d

这里还有补充一些细节,每一个leader能够理解为都是有本身的任期(term)的,每一期起始于选举阶段,直到因节点失效等缘由任期结束。每一期选举期间,每一个follower节点只能投票一次。图中t3多是由于没有得到超半数票等形成选举失败,须进行下一轮选举,此时follower能够再次对最早到达的candidate发出的RequestVote请求投票(先到先得)。

对全部的请求(RequestVote、AppendEntry等请求),若是发现其Term小于当前节点,则拒绝请求,若是是candidate选举期间,收到不小于当前节点任期的leader节点发来的AppendEntry请求,则承认该leader,candidate转换为follower。

日志复制(Log replication)

leader选举成功后,将进入有效工做阶段,即日志复制阶段,其中日志复制过程会分记录日志和提交数据两个阶段。

整个过程以下:

  1. 首先client向leader发出command指令;(每一次command指令均可以认为是一个entry)
  2. leader收到client的command指令后,将这个command entry追加到本地日志中,此时这个command是uncommitted状态,所以并无更新节点的当前状态;
  3. 以后,leader向全部follower发送这条entry,follower接收到后追加到日志中,并回应leader;
  4. leader收到大多数follower的确认回应后,此entry在leader节点由uncommitted变为committed状态,此时按这条command更新leader状态;
  5. 在下一心跳中,leader会通知全部follower更新确认的entry,follower收到后,更新状态,这样,全部节点都完成client指定command的状态更新。

能够看到client每次提交command指令,服务节点都先将该指令entry追加记录到日志中,等leader确认大多数节点已追加记录此条日志后,在进行提交确认,更新节点状态。若是还对这个过程有些模糊的话,能够参考Raft动画演示,较为直观的演示了领导人选举及日志复制的过程。

安全(Safety)

前面描述了Raft算法是如何选举和复制日志的。然而,到目前为止描述的机制并不能充分的保证每个状态机会按照相同的顺序执行相同的指令。咱们须要再继续深刻思考如下几个问题:

  • 第一个问题,leader选举时follower收到candidate发起的投票请求,若是赞成就进行回应,但具体的规则是什么呢?是全部的follower都有可能被选举为领导人吗?
  • 第二个问题,leader可能在任什么时候刻挂掉,新任期的leader怎么提交以前任期的日志条目呢?

选举限制

针对第一个问题,以前并无细讲,若是当前leader节点挂了,须要从新选举一个新leader,此时follower节点的状态多是不一样的,有的follower可能状态与刚刚挂掉的leader相同,状态较新,有的follower可能记录的当前index比原leader节点的少不少,状态更新相对滞后,此时,从系统最优的角度看,选状态最新的candidate为佳,从正确性的角度看,要确保Leader Completeness,即若是在某一任期一条entry被提交成功了,那么在更高任期的leader中这条entry必定存在,反过来说就是若是一个candidate的状态旧于目前被committed的状态,它必定不能被选为leader。具体到投票规则:
1) 节点只投给拥有不比本身日志状态旧的节点;
2)每一个节点在一个term内只能投一次,在知足1的条件下,先到先得;

咱们看一下请求投票 RPC(由候选人负责调用用来征集选票)的定义:
| 参数 | 解释 |
| ----- | ----- |
| term| 候选人的任期号|
| candidateId| 请求选票的候选人的 Id |
| lastLogIndex| 候选人的最后日志条目的索引值|
| lastLogTerm| 候选人最后日志条目的任期号|

返回值 解释
term 当前任期号,以便于候选人去更新本身的任期号
voteGranted 候选人赢得了此张选票时为真

接收者实现:

  1. 若是term < currentTerm返回 false
  2. 若是 votedFor 为空或者为 candidateId,而且候选人的日志至少和本身同样新,那么就投票给他

能够看到RequestVote投票请求中包含了lastLogIndex和lastLogTerm用于比较日志状态。这样,虽然不能保证最新状态的candidate成为leader,但可以保证被选为leader的节点必定拥有最新被committed的状态,但不能保证拥有最新uncommitted状态entries。

提交以前任期的日志条目

领导人知道一条当前任期内的日志记录是能够被提交的,只要它被存储到了大多数的服务器上。可是以前任期的未提交的日志条目,即便已经被存储到大多数节点上,也依然有可能会被后续任期的领导人覆盖掉。下图说明了这种状况:

如图的时间序列展现了为何领导人没法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。在 (b) 中,S1崩溃了,而后S5在任期3里经过S三、S4和本身的选票赢得选举,而后从客户端接收了一条不同的日志条目放在了索引 2 处。而后到 (c),S5又崩溃了;S1从新启动,选举成功,开始复制日志。在这时,来自任期2的那条日志已经被复制到了集群中的大多数机器上,可是尚未被提交。若是S1在(d)中又崩溃了,S5能够从新被选举成功(经过来自S2,S3和S4的选票),而后覆盖了他们在索引 2 处的日志。反之,若是在崩溃以前,S1 把本身主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(由于S5 就不可能选举成功)。 这样在同一时刻就同时保证了,以前的全部老的日志条目就会被提交。

为了消除上图里描述的状况,Raft永远不会经过计算副本数目的方式去提交一个以前任期内的日志条目。只有领导人当前任期里的日志条目经过计算副本数目能够被提交;一旦当前任期的日志条目以这种方式被提交,那么因为日志匹配特性,以前的日志条目也都会被间接的提交。

当领导人复制以前任期里的日志时,Raft 会为全部日志保留原始的任期号。

对Raft中几种状况的思考

follower节点与leader日志内容不一致时怎么处理?


咱们先举例说明:正常状况下,follower节点应该向B节点同样与leader节点日志内容一致,但也会出现A、C等状况,出现了不一致,以A、B节点为例,当leader节点向follower节点发送AppendEntries<prevLogIndex=7,prevLogTerm=3,entries=[x<-4]>,leaderCommit=7时,咱们分析一下发生了什么,B节点日志与prevLogIndex=7,prevLogTerm=3相匹配,将index=7x<-5)这条entry提交committed,并在日志中新加入entryx<-4,处于uncommitted状态;A节点接收到时,当前日志index<prevLogIndexprevLogIndex=7,prevLogTerm=3不相匹配,拒接该请求,不会将x<-4添加到日志中,当leader知道A节点因日志不一致拒接了该请求后,不断递减preLogIndex从新发送请求,直到A节点index,termprevLogIndex,prevLogTerm相匹配,将leader的entries复制到A节点中,达成日志状态一致。

咱们看一下附加日志 RPC(由领导人负责调用复制日志指令;也会用做heartbeat)的定义:
| 参数 | 解释 |
| ------ | ------- |
|term| 领导人的任期号|
|leaderId| 领导人的 Id,以便于跟随者重定向请求|
|prevLogIndex|新的日志条目紧随以前的索引值|
|prevLogTerm|prevLogIndex 条目的任期号|
|entries[]|准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提升效率)|
|leaderCommit|领导人已经提交的日志的索引值|

返回值 解释
term 当前的任期号,用于领导人去更新本身
success 跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真

接收者实现:

  1. 若是 term < currentTerm 就返回 false;
  2. 若是日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false;
  3. 若是已经存在的日志条目和新的产生冲突(索引值相同可是任期号不一样),删除这一条和以后全部的;
  4. 附加日志中还没有存在的任何新条目;
  5. 若是 leaderCommit > commitIndex,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个;

简单总结一下,出现不一致时核心的处理原则是一切听从leader。当leader向follower发送AppendEntry请求,follower对AppendEntry进行一致性检查,若是经过,则更新状态信息,若是发现不一致,则拒绝请求,leader发现follower拒绝请求,出现了不一致,此时将递减nextIndex,并从新给该follower节点发送日志复制请求,直到找到日志一致的地方为止。而后把follower节点的日志覆盖为leader节点的日志内容。

leader挂掉了,怎么处理

前面可能断断续续的提到这种状况的处理方法,首要的就是选出新leader,选出新leader后,可能上一任期还有一些entries并无提交,处于uncommitted状态,该怎么办呢?处理方法是新leader只处理提交新任期的entries,上一任期未提交的entries,若是在新leader选举前已经被大多数节点记录在日志中,则新leader在提交最新entry时,以前处于未提交状态的entries也被committed了,由于若是两个日志包含了一条具备相同index和term的entry,那么这两个日志在这个index以前的全部entry都相同;若是在新leader选举前没有被大多数节点记录在日志中,则原有未提交的entries有可能被新leader的entries覆盖掉。

出现网络分区时怎么处理?

分布式系统中网络分区的状况基本没法避免,出现网络分区时,原有leader在分区的一侧,此时若是客户端发来指令,旧leader依旧在分区一测进行日志复制的过程,但因收不到大多数节点的确认,客户端所提交的指令entry只能记录在日志中,没法进行提交确认,处于uncommitted状态。而在分区的另外一侧,此时收不到心跳信息,会进入选举流程从新选举一个leader,新leader负责分区零一侧的请求,进行日志复制等操做。由于新leader能够收到大多数follower确认,客户端的指令entry能够被提交,并更新节点状态,当网络分区恢复时,此时两个leader会收到彼此广播的心跳信息,此时,旧leader发现更大term的leader,旧leader转为follower,此时旧leader分区一侧的全部操做都要回滚,接受新leader的更新。

参考文档:
In Search of an Understandable Consensus Algorithm
The Raft Consensus Algorithm
深刻浅出RAFT共识算法
Raft共识算法及其实现

相关文章
相关标签/搜索