分布式教程1-分布式算法Raft入门

2020-03-01  本文已影响0人  老k的代码世界

简介

Raft是今比较流行的一个分布式选举算法。在它出来之前,业界只有Paxos一种算法。但是Paxos是非常难以理解,更不用说有统一的算法方案。Raft的出现,就是为了提供一个通俗易懂,容易实现的分布式方案。研究论文和相关源码已久,写下此文笔记,希望能对你也有一些启发。

基本原理

Server状态

集群的每个机器可以有3种角色状态

三者之间的关系,可以互相转换,如果下图

raft1.png
  1. 最开始,所有的Server都是Follower。
  2. 如果Follower没有收到心跳,就会变成Candidate
  3. Candidate 开始进行新一轮的选举,其中一个成为新的Leader,其余的成为Follower。
  4. Leader如果收到更大的Term(任期)消息,就降级为普通的Follower。

Term

时间划分为不同的Term(任期)。每次导致新的选举发生,Term就会改变+1.一个Term包括两个阶段:选举和正常阶段。每个服务器都会保存当前的任期Current Term。用于发送和接受RPC消息时验证比较。

raft2.png

Log Entry

每一个client操作,对于Leader来说,都是增加一个Log Entry,然后复制同步到其他的Server。包括以下数据:

Persistent State 持久化状态

每个server都会在本地存储一些持久化状态,每次在相应rpc时先持久化

选举过程

当一个Follower没有收到Leader的心跳时,就会投票开始新一轮的选举。所以当Leader出现故障时,可能有多个Follower变成Candidate开始投票。这里有个细节,就是开始的时间是不同的,是一个随机100-500ms,这样可以更快速产生新的Leader。

  1. 当前Term 增加1。
  2. 状态有Follower变成Candidate。
  3. 给自己投票。
  4. 发送投票信息RequeustVote给所有的Server,一直重试,直到以下情况之一发生才停止:
    • 收到大多数(>=n/2+1)的Server投票给自己,然后变成新一轮的Leader,开始发送心跳AppendEntry给其他的Server。
    • 收到新Leader的RPC,状态变成Follower
    • 没有人选举成功。增加Term,开始新一轮的选举。

RequeustVote RPC

参数

返回值

实现过程

1. if (term>currentTerm){
    currentTerm = term;
    //如果是(Leader or Candidate) -> Follwer
}
2. if(term==currentTerm){
    //本轮还没收到过其他的投票
    if(voteFor==null||voteFor==candidateId){
        if(candidateLog >=本地的log){
            赞同本次选举
            自己不再进行本轮主动投票
        }
    }
    
}

日志复制

当前Term如果处于正常状态,那么就可以接受client的请求。

AppendEntries RPC

参数

返回值

实现过程

//太旧的Term,说明这个Leader已经失效了
if(term<currentTerm){
    return false
}
if(term>currentTerm){
    currentTerm = term
}
//如果是candidater or leader,重新变成Follower
//如果找不到一个log,同时匹配preLogIndex和preLogTerm,return false
//删除冲突的entry
//添加新的entry
//更新本地的状态机

异常情况

Log一致性

基于上面的论证,每次AppendEntries RPC都会带上当前entry的前一个log信息,包括index和term,如果Follower找不到,就说明两者的log不一致了,需要删除冲突的,填上新加的,然后再提交本次需要committed的。

举个例子


raft3.png

选举最合适的Leader

当一个Log被committed时,它一定是被复制到大多数(大于1半)的server。如果要重新选举一个Leader,我们希望Candidate带上最多的committed entries。

上面RequeustVote RPC 介绍过,要么Term比较大,要么log index比较大

if(lastTermVote>lastTermC||
(lastTermVote==lastTermC)&&lastIndexVote>lastIndexC))

服务器节点变更

通常情况下,集群里面的服务器数量是固定的。但在实际生产中,经常需要扩容,缩容,所以服务器可能不断变化。我们来看下Raft是如何解决的

服务器的配置信息无法直接从旧配置转换为新配置。我们要解决的问题,就是变更期间,同一个任期不能有两个Leader选举胜出。为了保证安全,配置变更采用两阶段的办法。


rafe4.png

Raft引入了一个过渡状态-joint consensus。节点成员的变更,Leader会将C-old和C-new都放在joint consensus(也就是下图的C-old-new),然后作为一个Log发送给其他的Follower。其他Follower收到后,可以马上使用最新的节点数据,不需要等待log被committed。注意,此时只有C-old里面的集群可以进行选举。如果此时Leader down,新选出 的Leader在C-old or C-old-new。 C-new这边的集群不能单边决策


