Learning Material
A website that contains the visualization, implementation and courses materials
a simple visualization of raft(friendly to newcomer)
A github repo about raft (which is in MIT6.824)
Raft 总结博客
tbc…
Raft
前言
- 什么是分布式一致性?
首先,在一个集群里,为了防止数据丢失或者机器故障导致服务系统挂掉,一般会把同一份数据记录在不同的server上,那如何保证多台机器上记录的数据都是相同的呢?
- 我们可以把server假设为状态机,对于状态机来说,当输入确定的时候,输出也会随之确定.因此如果多台状态机以相同的顺序执行相同指令,那么他们达到的状态就是一致的.
所以现在来解决问题:什么是Raft算法?
Raft算法是用来管理日志一致性的算法.log代表记录任务的日志,entries代表需要执行的任务或者指令,保存在log中.
那么,只要保证日志的一致性,就能够保证不同主机执行的指令以及执行指令的顺序是相同的.那么进而保证了状态机的一致性.
三种角色
任意时刻,一个node会处在三种状态中的一种:leader,follower与candidate
- leader : 顾名思义,一个集群中,有且只有一个leader,是集群中唯一能够与客户端通信的server,所有client发起的任务都必须经过leader调度
- candidate: 当集群中leader宕机或者集群刚开始运行的时候,follower可以把自身状态调整为candidate然后竞选leader在选举中可能成为leader的状态
- follower: 只能被动的响应leader或者参与者的请求
角色转换规则:
- 集群中所有的server刚开启时候都是follower状态
- 每个follower随机化一个election timeout,如果在这个时间内没有收到来自leader的信息,则转为candidate向其他机器发送竞选请求
- 一个candidate如果收到超过半数节点,就成为leader,如果集群中存在leader或者收到更高任期的请求,则会转换为follower
- 如果出现网络分区,导致每个集群存在多个leader,则网络分区恢复后,任期较低的leader转为follower
Raft一致性
-
首先选择一个确定的leader,然后leader负责管理日志的复制
-
leader接收来自客户端的请求,然后追加到leader本地日志
-
leader负责把日志复制到其他机器,并且告诉其他机器什么时候可以把日志安全地应用到状态机
-
集群存在一个leader的好处是可以简化日志复制的管理,整个集群的数据流向都是从leader到其他机器的.使用term概念来标记leader顺序,一个term中最多有一个leader,也有可能没有leader
-
一旦leader宕机或者网络断开,其他机器可以重新选举一个新的leader
可以breakdown成三个问题:
-
leader election:当一个leader宕机之后,一个新的leader必须被选举
-
log replication leader必须响应客户端的请求,并且把日志复制到整个集群,来保证其他机器的日志与leader相同
-
safety 状态机的安全是Raft优先保证的 选举的时候要增加额外的规则约束
如果有任何节点已经应用了一个特定的日志条目到它的状态机中,那么其他节点不能在同一个日志索引位置应用一条不同的指令
Raft 新的特性
-
强领导者: 日志条目只从领导者发送到其他服务器
-
领导选取: Raft使用了随机定时器来选择leader
所有节点都有一个计时器,计时完毕的节点向其他所有节点发起请求投票的RPC,选票过半就可以成为leader.
注意每一轮所有节点只需要投票一次,来回应第一个发起询问的节点(也就是第一个计时完成的节点)
然后其他节点通过接收主节点的AppendEntries和心跳信息来得知leader的获胜与存活
- 成员变化: (Membership Change) 集群中的机器发生变更,整个集群也可以正常工作.
上述五条规则
- 选举安全原则
一个任期最多只能选出一个leader - 领导人只增加原则
leader不能重写或者删除日志,只能够追加日志记录 - 日志匹配原则
如果两个日志文件存在相同的索引和任期的日志记录,那么两个日志文件所有的日志记录在给定索引情况下是相同的. - 领导人完全原则
如果一条日志在一个给定的任期已经提交,那么这条日志将会出现在所有任期大于给定任期leader的日志当中 - 状态机安全原则
如果一个server已经将给定索引的日志应用到状态机,别的server将不能应用一个相同索引但是内容不同的日志记录到自己的状态机中
任期制度
Raft把时间分割为任意长度,每个间隔是一个任期,每个任期由一个连续的整数进行表示.
每个任期都是从选举开始的.在这个阶段会有一个或者多个candidate参与竞选,一旦有candidate得到超过半数选票,这个任期剩下的时间就由这个candidate作为leader
特殊情况下,一次选举可能会出现选举分裂,就是选出了两个以上leader的情况这时候当前任期不会选举出真正的leader,然后会超时知道新任期开启,重新启动选举.
任期过期的情况
- 如果一个节点的存储任期小于其他机器存储的任期,那么它将更新自己的任期到其他机器存储的最大任期
- 如果一个candidate或者leader发现自己的任期已经过期,会转变到参与者的状态
- 如果一个server接收到一个请求,这个请求中任期是过时的,那么这个请求会被拒绝
节点间的RPC通信
实现一致性算法只需要两种类型的RPC
- 请求投票RPC,由candidate在选举期间发起
- 追加条目RPC 由leader发起,用来复制日志和提供心跳机制
- 传输snapshot的RPC
当服务器没有及时收到RPC响应的时候,会进行重试,并且发送RPC是并行的
Leader选举
Raft使用心跳机制来触发leader选举,当服务器程序启动的时候,他们都是follower.
一个服务器节点只要能从leader或者candidate接收到有效的RPC,就可以抑制保持follower状态.Leader周期性向所有follower发送心跳,来维持自己的地位
如果一个follower在一段选举超时时间内没有接收到任何消息,就假设系统中没有可用的leader,然后就会开始选举,然后选出新的leader
选举详细过程:
-
要开始一次选举过程,follower先增加自己的当前term号,然后转换到candidate状态
-
然后投票给自己,并且并行地向集群中的其他服务器节点发送RequestVote RPC,就是发起选举
-
会有三种可能的结果:
- 自己赢得了选举
对于同一个任期,每个服务器节点只会投给一个candidate,按照先来先服务,就是谁的信号先到达我这,我就投给谁
一旦candidate赢得了选举,就会向其他服务器发送心跳信息,进而确保自己的定位并且阻止新的选举
- 其他服务器节点成为了leader
在投票的过程中,candidate可能会收到另外一个声称自己是leader的服务器节点发来的RPC,如果这个新的leader的term号大于等于当前candidate的任期号,那么candidate会承认这个leader的合法地位然后回到folower状态
如果RPC任期号比自己的小,那么candidate会拒绝这次的rpc,(不承认选举结果),然后保持candidate
- 一段时间之后没有任何获胜者(选举分裂)
没有candidate赢得过半投票,这时候每一个候选人都会超时,然后通过增加当前的任期号来开始新一轮的选举
Raft使用随机选举超时时间的方法来确保很少发生选票瓜分的情况
每个节点通过在一个固定的区间随机选择超时时间,这样很大几率情况下只有一台服务器会选举超时.
每个candidate在开始新的选举的时候会重置一个随机的选举超时时间,然后一直等待直到选举超时.这样减少了在新的选举中选票瓜分的可能性.
总结所有节点的执行规则:
所有节点:
- commitIndex > lastApplied,应用log[lastApplied]到状态机,增加lastApplied。先commit,后执行,2次交互,方便回滚
- 如果reply包含的任期T>currentTerm,将currentTerm设置为T并且转换为Follower。一般是网络分区恢复,低任期的leader退化为follower
Follower:
- 被动接受来自leader与candidate的RPC请求
- 在选举超时周期内没有收到AppendEntries或者给candidate投票,自己进化为candidate,发起投票请求
Candidate:
- 选举超时,follower把自己转换为candidate
1.递增currentTerm
2.给自己投票
3.重置选举超时时间
4.发送RequestVote给其他节点 - 收到超过半数的选票,转化为leader
- 如果收到Leader的AppendEntries请求,转换为follower
- 选举超时就重新开始选举
Leader:
- 一旦选举完成,发送心跳给所有节点,在空闲周期内不断发送心跳以维持leader身份
- 收到客户端的请求,将日志追加到本地log,日志被应用到状态机后响应给客户端
- 如果对于一个follower,其最后的日志条目的索引大于等于nextIndex,那么发送从nextIndex开始的所有日志条目:
1.成功的话,更新follower的nextIndex与matchIndex
if curCommitLen >= rf.matchIndex[server] {
rf.matchIndex[server] = curCommitLen
rf.nextIndex[server] = rf.matchIndex[server] + 1
}
2.如果日志不一致,减少nextIndex重试
// 日志不匹配
rf.nextIndex[server] = rf.addIdx(1)
i := reply.ConflictIndex
for ; i > rf.lastIncludedIndex; i-- {
// 回退到冲突的任期
if rf.logs[rf.subIdx(i)].Term == reply.ConflictTerm {
rf.nextIndex[server] = i + 1
break
}
}
3.如果存在一个满足N>commitIndex的N,并且超半数的matchIndex[i] >= N,而且log[N].term == currentTerm,这时候默认这个N为已经提交的最新的index,设置commitIndex为N(一旦创建日志条目的leader把该项条目复制到超过半数的机器上,这个日志条目就会被提交.)
if commitFlag && commitNum > len(rf.peers)/2 {
//超过半数的commit
commitFlag = false
rf.commitIndex = curCommitLen
rf.applyLogs()
}
Log replication 日志复制机制
请求流程:
1.leader将本次修改同步到Follower,注意这个时候修改未写入leader的log,也就是还没有提交
2.follower接收到leader的修改之后,将修改写入本次的log,给leader发送commit信号,但是自己还没有执行
3.当leader接收到超过半数的follower的回复,就会认为大部分follower同步了这条指令,把这条指令写入leader的log
4.leader把这次修改的commit给client,表明已经收到了这次修改
5.leader再次给所有follower发送信息,表明这次修改已经commit
6.这个时候follower接收到leader的信息,将会执行修改
总结:
Raft 协议强依赖 Leader 节点的可用性来确保集群数据的一致性。数据的流向只能从 Leader 节点向 Follower 节点转移。当 Client 向集群 Leader 节点提交数据后,Leader 节点接收到的数据处于未提交状态(Uncommitted),接着 Leader 节点会并发向所有 Follower 节点复制数据并等待接收响应,确保至少集群中超过半数节点已接收到数据后再向 Client 确认数据已接收。一旦向 Client 发出数据接收 Ack 响应后,表明此时数据状态进入已提交(Committed),Leader 节点再向 Follower 节点发通知告知该数据状态已提交。
以下摘抄自论文
- 客户端是和leader直接交互的
- 客户端每一个请求都包含一条将被复制状态机执行的指令
- leader把这个指令作为一个新的条目追加到日志当中,然后并行的发起AppendEntries RPC给其他的服务器,让他们复制这个条目
- 当这个条目被安全的复制的时候,leader会应用这个条目到其状态机中,然后把执行的结果返回给客户端.
- 如果follower奔溃或者运行缓慢,或者网络丢包,leader会不断重试AppendEntries RPC,直到所有的follower最终都存储了所有的日志条目
每一个日志条目会存储一条状态机指令和leader收到这个指令的时候的任期号
任期号会用来检测多个日志副本之间的不一致情况
leader会决定什么时候把日志条目应用到状态机中是安全的.这种日志条目称为已提交的.
Raft算法保证所有已经提交的日志条目都是持久化的,而且最终会被所有的可用的状态机执行.
一旦创建日志条目的leader把该项条目复制到超过半数的机器上,这个日志条目就会被提交.
如上图的条目1-7,都超过了半数
同时leader日志中该日志条目之前的所有日志条目都会被提交,包括由其他leader创建的条目
follower一旦知道某个日志条目已经被提交,就会把这个日志条目应用到自己的本地状态机中
-
如果不同的日志中的两个条目拥有相同的索引与任期号,说明他们存储了相同的指令,而且之前的所有日志条目都相同.
-
Leader在特定的任期号内的一个日志索引处最多只能创建一个日志条目
-
leader在发送AppendEntries RPC的时候,leader会把前一个日志条目的索引位置和任期号包含在里面
-
如果follower在其日志中找不到包含相同的索引位置和任期号的条目,那么就会拒绝更新这个新的日志条目,这就是一致性检查,每当RPC返回成功的时候,leader就会知道follower的日志与自己是同步的
-
只有当leader或者follower崩溃的时候,会使得日志处于不一致的状态.
图 7:当一个 leader 成功当选时(最上面那条日志),follower 可能是(a-f)中的任何情况。每一个盒子表示一个日志条目;里面的数字表示任期号。Follower 可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。例如,场景 f 可能这样发生,f 对应的服务器在任期 2 的时候是 leader ,追加了一些日志条目到自己的日志中,一条都还没提交(commit)就崩溃了;该服务器很快重启,在任期 3 重新被选为 leader,又追加了一些日志条目到自己的日志中;在这些任期 2 和任期 3 中的日志都还没被提交之前,该服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。
- 在Raft 算法中, leader通过强制follower复制leader的日志来保证日志数据的一致
- leader因此要找到本身与candidate的longest common index,即最后一个相同的日志
- 然后删除folower日志中从那个点之后的所有日志条目,然后将自己从那个点之后的所有日志条目发给follower
- 上述的操作发生在对AppendEntries RPCs中一致性检查的回复中
- 因此leader针对每一个follower都维护了nextIndex,表示leader要发送给follower的下一个日志条目的索引
- 当选出一个新的leader的时候,这个leader将所有的nextIndex的值都初始化为自己最后一个日志条目的index加1
- 如果follower拒绝了leader的AppendEntries RPC, leader就会减少nextIndex值,并且重试AppendEntries RPC.
- 重试成功之后,follower就会删除与leader冲突的日志条目,
- 一旦成功,follower日志就会与leader保持一致,并且在这个任期接下来时间都会保持一致
总结:
-
日志能够在回复AppendEntries一致性检查失败的时候自动趋于一致 而不需要额外的操作
-
新的日志条目可以在一个RPC来回中复制给集群的过半机器,并且单个运行慢的follower不会影响整体的性能
-
leader同步follower日志的方法是,把自己的log强制覆盖到follower的log,但并不是把leader所有的log都发过去,因为时间久了log可能会非常大,传输log会占很多带宽。正确做法是,找出leader与follower最后一个相同的log,然后用leader这个条目之后的log去覆盖follower这个条目之后的所有log
安全性保证
选举限制
-
RPC包含了 candidate的日志信息,如果投票者自己的日志比candidate的新,就会拒绝掉该投票请求
-
除非candidate包含了所有已经提交的日志条目,否则raft会通过投票机制阻止candidate赢得选举
那怎么保证这种raft特色的民主制度选出来的大大就一定包含了所有已经提交的日志条目。
- 首先candidate需要收到超过半数节点选票来成为leader
- 然后已经提交的日志条目至少存在与超过半数的节点
- 上面两种情况的节点集合分别记做A,B那么A,B必定有交集,由于follower只会投票给比自己新的Candidate,那么被选出的节点的日志一定包含了交集中节点已经commit的日志。
raft通过比较last index 与 last term来定义谁的日志比较新,
- 如果任期号不同,任期号的大的更新
- 如果任期号相同,那么last index 更大的会更新
日志压缩&快照技术
实际系统中,日志不能无限制增长.
随着日志越来越长,会占用越来越多的空间,并且需要更多的时间来回放.所以需要一个机制来清楚日志中的过期信息,但这会引起可用性的问题
SnapShot 技术
用于压缩日志,整个系统状态都以快照的形式持久化到稳定存储中,这个时间点之前的日志都会被丢弃.
如果这个时候又要创建一个snapshot.如何计算lastIndex和lastTerm?
- lastIndex = log.size + snapshotIndex -1
- lastterm = logs[lastIndex - snapshotIndex].term //logs 索引从0开始,实际上是第 lastIndex-snapshotIndex+1项
快照是怎么回事?
- 用来替代了日志中所有已经提交了的条目,这个快照只存储了当前的状态,如上图中当前x为0,y为9
- 快照记录 last included index 与 last included term 用来定位日志中条目6之前的快照
last inclued index 是最后一个被快照取代的日志条目的索引值
last included term 是该条目的任期号
保留这些元数据是为了支持快照后的第一个条目的AppendEntries一致性检查
一旦服务器完成了写快照,就可以删除last included index之前的所有日志条目,包括之前的快照
如果一个运行的缓慢的follwer或者一个新的节点加入集群,那么leader将会通过网络把snapshot发送给这个follower,使其更新到最新状态
follower收到snapshot的策略
- 如果快照包含接受者日志中没有的信息,follower会丢弃它所有的日志,然后日志会被快照所取代
- 如果接收到的快照是自己的日志的前面部分,那么被快照包含的条目将全部删除
成员变更部分
先不介绍了,这里比较复杂,感兴趣可以看paper
面试专区
这里收集一些在面试的时候对介绍Raft比较好的解答
枚举各种fail over的情况
参考
https://blog.csdn.net/chdhust/article/details/67654512#comments
1.client提交数据,发起修改请求是成功的
2.1 leader向follower发送数据,等待接受回应
3.1&3.2 follower向leader发送确认接受的信息
4.1leader收到超过半数的接受信息,向leader确认数据已经接受。
然后client向leade发送响应,这时候数据就是commited了
4.2 最后leader再向所有follower发送提交数据状态的信息
在上述过程中,leader可能在任意时候挂掉,接下来分析raft针对不同的情况如何保持一致性?
client数据到达leader之前
不影响一致性
数据达到Leader节点,但是没有复制到Follower节点
client收不到确认信息,可以安全重试。由于follower节点上没有该数据,重新选主后client重试就可以提交成功。
原来的leader节点恢复后,收到新leader的AppendEntries,就会从新leader处同步数据。
如果所有follower都接受到修改请求,这时候leader宕机
这时候日志保持一致性,只要重新选出leader即可
但只有部分follower接收到请求
- 如果接收到请求的follower是超过半数
日志不一致,Raft要求只能投票给拥有最新日志的节点,也就是说能被选举成为Leader的节点,一定包含了所有已经提交的日志条目,新选举出来的leader会同步剩余的未修改的follower - 如果接收到的请求的follower是少数
可能是没有接收到请求的follower成为新的leader,此时新的leader会要求接收到请求的follower回滚log。然后新的leader会接收到client的请求,重复执行修改的流程。
已经复制到超过半数的节点,leader也受到了超过半数的回复,处于commited状态
只要重新选举leader,然后和上上图情况一样
数据到达 Leader 节点,成功复制到 Follower 所有或多数节点,数据在所有节点都处于已提交状态,但未响应 Client
日志已经保持一致性,client重试不会影响结果
出现网络分区,也就是部分follower收不到leader的心跳,所以占大部分follower的一团人将“各自为政”,选出新的leader
然后原来的leader在网络恢复后,发现已经有新的leader了,只好退位变为follower
Go 实现Raft MIT6.824 lab 2
大体参照:
https://github.com/sworduo/MIT6.824/blob/master/6.824/src/raft/raft.go
https://github.com/shishujuan/mit6.824-2017-raft
完整代码:
https://github.com/AlexanderChiuluvB/6.824
Raft
每一个Raft server的数据结构定义
//
// A Go object implementing a single Raft peer.
//
// 每个Raft peer都叫做server,然后分为三种角色: leader,ca