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

简介

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

基本原理

Server状态

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

  • Leader 每个集群只能有一个,处理所有client的请求,日志复制。
  • Follower 普通的Server,完全被动的,只会对收到的消息进行回应。
  • Candidate 候选人,当进入新一轮选举的时候,所有的候选人都会发起投票。重新选出一个新leader

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

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。包括以下数据:

  • term Leader收到log时的term
  • index log下标。log存储结构是一个List
  • command 操作指令

Persistent State 持久化状态

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

  • currentTerm 当前Term,启动的时候是0
  • voteFor 当前term收到的投票
  • log[] 日志数据

选举过程

当一个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

参数

  • candidateId 投票的candidate
  • term candidate当前的term
  • lastLogIndex 上一个Log的index
  • lastLogTerm 上一个Log的term

返回值

  • term 当前term
  • voteGranted 如果是true表示赞同投票

实现过程

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

日志复制

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

  • client向Leader发送command
  • Leader 将command增加到log[]
  • Leader 向所有的Follower发送AppendEntry RPC
  • 如果有大多数(大于一半)的请求有响应,Leader将command提交到自己的状态机,然后返回消息给client。
  • Follower 也将command提交到自己的状态机。
  • 如果Follower crash或者很慢响应,那么Leader会继续重试直到成功

AppendEntries RPC

参数

  • term Leader的当前Term
  • leaderId Follower可以重新和新Leader交互
  • preLogIndex 倒数第二个Log Index
  • preLogTerm 倒数第二个Log的Term
  • entries[] 存储的log entry
  • commitIndex 即将commit的的entry

返回值

  • term 当前term,用于Leader更新自己的(如果Leader发现自己的是旧的,就会退回Follower)
  • succes 是否成功,如果preLogIndex和preLogTerm在本地能找到

实现过程

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

异常情况

Log一致性

  • 如果在不同的Server 有相同的index和term那么表明:
    • 他们存储相同的Command
    • 在这个log之前的所有entry也是相等的
  • 如果一个指定的entry被committed,那么在它之前的所有entry也是committed

基于上面的论证,每次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在快照中:

  • last included index 日志中最后一条被快照取代的日志的索引(也是状态机应用的上一条日志的索引)
  • last included term 这条日志对应的Term

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

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

InstallSnapshot RPC

参数

  • term Leader的当前Term
  • leaderId Follower可以重定向到Leader
  • lastIncludedIndex 快照替换的最后一条日志记录的索引
  • lastIncludedTerm 快照替换的最后一条日志记录的索引的Term
  • offset 快照中的字节偏移量
  • data[] 数据块,从偏移量开始存储
  • done 是否最后一个块文件

返回值

  • term 当前term,用于Leader更新自己

实现过程

//太旧的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容