文章目录
前言
这篇论文发表于2014年讲述的是raft协议,raft协议是保证数据一致性和fault-tolerance的协议,常用于分布式系统中,大家应该知道设计一个分布式系统,我们首先要考虑fault-tolerance的问题,也就是一个节点down了如何才能保证业务不断,数据不丢失,raft给了非常好的解决方案,大部分解决数据不丢失的手段是通过数据replicated,将数据的副本放在其他的节点上,但是这样又带来另一个问题,也就是数据一致性,数据和其副本因为网络原因,等等导致副本上的数据和主数据不一致(主数据和数据副本不在同一个节点上,需要通过网络传输),raft也规定了一系列的操作保证其数据一致,接下来就是论文的原文翻译总结了
ps:突然发现raft的作者Diego Ongaro竟然早就发布了关于raft一致性算法的讲解视频,还发布了Paxso的讲解视频(raft是paxso的子集)链接如下
raft
paxos
非常值得一看他讲述了作者发明算法的思路,非常有借鉴价值
名词解释:
consensus:这个一致性主要阐述的是一致性的思想,是一个大的概念
consistency:这个一致性主要讲的是分布式数据一致性
abstract
首先raft是一个管理数据副本的一致性算法,raft相当于mulit-paxos(paxos里的术语),raft和Pasox一样高效,但是raft的构建上和pasox不同,就是因为这些不同raft,导致raft比Pasox简单一些,并且也就是这些不懂使得raft为分布式系统生产环境提供了更坚实的基础,raft为了更好理解,将一致性概念中的关键要素分离成,leader选举,log复制,还有安全控制等板块,从用户学习raft的结果来看raft比paxos更简单,并且应对集群中成员变化raft包含更多的新机制,这些机制保证了raft最大的安全性
introduce
一致性算法允许一连串的机器就像一个相关联的组一样运行,这个组中的某些成员挂了,服务还可以照常提供服务,正因为如此一致性算法在构建大型可扩展,可信架构中扮演者关键角色,过去十年(2014年)paxos统治了这一领域(一致性算法),许多现实生产环境中关键的一致性算法,都是基于paxos,或者受他的影响,正因为如此paxos称为教学的主流
非常不幸的是paxos非常复杂,尽管非常多人尝试使其更加简单,因此paxos架构非常复杂,在生产环境中实现paxos和学生们学习paxos都非常困难
在学习实践paxos的时候我们开始寻找一个算法,去提供更好的一个基础对于生产环境和教学来说,我们设计的出发点是易懂,我们想此算法促进相关发展,所以在此我们想给读者呈现的不光是算法如何工作,更多的是算法为什么要工作
所以我们研究的结果就是raft,在raft中我们引入一些特别的技术提高raft的易理解程度,比如一些组件的分解,减少了一些状态空间
raft和许多已经存在了的一致性算法非常像,但是raft又有以下的新功能
- strong leader
名字说的这么流弊,其实就是增加了leader的任务,比如log(应该是数据的备份)必须要经过leader之手发往其他的服务器,但是这一个设计使raft更好理解了
- leader election(leader 选举)
raft使用随机计时器去进行leader的选举,raft只是在大多数一致性算法都有的心跳连接上增加了少量的机制,然后非常快速,简单的解决了冲突
- membership changes
raft使用了一个叫做joint consensus的机制去解决了集群中一些服务器传递了2种不同配置的情况,所以这个机制允许了集群在运行的时候更改配置
我们相信raft比paxos和其他的一致性算法跟牛逼(2014年),不管是教学还是生产环境中,因为他比其他大多是一致性算法更易懂(这个大哥提了N遍…)
本篇paper第二章(下一章)讲了在replicated时候的状态机,第三章介绍了本算法的优点和paxos的缺点,第四章介绍了如何让算法变得简单易懂的,第五章到第八章介绍了raft一致性的算法,第九章主要是评估raft,第十章讲述了raft相关的工作
replicated stat machines
在raft的实现中,状态机是通过一组服务器去计算识别相同状态的备份,尽管一些服务器挂掉了他也可以照常工作,replicated状态机是解决一系列fault-tolerance的问题关键,在分布式系统中,举个例子,在一些大型的分布式系统中GFS,HDFS都只有一个leader,他们典型使用分离的状态机去管理leader的选举,还有leader挂掉的时候必须存储的配置信息,上述关于分离式的状态机包括zookeeper和chubby也是这样设计的
raft是用一组服务器形成一个状态机,这样可以保证状态机的容错性(agreement),状态机可以看我们给的连接,简而言之状态机就是一个机器,接收client的请求,改变自己的状态,输出
带有容错机制的状态机(replicated state machine我以后就这样翻译),通常来说都是通过replicate log 实现,每一个组成状态机的服务器都会存一个日志,每一个日志包含一系列的命令,这些命令都是状态机要执行的(命令还是按需排列好的,状态机执行的命令执行序列极其重要,序列一乱不同状态机输出就不一样,状态也有可能不一样),每一个日志的内容都一样(就连顺序都一样),比如下图
上述的图片使我们更加的了解状态机是个啥,首先每个服务器都有一个状态机,客户端发来改变状态机的请求都先通过一致性模型缓存到log里(按照一定的序列),然后再由log给状态机,client连接了不同服务器的一致性模型,确保了其他的服务器存储的log都是相同的,且顺序也相同,就算一个服务器挂了,也没有事情,
保证replicated log是一致性算法的事情
一致性算法对于生产环境来说必须要有以下几个属性
- 在非拜占庭问题下一致性算法要保证安全性(安全性值得是返回错误结果) ,这里的拜占庭问题值得是网络延迟丢包,log中的command被重新排序
拜占庭帝国想要进攻一个强大的敌人,为此派出了10支军队去包围这个敌人。这个敌人虽不比拜占庭帝国,但也足以抵御5支常规拜占庭军队的同时袭击。这10支军队在分开的包围状态下同时攻击。他们任一支军队单独进攻都毫无胜算,除非有至少6支军队(一半以上)同时袭击才能攻下敌国。他们分散在敌国的四周,依靠通信兵骑马相互通信来协商进攻意向及进攻时间。困扰这些将军的问题是,他们不确定他们中是否有叛徒,叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们才能保证有多于6支军队在同一时间一起发起进攻,从而赢取战斗?
- 集群对外提供的服务是可工作的,这里指的是集群中的大部分机器是可工作的,可相互通讯的(包括和client),一个典型的集群有5个机器,这个集群中任意2台挂了不能影响集群的工作
- 一致性算法不能通过计时去保证上述提到的log的一致性,但是额外的信息延迟和faulty clock却可以
- 在对通常的情况下,一个命令要快速的在集群大多数机器回复单个rpc的时候完成,集群中的少数机器不会因为慢而影响系统性能
what’s wrong with Paxos
paxos一共有2大缺点,第一个就是太难了,大多数人不能理解,因为他不透明,除非你费老大劲才行,2012年NSDI的一项调查,只有非常少的人才能驾驭paxos,甚至是一些研究学者,
Designing for understandability
我们在设计raft的时候有多个出发点,比如对于生产环境提供一个完整且实践效果好的基石,所以他显著的减少了开发者的使用难度,他还要在所有的操作条件下保证安全,在普通的操作下他必须保证高效,当然最主要的目标还是要易懂,一定要大部分人看的清清爽爽,还必须要读者对这个算法有主观能动性
在raft设计的时候我们必须在几个来回替换的点子中选择,在这个情况下我们出于易懂性出发来评估这些点子
我们认识到一些分析有着高度主观性,尽管如此我们还是用2种常用的方法,第一种方法是将问题切分成多个可以容易被解决,容易被弄懂,还有一定相关独立性的小问题,
The Raft consensus algorithm
还是先说一下,raft是一个管理冗余日志(replicated log)的一个算法,raft如何保证冗余日志一致性的呢?通过第一次选举leader,这个leader负责管理replicated log,leader从其他的client中获取log,然后复制他到其他服务器上,并且会告诉服务,何使你才能将日志放到状态机中,leader可以决定替换log条目,而不通知其他的服务器,数据流方向是从leader到其他服务器,
在设计leader的时候raft将leader分成三个子相对独立的子问题
- leader选举:当一个leader凉了,新的leader必须选出来
- log replicated:leader必须要接收log条目,并且将其复制到其他的服务器上
- safety:对于raft来说最关键的安全属性是状态机安全
接下列举几张关于raft的图
raft basics
典型的raft集群有5个机器,可以运行2个机器宕机,每个机器有1到3个状态(raft一共有三个状态,leader,candidate,follower),其中只有leader可以接收客户端发来的log(客户端发给follower会被重定向给leader),candidate是用来选举leader时候用的(候选人),除了leader,candidate,follower不发送请求,只会回复leader,和candidate的信息
为什么要奇数?因为偶数可能会引发split vote的情况,也就是2个候选者票数一致,这个情况在此term中
btw其实偶数也不是不行,具体看StackOverflow中的这个回答,链接在此
所有服务器刚开始都是follower,那么follower什么时候开始变成candidate的呢?当follower在一定的时间内没有收到消息(收到rpc等)然后follower就变成candidate,最后这个server开始参与选举
raft将整个运行的过程时间分为任意时长的terms(不能叫术语,叫学期,任期?,朝代?,terms包含多个term,一个term描述从选举到重新选举这一段时间机器的过程),每一个term开始都要选举,每一个candidate都想成为leader,但是只有一个成功,其他的都会成为follower,如果陷入split vote的情况,那么此回合(term)将会没有leader,然后新的回合(term)将会快速开始,然后再选,如下图中间的no emerging leader就是split vote情况
不同的服务器可能在不同的时间看到leader换届(经过一个term),还有一些极端的情况导致server没有进入term(也就是没有选举),term就像raft的一个逻辑时钟一样,每一个server都会在本地存储term(也就是current term),每一个服务器的当前term都是服务器之间交互的时候知道的(server从candidate的request vote RPC接受到的),交互得到信息比如term,发现收到的term比自己的更新就更新自己的term,如果leader或者candidate发现自己的term太大了(原文用out of data),那么就将自己的状态改为follower
raft中的server使用raft通讯,基本的raft有2种rpc,
一个是RequestVote rpc,在选举中由candidate发起(让别人投票),
还有一个是append-entire rpc,它由leader初始化,发起,他的用途是发送日志,和心跳(appendentire RPC中不包含entire就是心跳包),
其实还有一种RPC叫做server retry rpc,如果服务器在一定的时间内没有收到回复,为了最好的性能,他们会同时发出rpc
leader选举
raft用心跳去触发选举,当一个server启动后他的状态就是followers,只要followers周期性的收到从candidate和leader发来的有效rpc他就会一直保持follower状态
leader会定期的发送心跳rpc给followers(就是append-entire rpc,但是这个不带log),如果server没有在一定的周期中收到这个append-entire rpc(心跳)那就认为当前没有leader存在,然后就准备选举进行新的leader选举
选举开始
在选举开始的时候一个follower先给自己的term加一(因为这个follower先election timeout,为什么会timeout,因为没有收到candidate或者leader发送过来的有效RPC),然后将其的状态改为候选人状态candidate
然后candidate会开始干2种事情
1,给自己投一票
2,发送requestvote rpc给其他的server要选票
一个candidate只有当以下1或者3个事情发生的时候才会改变自己candidate状态
1,他赢得了选举(成了leader)
2,其他的服务器赢了选举(自己失败就变回成follower)
3,leader迟迟选不出(split vote),下面会特别的提到这个特殊情况
leader选不出来的时候大家都是candidate,那么candidate不能得到大多数机器的选票,这个时候就会等到timeout,然后会通过增加term开始新的一轮选举,并且会发送request vote给其他的candidate来初始化
当一个candidate赢得了集群中大多数机器的选票*(特别要注意这里的大多数机器指的是集群中所有机器的大多数机器,而不是现集群中存活的机器中大多数机器)*(并且这些机器都处于一个term中,他才能成为leader(如何确定全部机器数量?),在选举中最主要的一个规则是最多只有一个candidate能赢得选举,当一个candidate赢得了选举,他会发送心跳给所有的server宣告自己是leader(心跳只能是leader发),并且预防下一次选举发生
还有一个情况,在candidate等待选举的时候收到了AppendEntireRPC(AppendEntireRPC是leader发的,根据上述图二知道AppendEntireRPC包含term),如果term大于candidate当前的term,那么就将自己的状态改为follower并且更新term,如果term小于自己的term那么就拒绝心跳包,位置candidate状态
我们理一下如何选举的,首先服务器初始化会随机设置一个选举超时时间(下面会讲到),如果超时时间小的说明最先超时,最开始所有的服务器都是follower,这时候第一个服务器超时了,超时后term+1,并且状态变成candidate,他是会和其他服务器发request vote但是他带的term比其他的服务器要高一些,其他服务器收到后发现自己还活在上个时代,将自己的term加一,然后respond那个最先超时的服务器,把选票投给他,最后这个最先超时的服务器变成了leader
split vote
raft使用随机的选举超时时间,来确保split vote的情况快速被解决,随机时间是150ms~300ms,假设我有多个candidate进入split vote的情况,多个candidate都会在下一次的选举之前(因为本身就有超时时间)设置好自己的选举超时时间,这样当超时就退出选举,这样也预防了下一次选举超时split vote,所以说选举超时时间在raft里面非常重要
leader选举图解
最开始作者想用一个ranking系统,每一个candidate在一个ranking,这个ranking用来candidate之间竞争用的,,如果某个candidate发现了一个比自己更牛逼的candidate(higher ranking),他就会回到follower的状态,这个更牛逼的candidate就有可能变成leader(参考上面我整理的选举过程),这个ranking就是term
首先看下图我们有3个server,3个server都是刚刚开机都是follower状态,刚刚开始三个server的currentterm都是0↓
因为timeout时间是每一个server开始选举的时候随机选择的所以server 0 在设定了timeout时间后,在一定的时间内没有收到leader,candidate发来的RPC后自己timeout了(没有收到是因为此时没有leader和candidate),然后server0 timeout,其他的server timeout时间可能要大一点,所以server0先timeout timeout后server0变成了candidate如下图↓
当server0变成candidate后会发送requestvote RPC,并且candidate的currentTerm加一变成了1,当其他的follower收到这个requestvote RPC后就不会等待timeout了
当follower收到request vote后,follower会考量几个方面,来确定是否要给这个candidate投票
1,candidate的current term是否大于我(收到requestvote的follower)的current term,上图中current term是大于follower的current term
2,再比较candidate的log entire和我相比是否最少是update-to-data(最不济要和自己的相等大于最好),对于这个update-to-data有以下2个方面考量
①拿candidate发过来他的lastlogterm和自己logentire中最后一个logentire的term作比较,看这个对方发过来的term和自己的是否相等,或者大,如果大就不用比第②条,如果term相等再比第②条
②如果candidate log中最后一个log entire的term和我的相等,那么我们就比谁的log长,也就是比较candidate发送过来的lastlogindex和自己的lastlogindex
以上的2个条件缺一不可!!!
根据上图中candidate发过来的requestvote发现term比follower的大,然后开始对比第二条,发现和我的一样,at lease满足up-to-data条件,所以2个follower会给这个candidate投票,如下candidate收获3票当选(满足majority规则,总server数为3大于等于2就可以)leader(2票其他的follower的1票自己投自己的)
可能你对up-to-data还是有一些迷糊,下面我们有几个例子,列举那个log是up-to-data
1,1,1,2,3 #这个log为up-to-data
1,1,1,1,1,1,1
----------------------------------------------------
1,1,1,2,3
1,1,1,2,3,3,3 #这个log为up-to-data
-----------------------------------------------------
1,1,1,2,3
1,1,4 #这个log为up-to-data
看到网上有很多人对timeout不是很清楚,搞出了个election timeout和heartbeat timeout,这里我又重新回去看论文和查资料发现只有一个timeout,那就是election timeout,是由follower在一定时间没有收到心跳就发生的事件,election timeout后follower转变为candidate,没有所谓的heartbeat timeout,还有一点是follower变成candidate后会重置election timeout,这里要注意
log replicated
这里一定要理解2术语,commit和replicate
commit指的是log entire中的log entire commit到状态机中,让状态机执行(leader要决定什么时候commit安全)
replicate指的是将log entirereplicate到其他的server中
一但当一个leader被选了后,leader开始处理client的请求,每一个client请求包含一个能被replicated state machine执行的命令,leader收到这个命令后,先存入自己的log中(log看成一个中间存储,最后要输入给状态机的),然后leader会并行的发送AppendEntriesRPC给所有的服务器,这个AppendEtriesRPC包含要append的命令,具体可以看上面的图解AppendEntriesRPC结构,当这个日志条目被安全的replicate到所有别的服务器上后,leader才会将log entire放入状态机中执行,然后返回结果给client,如果一个follower crash了或者因为网络等原因执行的非常慢,leader都会重新发AppendEntriesRPC,甚至在已经回复client之后还是会发,直到follower已经存储了log之后
server中log的组成形式如下图所示
每一个小格子都存储了一个日志还有一个term,这些条目都是从leader中收集的,在每一个日志中还放置了term,说是用来探测数据不一致性发生(没搞明白如何探测),他还能保证一些特性,这个特性描述如下图
这里提及一个概念,committed,意思为可以提交的,指的是一个log entry,此log entry被leader认为是安全的,可以被状态机执行,raft算法保证committed是可以在每一个server中是持久存储的,且最终会被可用的状态机执行
什么时候刚刚创建的log entire会被commit到状态机中?在log entire被replicate到大多数机器上的时候
作者设计的raft log机制,在更高层面维持了日志的连贯性(当然在不同的服务器上),raft不仅仅简化了日志的行为,更是保证了安全,raft使用了以下2个属性一起组成了Log Matching Property
1,如果2个日志条目(entry)在不同的log中(也就是在不同的server中,一个server一个log),并且他们有相同的index和term,这时候他们存储的是相同的命令
对于第一个属性来说,follower门收到leader发过来的log entry,这个entry在相应的term下给定了index(是leader给定的,并且log entry从不改变自己在log中的位子),所以可以保证第一个属性
2,如果2个日志条目(entry)在不同的log中,并且他们有相同的index和term,那么2个日志在此entry之前的entry是完全相同的
对于第二个属性呢来说它是由raft的一个非常简单的一致性检查机制确保的,怎么检查呢?首先leader在发送AppendEntiresRPC给server(为了日志的replication),这个包里面除了要append日志条目(log entire)还有这个entire在ledaer log中的前一个entire的index和term,如果server中发现接受到的报文中的prelogterm和prelogindex和自己相应位置的结果不一样(需要append的位子的前一个entire),那么说明不再是一致性,再append就打破raft强一致性的性质,所以直接拒绝
raft不一致性情况
在老的ledaer挂掉后(在replicate的时候挂掉,日志还没有完全的copy到所有服务器上,或者日志还没有完全replicate完),新的leader的log可能和一些follower的log有一些不一样,具体看下面的图片
在上图中最上面一行(没有标注a,b,c,d,e的一行)是新的leader,此时有可能follower缺少log entire(a,b),也有可能log entire多于leader(c,d),也有可能都发生(e,f)
注意看f,有可能他之前是一个leader,在接受了log entire后(f中间为紫色的term2),还没来得及将他发送给所有的其他server,就crash了,但是他又快速的重启,重新回到leader的位子上,开始接收term为3的log entire,接收完后还没有发送出去又crash了…
恢复一致性
raft如何处理一致性的问题呢?强制follower从自己(leader)的log entire中复制,这样意味着,如果leader发送过来的log entire根据其index,follower发现自己这个index上已经有上一任leader发送过来的log entire,此时leader会将其用新leader的log entire覆盖老的log entire
为了保持一致性,除了新leader发送的log entire要保持一致性,还有之前遗留下来的非一致性也要解决
假如在leader发送appendRPC的时候一致性检查不通过(在leader发送appendRPC请求给follower新增log entire的时候检测新log entire前一个位子的log entire的index和term是否和发送过来的append RPC 中prelogindex,prelogterm一致,这个prelogindex和prelogterm是leader的log中数据)假设这个校验不通过就发送respond给leader,leader开始找follower log中的一个point,这个point后面的log entire都是和leader不一致,找到后,leader开始用自己的log entire 覆盖follower这些不一致的log entire
进一步的讲,leader会在自己身上维持一个叫做nextindex的东西(这个东西是一个数组/切片等等类似的东西,他会记录每一个server的nextindex,包括自己的),这个东西由leader维护,这个nextindex是当前leader和所有server的当前log entire的下一个entire的index,当leader上任后会初始化这个变量,当我们的一致性校验不通过,那么就将这个server对应的next index减一,然后leader再发送appendRPC给这个不一致的server,如果当前的nextindex对应的值还是不一致,那么server对应的next index再减一,直到server对应的next index和leader的next index对应的值一模一样,那么就不再对server对应的next index减一,这个时候leader大概的确定了不一致server从那个点开始数据不一致的,然后leader将这个点(不一致的server对应的next index)到后面所有的log entire 发送给不一致的server,让server覆盖他
为什么leader发送过来的appendRPC可以确定不一致的log index呢?首先append RPC发送过来的包包含prevlogindex和prevlogterm,这两个用于做一致性检查,但是我们是用nextindex去确定这个不一致的点,其实prevlogindex在发送的时候会变的,首先leader中的nextindex是6,6,6(第一个为成员一,第二个为leader,第三个为不一致的server),然后leader发送AppendRPC进行一致性校验,leader发送给不一致的server的AppendRPC包中previndex为5(相应nextindex-1),然后发现这个server index为5的term和leader index为5的term不一致,然后leader发现这第三个server数据不一致,发现不一致后leader为了定位不一致的位子,开始对不一致的server对应的nextindex减一,所以此时leader中nextindex为6,6,5,然后leader再发送AppendRPC给server,这时候previndex为4(nextindex -1),此时如果server对比自己对应index为4的term和leader发送过来的AppendRPC中prevterm不一致就继续返回false给leader,leader再减一对应的nextindex,如果返回success说明leader定位了不一致的那个点,然后就开始发送从这个点到后面的所有的log entire给server,叫server覆盖对应数据
raft replicate的详细步骤图解
假定我们当前有3个server,分别是server0,server1,server2,其中每个server中自己保存的条目如下如图
经过选举server0成为leader,server1,2都还是follower,具体选举过程不再说了,请看之前的leader选举图解,注意的是server0成为leader后,leader会维护2个成员(leader only),分别是nextindex和matchindex,nextindex和matchindex是怎么来的呢?是leader发送appendentireRPC后如果成功就更新自己维护的nextindex和matchindexn,因为appendentireRPC会返回是否成功,和follower发送的currentterm,如果成功,就跟新nextindex和matchindex,怎么更新,更具自己发出的appendentireRPC中的prevlogindex条目进行更新(一般这个用于一致性检查),因为是强一致性基本可以确定这个就是当前follower的log中最后一个log entire,将其加一直接放入matchindex这个切片中,再加一放入nextindex中
nextindex[]是每个server(包括leader),的next log entire to send to that server
matchinidex[]是每一个server(包括leader)的log中最后一个log entire的index,一般来说是nextindex+1
如下图,leader收集到了自己和server1,server2的当前最后一个log entire的index(matchindex)和append的log entire对应的index+1(nextindex)
leader给server发送心跳↓,prevlogterm为-1因为当前没有logentire
server返回appendentirereply ↓
此时client发送了请求过来(3个请求),三个请求按序存入leader的log中↓
此时leader开始replicate了,在replicate(发送appendentirerpc之前,先把自己的nextindex和matchindex改了)↓
2个server返回成功和term,leader收到后开始跟新自己的nextindex和matchindex(leader状态见下下张图)
此是三个log entire已经replicate到大部分 server中,此时leader开始commit到状态机中,先将自己的commitindx变成3,这里还有一个判断,当commitindex大于lastapplied就开始将log entire放入状态机中,放入完毕,lastapplied也改为3,然后返回client
commitindex:index of highest log entry know to be committed
lastApplied: index of highest log entry applied to state machine
leader挂掉后处理图解
假设我们的场景是这样的,server0曾经是leader,但是发生了网络故障,他被隔离出去(不能和其他的server通讯),server1变成了leader,此时网络故障被修复,现在又2个leader,分别是sever0和server1
server0好了后还是试图像以前一样发送AppendentireRPC给其他的server(心跳),不过其他的server经过选举term已经大于server0,所以他们会返回false并且返回自己当前的term
此时server0意识到已经有了一个leader,并且term大于自己当前的term,然后开始清空自己的nextindex,matchindex(因为只有leader才维护),并且改变自己的currentTerm为最新
leader因为收到了appendentireRPC,所以根据自己维护的nextindex像server0发送对应的log entire,和告诉leader已经commit到5了(appendentireRPC的leadercommit成员),server 1在当选leader的时候将自己的nextindex改为4,4,4根据自己的log entire数量
server0开始更新log entire,并且更改自己的commitindex,然后发现commitindex大于lastapplied(3),所以开始commit到状态机中,commit最后2个(commitindex-lastapplied),commit完成后更新lastappliend为5
commitindex代表将要commit到那里,lastapplied代表已经commit到那里,如果是leader,一般收到大多数的server返回就开始更改commitindex,其他的server一般是收到appendentireRPC后先看leader commit到那里,如果高于自己,自己再改,改完commitindex后必定立即跟着一个判断,判断是否大于lastapplied,大于就commit
最后成为这样
raft一致性检查和修复图解
首先raft一致性检查发生于发送appendentireRPC的时候,follower接收到了发送过来的appendentireRPC中的prevlogterm和prevlogindex来进行一致性检查,具体看下面图解
现在生产环境中有3台机器,S2作为之前的leader被网路隔离了,S0和S1中S1被选为leader,并且S2中的log和S0,S1之间不一样,S2只commit到了index为3的log entire ↓
然后的步骤在之前图解过,leader挂掉后又好了会发生什么,不过此时之前挂掉的leader(S2)的log不一样,此时S1在发送appendentireRPC的时候,S2收到会根据appendentireRPC中的prevlogindex和prevlogterm进行一致性检查,leader发送的appendentireRPC中prevlogindex为5,prevlogterm为4(记住prevlogindex是对应nextindex-1),S2发现自己index为5的log entire的term是2而不是leader发送过来的5,此时说明一致性检测失败,数据不一致 ↓
S2发现一致性检测失败会回S1 false ↓
此时leader收到S2发送过来的false后会将S2的nextindex减一,然后再发送appendentireRPC,此时RPC中的prevlogindex为4,对应的prevlogterm为2(为什么prevlogindex为4,因为他是对应nextindex-1),还包含了要更新的entire 4,S2收到后再进行匹配,发现自己index为4的log entire term为2不等于leader发过来的3,所以一致性检测又失败了,所以拒绝leader发送过来的entire,并且返回失败↓
失败后S2返回appendEntirereply false
还是按照之前的经验(此处的实现应该是一个循环),leader将自己维护的nextindex中S2对应的元素减一,此时变成4,然后再发送appenentireRPC,此时appendentireRPC的prelogindex是3(S2对应nextindx-1),对应的term是1,并且携带2个要append的entire(如果S2一致性检测还是失败就拒绝这个),S2收到后对比自己的log entire发现自己index为3的entire term为1,和leader发送过来的匹配上了,所以一致性检测通过然后接收appendentireRPC携带的entire,将其覆盖到自己的log中(原有的覆盖,并且后面都抹去),最后返回success↓
此时数据都一至了
假设数据一开始都一直,一致性检测会直接通过,然后接收appendentireRPC的log entire
safety
以上讲了那么多的机制还是不能确认每个server的状态机精准的按照顺序执行每一个log entire,比如一个follower在leadercommit 一些og的时候挂了,极有可能这个挂掉的follower起来后被选为leader,然后发送的appendentireRPC给其他server,其中leadercommit可能早已经被一些server执行当这些server接收到这个新的leadercommit的时候极有可能再执行一次,这样就不能确保log entire的顺序性,这一部分呢,主要讲解在选leader的时候一些限制,这些限制可以保证任何term的leader,都会包含所有前任leader提交的log entire,后面会讲解这些限制
election restriction
对于所有的基于leader的一致性算法来说,leader最终都需要存储所有的committed log entire,有一些算法比如Viewstamped,它可以使leader在被选举出来后不包含所有的committed log entire,而是通过随后的一些非常复杂的机制确保leader上缺失的log entire传送到leader上,但是这样使算法变的极其复杂,而raft却使用了一个非常简单的机制确保刚刚被选举出来的leader有之前所有term对应leader commit后的log entire(就是让新的leader记得上个leader commit到了那里,自己好继续commit)
raft要确保leader在被选举之前,和集群中大多数机器contract,意味着所有term对应的leader committed后的log entire至少全部存于一个或者多个server中
candidate的限制
首先对于candidate,假设candidate的log entire和集群中大多数的server一样,那么当candidate发送requestRPC的时候这个RPC会包含candidate的log entire的信息,其他服务器如果收到requestRPC,对比candiate的log entire和自己的log entire发现自己的比candidate的还要多,那么就拒绝requestRPC
具体怎么比较呢?raft通过比较两个log的最后一个index和term来确定谁是最新的,谁最后一个的term大谁就是最新的,如果2个log的最后一个term相等,那就比较谁的日志长(最后一个index大小),谁就是最新的
committing entires from previous term
首先我们先确定什么时候才会commit
1,log entire已经存于大多数服务器上,并且被写成了当前的term(这一条不适用于前任term)
2,这个entire之前的entire已经被commit(如果要commit的log entire之前的log entire还没commit,会先commit)
就像我们之前讲的那样,一但leader将log entire committed,意味着集群中大多数的机器都已经收到了这个log entire,但是leader在commit之前crash了,怎么办?其实下一个leader尝试完成replicate这个entire,注意新的leader不能立马的得出结论这个entire已经存于大部分server中,leader并不会replicate之前term的log entire
假设我们没有leader并不会replicate之前term的log entire,并且follower不会按照2个限制条件去reject candidate发过来的requestvote这时会发生什么
再重复一遍新的leader不能立马的得出结论老leader发的entire已经存于大部分server中,所以我们的限制条件是新的leader不能replicate之前term的log enrtire
下面是没有遵守新的leader不能replicate之前term的log enrtire
首先S1成为leader当前term为2,他将会replicate term为2的log entire给其他的服务器,但是replicate到S1就crash了,此时只有S2收到了replicate的log ↓
S1挂掉后,S5选举成leader(term 3),并且接收了client发来的log entire存入自己的log中,S5是通过S3.S4和自己的票数当选leader,(此时S1又起来了),S1,S2因为自己的log entire term比S5的还要多所以拒绝为S5投票↓,S5成为leader后term为3,在S5当选leader后发送appendRPC(无log entire就是心跳)给其他的server的时候其他server更新自己的term(包括S1,S2)
此时在S5还没有将log entire发送给其他服务器的时候自己先挂掉了,所以此时的log entire如上图↑所示,其他的服务器还是没有收到心跳在一定的时间内timeout,有可能S1的timeout时间更小,所以S1的timeout更快,所以S1的term变成了4,发送给S2,S3,S4 request vote,所以此时S1当选leader,并且S1继续将他作为上一次leader还未完成的任务完成(发送index为2的log entire,当时只有S2接收) S3接收了S1发送过来的index为2,term为3的log entire(我们此时假设没有规定不能发送之前term commit的log entire),所以此时S3收到了term为2的log entire(来自S1),恰好此时client发送过来的log entire存入S1中标记为term4,如下如↓不幸的时,在S1replicate给S3后又crash了,term为4的那个还没来的及replicate,而且term为2的也只replicate给S3,crash后又起来,但是此时S5有可能已经选为leader
S5可能已经选为leader,假设S1起来了,S5会收到4个选票分别来自S2,S3,S4,S5,然后S5成为leader(S1不给是因为S1的最后一个log entire比S5新),因为此时S5的log entire分别是1,3,此时S5发送一致性检测(appendRPC),nextindex是3,比较previndex是2,prevterm是3,所以S1,S2,S3,S4都被S5给一致性了如下图↓
这个就是不遵守我们leader并不会replicate之前term的log entire的后果,如果遵守了,d中S5不会overwrite其他的server(因为当前term为5,需要overwrite的entire的term为3),c中S3不会多出了term为2的entire(来自S1),并且在c中S1replicate了term为4的log entire到大多数机器上,然后commit term为4的log entire(间接commit了log term为2的那个log entire,因为要commit term为4的log entire需要将之前的log entire提交所以会卡在这里等待),那么S2,S3,S4如何保证数据和S1一致呢?在S1commit term为4之前,会将appendentireRPC发送到大多数的服务器上,appendentireRPC到server上,server会展开一致性检查,可以通过一致性检查来同步数据,直到大多数的服务器上有了term,S1才会commit
safety argument
follower and candidate crashes
之前都在讲leader crash了该怎么办,这个时候我们讲一下candidate/follower crash了改怎么办,首先如果candidate/follower crash了request vote 或者appendRPC都会失败,raft机制会无限期的retry假如crash的server重启成功就会返回success,raft是幂等的,假如server收到leader发送过来的appendRPC,对比里面的log entire和index发现自己已经有了,那么就会忽略这个log entire
timing and available
raft的安全机制不能依赖于定时器,我们的分布式系统不能因为某一些时间发生快了,或者慢了就产生一些错误(因为网络的原因),但是系统的可用性必须依赖定时器(可用性指的是系统要在一定的时间内回复client)
Log compaction
为什么需要snapshot
在特定的系统中,raft的log增长不能没有边界,假如日志增长的太长,他会占用更多的空间,并且花费更多的时间去replay,如果没有某些机制去消除累计在log中过时的信息,会带来可用性的问题,snapshotting就是一个解决过时信息的方法,看下图
看下面的解释我们大概了解他是怎么工作的,我们把commited的log entire用快照代替,这个快照记录了最近一次的included index和最近一次included term 3,最主要的记录了状态机的状态,比如上图x最后的状态为0,y最后的状态为9,这样我们前面commit后的log entire就可以不要了,只保存快照就可以,为什么可以不要commit后的log entire呢?因为既然是commit后的那么说明这个log entire已经存在于majority的机器上,
snapshotting是compaction最简单的方法,整个系统的当前状态(状态机的状态)被写入一个稳定性的存储中
figure 12解释了raft最基本的快照原理,每一个server都自己维护着一个快照,这个快照来代替刚刚commit的log,大部分状态机的状态被写入快照,raft的快照也有一些小的元数据在里面,比如last index,last term,而这些元数据也是appendentireRPC做一致性检查的时候需要的,一但server完成写入状态机(将当前状态写入快照,也意味着有了快照commit后的log entire可以删了),他就会将之前commit之后的log entire删了
什么时候需要snapshot
尽管说每个server维护自己的snapshot,但是leader还是要偶尔发送snapshot到每个follower上
什么时候发?当leader把next log删除了的时候发,为什么next log会被删除?因为next log已经被commit到了状态机中,换句话说next log已经被replicate到大部分的机器上
leader发送的RPC长什么样
raft使用一个新的rpc,名叫installsnapshot,这个RPC用于leader发送snapshot给follower,见下图
一个follower收到leader发过来的installsnapshotRPC,首先的是要决定是怎么应对现存的log entire
通常来说snapshot会包含接受者没有的信息,这种情况,follower直接清空自己的log entire,用leader发过来的snapshot代替,
发现的问题
candidate,follower被隔离不断timeout,term一直增加
假设我们的follower和candidate被网络隔离咋搞…要知道按照基本的协议,当隔离发生并且在election timeout后还没有解除网络隔离,那么这个节点会一直timeout下去,带来的问题是这个被隔离的follower和candidate的term一直快速线性增加…
解决方法
这个时候我们引入pre-vote机制
这个是什么路数?
我们的node election-timeout之后按照原来情况是立即开始选举,但是引入pre-vote机制后,先进入pre-candidate状态,再先发送pre-vote request,得到了大多数机器的回复yes就开始进入candidate,再发送request vote