rafe5.png

当进入join consensus之后,Leader会再提交一个新的C-new Log,其他Follower收到这个Log,就可以使用新的配置了。当C-new这个Log被committed,C-old就没有用了,不在C-new的节点可以关闭。基于这套流程,在任何时刻,C-old和C-new不会出现单边投票的情况。

三个问题

  1. 新机器初始化的时候可能没有任何存储日志。如果以这样的状态加入集群,需要花费很长时间才能赶上集群中的其他机器,这个期间机器是不能提交新日志。为了避免不可用的间隙,Raft在配置变更前引入一个新的阶段,类似Learner的角色,不参与投票,只复制日志。
  2. leader可能不是新配置的机器。这种情况下,leader在提交新配置后就降级成Follower
  3. 最后一步删除不在新配置的机器,这些机器可能干扰中断集群。它们收不到心跳,会发起新的选举,导致当前leader回退到Follower。新的leader被选举出来,但是旧机器将会再次超时。这个过程不断重复。为了避免这个问题,servers在确定leader存在的情况下将不理会RequestVote。如果一个机器在最小超时时间下收到当前term的vote,他将不更新自己的term,也不参与本次选举。这样做并不会影响正常的选举。

日志压缩

随着集群的运行,机器的日志将会不断增长。为了保证集群的可用性,需要额外的机制来删除日志中过期的日志。快照是最简单的压缩方法。

每个机器独立进行快照,快照只需要覆盖已经提交的日志。更多的工作是状态机将自己的当前状态写入快照中。Raft中包含了少量的metadata在快照中:

这些被持久化用来支持AppendEntries的日志一致性检测,如前面提过,日志的一致性检测需要前一条日志的Term和索引。为了支持集群成员变更,快照也包含了最后一条被快照取代的日志所使用的配置。一旦机器完成快照,它将可以删除last included index 之前的所有日志,也包括之前的快照。

正常情况下,机器都是单独的进行快照。但是在某些情况下,leader也会发送快照到那些落后的机器上。发送快照可以是分段发送的,带上偏移量。具体的过程如下所示。

InstallSnapshot RPC

参数

返回值

实现过程

//太旧的Term,说明这个Leader已经失效了
if(term<currentTerm){
    return false
}
if(偏移量==0){
    创建新的快照文件
}
在快照文件的指定偏移量写入数据
if(!done){
    return 响应 等待更多的块数据
}
保存快照文件,删除快照之前的日志
如果存在快照点之后的日志文件,保留这些日志并重放
删除整个日志文件
使用快照的状态来重置状态机,并加载快照中的配置

线性读

我们之前讲过,client的每个操作,leader都要封装成一个log,复制到所有的节点。如果只是读操作,我们其实是可以提高性能,不进行日志复制的。但是要避免返回脏数据的风险。因为Leader节点响应client请求时可能已经故障(或者是发送了网络分区),集群中已经选出了新的Leader,但是此时旧Leader自己还不知道。

Raft在处理只读请求,除了直接读取Leader节点的状态信息数据,还需要通过一些额外的操作:

  1. Leader节点必须有关于已经提交日志的最新信息。Leader在任期开始时,会提交一条空白的log,这样确保上一个term的所有log都会被提交。此时Leader拥有所有已经被committed的log
  2. Leader会记录只读请求对应的编号readIndex,当Leader提交的位置committedIndex达到或者超过该位置后,即可响应只读请求
  3. Leader在处理只读请求之前必须检查集群中是否有新的Leader。在处理只读请求之前,让Leader节点可以和半数以上的节点交换一次心跳。如果成功,Leader依然是最新的,readerIndex值也是整个集群中所有节点能看到的最大提交位置commitIndex。
  4. 随着日志记录不断提交,Leader节点的提交位置commitIndex最终会超过上述readIndex,此时Leader就可以响应client的只读请求了。

开源项目

  1. BRaft 百度开源的c/c++实现的raft框架
  2. JRaft 蚂蚁金服开源的Java实现的raft框架,参考了BRaft
  3. Etcd 内部采用raft协议作为一致性算法,基于Go语言实现。

参考资料

  1. 《Raft论文》 (我的这篇笔记其实只是这篇伟大论文的拙劣复述)
  2. 《Raft User Study》 (简洁易懂)
  3. 《JRaft 官方文档》。(里面的原理讲得很好,后续我将写点源码笔记)
  4. 《etc技术内幕》

后记

哪里有恐惧,哪里就有爱。--《霍乱时期的爱情》2020.2.29

上一篇下一篇

猜你喜欢

热点阅读