前面一篇文章讲了Paxos协议,这篇文章讲它的姊妹篇Raft协议,相对于Paxos协议,Raft协议更为简单,也更容易工程实现。有关Raft协议和工程实现能够参考这个连接https://raft.github.io/,里面包含了大量的论文,视屏已经动画演示,很是有助于理解协议。
概念与术语
leader:领导者,提供客户提供服务(生成写日志)的节点,任什么时候候raft系统中只能有一个leader。
follower:跟随者,被动接受请求的节点,不会发送任何请求,只会响应来自leader或者candidate的请求。若是接受到客户请求,会转发给leader。
candidate:候选人,选举过程当中产生,follower在超时时间内没有收到leader的心跳或者日志,则切换到candidate状态,进入选举流程。
termId:任期号,时间被划分红一个个任期,每次选举后都会产生一个新的termId,一个任期内只有一个leader。termId至关于paxos的proposalId。
RequestVote:请求投票,candidate在选举过程当中发起,收到quorum(多数派)响应后,成为leader。
AppendEntries:附加日志,leader发送日志和心跳的机制
election timeout:选举超时,若是follower在一段时间内没有收到任何消息(追加日志或者心跳),就是选举超时。
Raft协议主要包括三部分,leader选举,日志复制和成员变动。git
Raft协议的原则和特色
a.系统中有一个leader,全部的请求都交由leader处理,leader发起同步请求,当多数派响应后才返回客户端。
b.leader历来不修改自身的日志,只作追加操做
c.日志只从leader流向follower,leader中包含了全部已经提交的日志
d.若是日志在某个term中达成了多数派,则之后的任期中日志必定会存在
e.若是某个节点在某个(term,index)应用了日志,则在相同的位置,其它节点必定会应用相同的日志。
f.不依赖各个节点物理时序保证一致性,经过逻辑递增的term-id和log-id保证。
e.可用性:只要有大多数机器可运行并可相互通讯,就能够保证可用,好比5节点的系统能够容忍2节点失效。
f.容易理解:相对于Paxos协议实现逻辑清晰容易理解,而且有不少工程实现,而Paxos则难以理解,也没有工程实现。
g.主要实现包括3部分:leader选举,日志复制,复制快照和成员变动;日志类型包括:选举投票,追加日志(心跳),复制快照github
leader选举流程
关键词:随机超时,FIFO
服务器启动时初始状态都是follower,若是在超时时间内没有收到leader发送的心跳包,则进入candidate状态进行选举,服务器启动时和leader挂掉时处理同样。为了不选票瓜分的状况,好比5个节点ABCDE,leader A 挂掉后,还剩4个节点,raft协议约定,每一个服务器在一个term只能投一张票,假设B,D分别有最新的日志,且同时发起选举投票,则可能出现B和D分别获得2张票的状况,若是这样都得不到大多数确认,没法选出leader。为了不这种状况发生,raft利用随机超时机制避免选票瓜分状况。选举超时时间从一个固定的区间随机选择,因为每一个服务器的超时时间不一样,则leader挂掉后,超时时间最短且拥有最多日志的follower最早开始选主,并成为leader。一旦candidate成为leader,就会向其余服务器发送心跳包阻止新一轮的选举开始。服务器
发送日志信息:(term,candidateId,lastLogTerm,lastLogIndex)
candidate流程:
1.在超时时间内没有收到leader的日志(包括心跳)
2.将状态切换为candidate,自增currentTerm,设置超时时间
3.向全部节点广播选举请求,等待响应,可能会有如下三种状况:
(1).若是收到多数派回应,则成为leader
(2).若是收到leader的心跳,且leader的term>=currentTerm,则本身切换为follower状态,
不然,保持Candidate身份
(3).若是在超时时间内没有达成多数派,也没有收到leader心跳,则极可能选票被瓜分,则会自增currentTerm,进行新一轮的选举网络
follower流程:
1.若是term < currentTerm,说明有更新的term,返回给candidate。
2.若是尚未投票,或者candidateId的日志(lastLogTerm,lastLogIndex)和本地日志同样或更新,则投票给它。
注意:一个term周期内,每一个节点最多只能投一张票,按照先来先到原则动画
日志复制流程
关键词:日志连续一致性,多数派,leader日志不变动
leader向follower发送日志时,会顺带邻近的前一条日志,follwer接收日志时,会在相同任期号和索引位置找前一条日志,若是存在且匹配,则接收日志;不然拒绝,leader会减小日志索引位置并进行重试,直到某个位置与follower达成一致。而后follower删除索引后的全部日志,并追加leader发送的日志,一旦日志追加成功,则follower和leader的全部日志就保持一致。只有在多数派的follower都响应接受到日志后,表示事务能够提交,才能返回客户端提交成功。
发送日志信息:(term,leaderId,prevLogIndex,prevLogTerm,leaderCommitIndex)
leader流程:
1.接收到client请求,本地持久化日志
2.将日志发往各个节点
3.若是达成多数派,再commit,返回给client。
备注:
(1).若是传递给follower的lastLogIndex>=nextIndex,则从nextIndex继续传递
.若是返回成功,则更新follower对应的nextIndex和matchIndex
.若是失败,则表示follower还差更多的日志,则递减nextIndex,重试
(2).若是存在N>commitIndex,且多数派matchIndex[i]>=N, 且log[N].term == currentTerm,
设置commitIndex=N。spa
follower处理流程:
1.比较term号和自身的currentTerm,若是term<currentTerm,则返回false
2.若是(prevLogIndex,prevLogTerm)不存在,说明还差日志,返回false
3.若是(prevLogIndex,prevLogTerm)与已有的日志冲突,则以leader为准,删除自身的日志
4.将leader传过来的日志追加到末尾
5.若是leaderCommitIndex>commitIndex,说明是新的提交位点,回放日志,设置commitIndex =
min(leaderCommitIndex, index of last new entry)线程
备注:默认状况下,若是日志不匹配,会按logIndex逐条往前推动,直到找到match的位置,有一个简单的思路是,每次往前推动一个term,这样能够减小了网络交互,尽快早点match的位置,代价是可能传递了一些多余的日志。设计
快照流程
避免日志占满磁盘空间,须要按期对日志进行清理,在清理前须要作快照,这样新加入的节点能够经过快照+日志恢复。
快照属性:
1.最后一个已经提交的日志(termId,logIndex)
2.新的快照生成后,能够删除以前的日志和之前的快照。
删日志不能太快,不然,crash后的机器,原本能够经过日志恢复,若是日志不存在,须要经过快照恢复,比较慢。日志
leader发送快照流程
传递参数(leaderTermId, lastIndex, lastTerm, offset, data[], done_flag)
1.若是发现日志落后太远(超过阀值),则触发发送快照流程
备注:快照不能太频繁,不然会致使磁盘IO压力较大;但也须要按期作,清理非必要的日志,缓解日志的空间压力,另外能够提升follower追赶的速度。blog
follower接收快照流程
1.若是leaderTermId<currentTerm, 则返回
2.若是是第一个块,建立快照
3.在指定的偏移,将数据写入快照
4.若是不是最后一块,等待更多的块
5.接收完毕后,丢掉之前旧的快照
6.删除掉不须要的日志
集群配置变动
C(old): 旧配置
C(new): 新配置
C(old-new): 过渡配置,须要同时在old和new中达成多数派才行
原则:配置变动过程当中,不会致使出现两个leader
二阶段方案:引入过渡阶段C(old-new)
约定:任何一个follower在收到新的配置后,就采用新的配置肯定多数派。
变动流程:
1.leader收到从C(old)切换到C(new)配置的请求
2.建立配置日志C(old-new),这条日志须要在C(old)和C(new)中同时达成多数派
3.任何一个follower收到配置后,采用的C(old-new)来肯定日志是否达成多数派(即便C(old-new)这条日志还没达成多数派)
备注:1,2,3阶段只有可能C(old)节点成为leader,由于C(old-new)没有可能成为多数派。
4.C(old-new)日志commit(达成多数派),则不管是C(old)仍是C(new)都没法单独达成多数派,即不会存在两个leader
5.建立配置配置日志C(new),广播到全部节点
6.一样的,任何一个follower收到配置后,采用的C(new)来肯定日志是否达成多数派
备注:在4,5,6阶段,只有可能含有C(old-new)配置的节点成为leader。
7.C(new)配置日志commit后,则C(old-new)没法再达成多数派
8.对于不在C(new)配置的节点,就能够退出了,变动完成。
备注:在7,8阶段,只有可能含有C(new)配置成为leader。
因此整个过程当中永远只会有一个leader。对于leader不在C(new)配置的状况,须要在C(new)日志提交后,自动关闭。
除了上述的两阶段方案,后续Raft做者又提出了一个相对简单的一阶段方案,每次只添加或者删除一个节点,这样设计不用引入过渡状态,这里再也不赘述,有兴趣的同窗能够去看他的毕业论文,我会附在后面的参考文档里面。
Q&A
1.Raft协议中是否存在“活锁”,如何解决?
活锁是相对死锁而言,所谓死锁,就是两个或多个线程相互锁等待,致使都没法推动的状况,而活锁则是多个工做线程(节点)都在运转,可是总体系统的状态没法推动,好比basic-paxos中某些状况下投票老是没有办法达成多数派。在Raft中,因为只要一阶段提交(只有leader提议),在日志复制的过程当中不存在活锁问题。可是选主过程当中,可能出现多个节点同时发起选主的状况,这样致使选票瓜分,没法选出主,在下一轮选举中依旧如此,致使系统状态没法往前推动。Raft经过随机超时解决这个“活锁”问题。
2.Raft系统对于各个节点的物理时钟强一致有要求吗?
Raft协议对物理是时钟一致性没有要求,不须要经过原子钟NTP来校准时间,可是对于超时时间的设置有要求,具体规则以下:
broadcastTime ≪ electionTimeout ≪ MTBF(Mean Time Between Failure)
首先,广播时间要远小于选举超时时间,leader经过广播不间断给follower发送心跳,若是这个时间比超时时间短,就会致使follower误觉得leader挂了,触发选主;而后是超时时间要远小于机器的平均故障时间,若是MTBF比超时时间还短,则永远会发生选主问题,而在选主完成以前,没法对外正常提供服务,所以须要保证。通常broadcastTime能够认为是一个网络RTT,同城1ms之内,异地100ms之内,若是是跨国家,可能须要几百ms;而机器平均故障时间至少是以月为单位,所以选举超时时间须要设置1s到5s左右便可。
3.如何保证leader上拥有了全部日志?
一方面,对于leader不变场景,日志只能从leader流向follower,而且发生冲突时以leader的日志为准;另外一方面,对于leader一直有变换的场景,经过选举机制来保证,选举时采用(LogTerm,LogIndex)谁更新的比对方式,而且要获得多数派的承认,说明新leader的日志至少是多数派中最新的,另外一方面,提交的日志必定也是达成了多数派,因此推断出leader有全部已提交的日志,不会漏。
4.Raft协议为何须要日志连续性,日志连续性有有什么优点和劣势?
由Raft协议的选主过程可知,(termId,logId)必定在多数派中最新才可能成为leader,也就是说leader中必定已经包含了全部已经提交的日志。因此leader不须要从其它follower节点获取日志,保证了日志永远只从leader流向follower,简化了逻辑。但缺陷在于,任何一个follower在接受日志前,都须要接受以前的全部日志,而且只有追遇上了,才能有投票权利【不然,复制日志时,不考虑它们是大多数】,若是日志差的比较多,就会致使follower须要较长的时间追赶。任何一个没有追上最新日志的follower,没有投票权利,致使网络比较差的状况下,不容易达成多数派。而Paxos则容许日志有“空洞”,对网络抖动的容忍性更好,但处理“空洞”的逻辑比较复杂。
5.Raft如何保证日志连续性?
leader向follower发送日志时,会顺带邻近的前一条日志,follwer接受日志时,会在相同任期号和索引位置找前一条日志,若是存在且匹配,则接受日志,不然拒绝接受,leader会减小日志索引位置并进行重试,直到某个位置与follower达成一致。而后follower删除索引后的全部日志,并追加leader发送的日志,一旦日志追加成功,则follower和leader的全部日志就保持一致。而Paxos协议没有日志连续性要求,能够乱序确认。
6.若是TermId小的先达成多数派,TermId大的怎么办?可能吗?
若是TermId小的达成了多数派,则说明TermId大的节点之前是leader,拥有最多的日志,可是没有达成多数派,所以它的日志能够被覆盖。但该节点会尝试继续投票,新leader发送日志给该节点,若是leader发现返回的termT>currentTerm,且尚未达成多数派,则从新变为follower,促使TermId更大的节点成为leader。但并不保证拥有较大termId的节点必定会成为leader,由于leader是优先判断是否达成多数派,若是已经达成多数派了,则继续为leader。
7.达成多数派的日志就必定认为是提交的?
不必定,必定是在current_term内产生的日志,而且达成多数派才能认为是提交的,持久化的,不会变的。Raft中,leader保持原始日志的termId不变,任何一条日志,都有termId和logIndex属性。在leader频繁变动的状况下,有可能出现某条日志在某种状态下达成了多数派,但日志最终可能被覆盖掉,好比下图:
(a).S1是leader,termId是2,写了一条日志到S1和S2,(termId,logIndex)为(2,2)
(b).S1 crash,S5利用S3,S4,S5当选leader,自增termId为3,本地写入一条日志,(termId,logIndex)为(3,2)
(c).S5 crash,S1 重启后从新当选leader,自增termId为4,将(2,2)从新复制到多数派,提交前crash
(d).S1 crash,S5利用S2,S3,S4当选leader,则将(3,2)的日志从新复制到多数派,并提交,这样(2,2)这条日志曾经虽然达成多数派也会被覆盖。
(e).假设S1在第一个任期内,将(2,2)达成多数派,则后面S3不会成为leader,也就不会出现覆盖的状况。
参考文档
https://raft.github.io/raft.pdf
https://ramcloud.stanford.edu/~ongaro/thesis.pdf
https://ramcloud.stanford.edu/~ongaro/userstudy/paxos.pdf