1620秒入门Raft

1620秒入门Raft

0. 别除了

1620秒是27分钟,我们抓紧开始

1. Raft要解决什么问题

Raft论文的第一句说:

Raft is a consensus algorithm for managing a replicated log.

replicated log 主要用在 replicated status machine 中,如下图所示:

说明:

  1. replicated state machine 通过集群方式,为客户提供强一致、高可靠的数据服务;所谓强一致,从用户角度看是指永远可以读到最新的写成功的数据,而从服务内部来看指的是所有的state machine中的数据都保持一致;所谓高可靠,是指即使出现网络延迟、丢包、乱序、服务器宕机等情况下,依然可以保证数据的强一致;
  2. 对于 replicated state machine 来说,会按序读取本地的日志,根据日志中记录的操作来更新状态;
  3. 由于 state machine 的状态变化是确定的( deterministic),因此只要保证所有的服务器上的日志都完全一致,就可以保证 state machine 的一致;(举例:例如对于一个 status += x的操作,设初始状态 status = 0, 对于一组确定的日志 [x=3, x=5, x=-4, ...],所有的 state machine最终都会得到同样的结果)
  4. 日志的一致性是由一致性模块来保证的;下面这张图从交互过程说明一致性模块在 replicated state machine 中的作用:

如何保证各个机器上日志的一致性?这就是Raft要解决的核心问题

2. Raft正常工作状态

一个Raft集群通常包括多个节点以保证高可用,例如对于包含五个节点的集群,可以容忍两个节点出现故障。下图是一个正常工作状态的三个节点的Raft集群:

说明:

  1. 一个Raft集群只包含一个Leader节点,其余均为Follower节点;
  2. 客户端只允许和Leader节点进行交互;
  3. Follower节点只允许从Leader节点接收写日志的请求(AppendEntries RPC);
  4. Leader节点将客户端的请求发送给所有的Follower节点,只有至少一半的节点回复OK时,才可以将日志commit,并返回给用户成功;
  5. Leader节点定期向所有的Follower节点发送心跳信息(不包含实际操作的 AppendEntries RPC),来告诉它们自己依然健康;

这里我们可以看出,Raft协议由Leader来确定日志的写入顺序(实际上也是操作执行的顺序),而Follower节点只要接收Leader节点的写日志请求并将日志追加写入即可。

我们考虑一种异常情况:

假设5个节点Raft集群:[s1, s2, s3, s4, s5] ,设s1为Leader,其余为Follower

第1个操作:SET x = 0
    所有节点均更新成功:s1(x=0), s2(x=0), s3(x=0), s4(x=0), s5(x=0)
第2个操作:SET x = x + 1
    s5由于网络原因更新失败:s1(x=1), s2(x=1), s3(x=1), s4(x=1), s5(x=0)
第3个操作:SET x = x + 2
    s5恢复正常:s1(x=3), s2(x=3), s3(x=3), s4(x=3), s5(x=2)

集群内节点的状态已经不一致了!不一致的主要原因是,除了s5以外的其他节点读取的日志序列是[x=0, x=x+1, x=x+2],而s5读取的日志序列为[x=0, x=x+2]

如何解决这个问题呢?后文会回答

3. Raft协议如何进行选主

当Leader节点由于异常原因(宕机、网络故障等)无法继续提供服务时,可以认为它结束了本轮任期(Term=n),需要开始新一轮的选举(Election),而新的Leader当然要从Follower中产生,开始新一轮的任期(Term=n+1)。

从Follower节点的角度来看,当它接收不到Leader节点的心跳时,就认为Leader可能已经不再了,这个时候就要开始准备自己的选举了。

一个Follower的竞选过程如下:

  1. 节点状态由Follower变为Candidate,同时设置当前的 Term
  2. Candidate节点给自己投1票,同时向其他所有节点发送拉票请求(RequestVote RPC)
  3. Candidate节点等待投票结果,确认下一步自己的去向:
    a)赢得选举,节点状态变为Leader
    b)有其他人赢得选举,节点状态变为Follower
    c)第一轮选举未产生结果,节点状态保持为Candidate

对投票结果进行详细说明:

自己赢得选举
只有当一个Candidate得到过半的选票时才能赢得选举,每一个节点按照先到先得的方式,最多投票给一位Candidate。
在Candidate赢得选举后,自己变为Leader,同时向所有节点发送心跳信息以使其他节点变为Follower,开始下一任任期。

他人赢得选举
在等待投票结果的过程中,如果Candidate收到其他节点发送的心跳信息(AppendEntries RPC),并检查心跳信息中的任期不比自己小,则自己变为Follower,听从新上任的Leader的指挥。

未产生结果
例如:当有多个Candidate同时竞选时,由于每个人先为自己投一票,导致没有任何一个人的选票数量过半。
当这种情况出现时,每一位Candidate都开始准备下一任竞选:将Term+1,同时再次发送拉票请求。
为了防止出现长时间选不出新Leader的情况,Raft采用了两个方法:
1. Follower认为Leader不可用的超时时间(election timeout),是一个随机值,这首先防止了所有的Follower都在同一时刻发现Leader不可用的情况,从而让先发现的Follower顺利当选;
2. 即使出现多个Candidate同时竞选的情况,再发送拉票请求时,也有一段随机的延迟,来保证大家不是同时发送拉票请求;

了解了选举过程,我们就不难看懂下面这张图了(重点理解 “Term 3” 是怎么回事):


回到上一节的问题,首先来看看日志的基本组织方式:

说明:

  1. 每一条日志都有日志序号(log index)
  2. 每一条日志记录除了保存 state machine 要执行的操作(如 x ← 0)外,还需要保存该操作对应的 Term
  3. 由Leader决定什么时候对记录进行commit:当一条记录已经在至少半数的Follower上持久化后,Leader可以将该记录commit,并提供给state machine执行对应的操作
  4. Leader给Follower的复制日志请求中(AppendEntries RPC),不仅包含具体的操作,还包含本次插入的前一个位置的index和Term;这样对于刚才的问题,s5节点就不会直接追加日志了,而会检查发现自己有日志缺失,进而由Leader将缺失的日志全部同步过来(实际上Leader里记录了发送给每一个Follower的日志序号);

4. 关于选主的其他重要问题

长时间的Leader下线,重新选Leader的过程可能会导致各个节点出现如下的日志序列(注意:图中不是所有的日志都是commited)

从图中可以看到,对于新选出的Leader,有些Follower可能缺少记录,有些Follower可能多出记录,而有些甚至有多有少。什么时候会出现这种情况呢?以节点f举例:

  1. 节点f在Term 2时是Leader,在提交了几条记录后,还未执行commit就下线
  2. 节点f在Term 3时再次上线并成为Leader,在提交了几条记录后,还未执行commit(包括Term 2的commit)就又一次下线

在Raft中,由Leader负责修复这种不一致,具体的做法是:Leader为每一个Follower节点维护一个nextIndex计数,对于每一个Follower节点,首先设置nextIndex为Leader节点的下一个index位置(图中为11),然后依次向前比较Leader和Follower对应的记录,直到找到重合的记录为止,再将所有Leader节点的记录复制到Follower节点。

总结就是:Leader负责一致性检查,同时让所有的Follower都和自己保持一致


读到这里,我们应该意识到,并不是任何一个Follower都有资格成为Leader,因为如果一个Follower缺少了上任Leader已经commit的日志,那它是无论如何也不能做新的Leader的,因为这会导致数据的不一致。

所以Raft对选举有限制条件1:Candidate在拉票时需要携带自己本地已经持久化的最新的日志信息,等待投票的节点如果发现自己本地的日志信息比竞选的Candidate更新,则拒绝给他投票。


还有一种情况,可能会导致即使多个超过半数的节点已经提交了日志,依然有可能被其他新选的Leader覆盖日志,如下所示:


说明:

  1. 在图(a)中,S1为Leader,它只完成了部分日志复制(在index=2的位置)
  2. 在图(b)中,S1下线,S5由S3、S4选为Term 3的Leader,并在 index=2 的位置接收到新的请求
  3. 在图(c)中,S5下线,S1上线并被重新选为Leader,这时日志被复制到超过半数的机器上,S1,S2,S3上index=2位置的日志虽然符合commit的条件,但是不能被commit,原因见4,5
  4. 如果S1在图(d)中再次下线,此时S5可以被选为Leader(因为它的Term最大),这时它可能将自己的数据覆盖S1,S2,S3上已经commit的数据,导致严重错误
  5. 而如果S1像在图(e)中那样commit了Term 4的日志,则及时S1下线,S5也不会被选为Leader

所以Raft对选举有限制条件2:只允许Leader提交(commit)当前Term的日志。

5. 喘一口气

现在差不多过了1537秒,Raft论文里还讲了很多关于正确性的证明、性能优化和实际使用等一些问题,这篇短文入门应该够了,建议大家有时间还是去读读论文吧。

感谢女朋友在炎热的夏天帮我花9块钱打印Raft的论文,如果你觉得这篇文章对你有帮助,请点赞和鼓励。

感谢 @丁凯 的专栏文章 “Raft协议详解”,大家读完这篇可以去看看那篇。

time's up


欢迎关注 我的专栏

编辑于 2017-08-09

文章被以下专栏收录