系列文章java
1.1 刚开始全部server启动都是follower状态git
而后等待leader或者candidate的RPC请求、或者超时。github
上述3种状况处理以下:算法
leader的AppendEntries RPC请求:更新term和leader信息,当前follower再从新重置到follower状态数组
candidate的RequestVote RPC请求:为candidate进行投票,若是candidate的term比本身的大,则当前follower再从新重置到follower状态安全
超时:转变为candidate,开始发起选举投票服务器
1.2 candidate收集投票的过程微信
candidate会为这次状态设置随机超时时间,一旦出如今当前term中你们都没有获取过半投票即split votes,超时时间短的更容易得到过半投票。atom
candidate会向全部的server发送RequestVote RPC请求,请求参数见下面的官方图.net
上面对参数都说明的很清楚了,咱们来重点说说图中所说的这段话
If votedFor is null or candidateId, and candidate’s log is at least as up-to-date as receiver’s log, grant vote
votedFor是server保存的投票对象,一个server在一个term内只能投一次票。若是此时已经投过票了,即votedFor就不为空,那么此时就能够直接拒绝当前的投票(固然还要检查votedFor是否是就是请求的candidate)。
若是没有投过票:则对比candidate的log和当前server的log哪一个更新,比较方式为谁的lastLog的term越大谁越新,若是term相同,谁的lastLog的index越大谁越新。
candidate统计投票信息,若是过半赞成了则认为本身当选了leader,转变成leader状态,若是没有过半,则等待是否有新的leader产生,若是有的话,则转变成follower状态,若是没有而后超时的话,则开启下一次的选举。
一旦leader选举成功,全部的client请求最终都会交给leader(若是client链接的是follower则follower转发给leader)
2.2.1 client请求到达leader
leader首先将该请求转化成entry,而后添加到本身的log中,获得该entry的index信息。entry中就包含了当前leader的term信息和在log中的index信息
2.2.2 leader复制上述entry到全部follower
来看下官方给出的AppendEntries RPC请求
从上图能够看出对于每一个follower,leader保持2个属性,一个就是nextIndex即leader要发给该follower的下一个entry的index,另外一个就是matchIndex即follower发给leader的确认index。
一个leader在刚开始的时候会初始化:
nextIndex=leader的log的最大index+1 matchIndex=0
而后开始准备AppendEntries RPC请求的参数
prevLogIndex=nextIndex-1 prevLogTerm=从log中获得上述prevLogIndex对应的term
而后开始准备entries数组信息
从leader的log的prevLogIndex+1开始到lastLog,此时是空的
而后把leader的commitIndex做为参数传给
leaderCommit=commitIndex
至此,全部参数准备完毕,发送RPC请求到全部的follower,follower再接收到这样的请求以后,处理以下:
重置HeartbeatTimeout
检查传过来的请求term和当前follower的term
Reply false if term < currentTerm
检查prevLogIndex和prevLogTerm和当前follower的对应index的log是否一致,
Reply false if log doesn’t contain an entry at prevLogIndex whose term matches prevLogTerm
这里可能就是不一致的,由于初始prevLogIndex和prevLogTerm是leader上log的lastLog,不一致的话返回false,同时将该follower上log的lastIndex传送给leader
leader接收到上述false以后,会记录该follower的上述lastIndex
macthIndex=上述lastIndex nextIndex=上述lastIndex+1
而后leader会重新按照上述规则,发送新的prevLogIndex、prevLogTerm、和entries数组
follower检查prevLogIndex和prevLogTerm和对应index的log是否一致(目前一致了)
而后follower就开始将entries中的数据所有覆盖到本地对应的index上,若是没有则算是添加若是有则算是更新,也就是说和leader的保持一致
最后follower将最后复制的index发给leader,同时返回ok,leader会像上述同样来更新follower的macthIndex
2.2.3 leader统计过半复制的entries
leader一旦发现有些entries已经被过半的follower复制了,则就将该entry提交,将commitIndex提高至该entry的index。(这里是按照entry的index前后顺序提交的),具体的实现能够经过follower发送过来macthIndex来断定是否过半了
一旦能够提交了,leader就将该entry应用到状态机中,而后给客户端回复OK
而后在下一次heartBeat心跳中,将commitIndex就传给了全部的follower,对应的follower就能够将commitIndex以及以前的entry应用到各自的状态机中了
对于上述leader选举有个重点强调的地方就是
被选举出来的leader必需要包含全部已经比提交的entries
如leader针对复制过半的entry提交了,可是某些follower可能尚未这些entry,当leader挂了,该follower若是被选举成leader的时候,就可能会覆盖掉了上述的entry了,形成不一致的问题,因此新选出来的leader必需要知足上述约束
目前对于上述约束的简单实现就是:
只要当前server的log比半数server的log都新就能够,这里的新就是上述说的: 谁的lastLog的term越大谁越新,若是term相同,谁的lastLog的index越大谁越新
可是正是这个实现并不能彻底实现约束,才会产生下面的另一个问题,一会会详细案例来讲明这个问题
raft给出的答案是:
当前term的leader不能“直接”提交以前term的entries
也就是能够间接的方式来提交。咱们来看下raft给出不能直接提交的案例
最上面一排数字表示的是index,s1-s5表示的是server服务器,a-e表示的是不一样的场景,方框里面的数字表示的是term
详细解释以下:
a场景:s1是leader,此时处于term2,而且将index为2的entry复制到s2上
b场景:s1挂了,s5当选为leader,处于term3,s5在index为2的位置上接收到了新的entry
c场景:s5挂了,s1当选为leader,处于term4,s1将index为2,term为2的entry复制到了s3上,此时已经知足过半数了
重点就在这里:此时处于term4,可是以前处于term2的entry达到过半数了,s1是提交该entry呢仍是不提交呢?
假如s1提交的话,则index为2,term为2的entry就被应用到状态机中了,是不可改变了,此时s1若是挂了,来到term5,s5是能够被选为leader的,由于按照以前的log比对策略来讲,s5的最后一个log的term是3比s2 s3 s4的最后一个log的term都大。一旦s5被选举为leader,即d场景,s5会复制index为2,term为3的entry到上述机器上,这时候就会形成以前s1已经提交的index为2的位置被从新覆盖,所以违背了一致性。
假如s1不提交,而是等到term4中有过半的entry了,而后再将以前的term的entry一块儿提交(这就是所谓的间接提交,即便知足过半,可是必需要等到当前term中有过半的entry才能跟着一块儿提交),即处于e场景,s1此时挂的话,s5就不能被选为leader了,由于s2 s3的最后一个log的term为4比s5的3大,因此s5获取不到投票,进而s5就不可能去覆盖上述的提交
这里再对日志覆盖问题进行详细阐述
日志覆盖包含2种状况:
commitIndex以后的log覆盖:是容许的,如leader发送AppendEntries RPC请求给follower,follower都会进行覆盖纠正,以保持和leader一致。
commitIndex及其以前的log覆盖:是禁止的,由于这些已经被应用到状态机中了,一旦再覆盖就出现了不一致性。而上述案例中的覆盖就是指这种状况的覆盖。
从这个案例中咱们获得的一个新约束就是:
当前term的leader不能“直接”提交以前term的entries 必需要等到当前term有entry过半了,才顺便一块儿将以前term的entries进行提交
因此raft靠着这2个约束来进一步保证一致性问题。
再来仔细分析这个案例,其问题就是出在:上述leader选举上,s1若是在c场景下将index为二、term为2的entry提交了,此时s5也就不包含全部的commitLog了,可是s5按照log最新的比较方法仍是能当选leader,那就是说log最新的比较方法并不能保证3.1中的选举约束即
被选举出来的leader必需要包含全部已经比提交的entries
因此能够理解为:正是因为上述选举约束实现上的缺陷才致使又加了这么一个不能直接提交以前term的entries的约束。
Leader Completeness: 若是一个entry被提交了,那么在以后的leader中,必然存在该entry。
通过上述2个约束,就能得出Leader Completeness结论。
正是因为上述“不能直接提交以前term的entries”的约束,因此任何一个entry的提交必然存在当前term下的entry的提交。那么此时全部的server中有过半的server都含有当前term(也是当前最大的term)的entry,假设serverA未来会成为leader,此时serverA的lastlog的term必然是不大于当前term的,它要想成为leader,即和其余server pk 谁的log最新,必然是须要知足log的index比他们大的,因此必然含有已提交的entry。
在client看来:
若是client发送一个请求,leader返回ok响应,那么client认为此次请求成功执行了,那么这个请求就须要被真实的落地,不能丢。
若是leader没有返回ok,那么client能够认为此次请求没有成功执行,以后能够经过重试方式来继续请求。
因此对leader来讲:
一旦你给客户端回复OK的话,而后挂了,那么这个请求对应的entry必需要保证被应用到状态机,即须要别的leader来继续完成这个应用到状态机。
一旦leader在给客户端答复以前挂了,那么这个请求对应的entry就不能被应用到状态机了,若是被应用到状态机就形成客户端认为执行失败,可是服务器端缺持久化了这个请求结果,这就有点不一致了。
这个原则同消息队列也是一致的。再来讲说什么叫消息队列的消息丢失(不少人还没真正搞明白这个问题):client向服务器端发送消息,服务器端回复OK了,以后由于服务器端本身的内部机制的缘由致使该消息丢失了,这种状况才叫消息队列的消息丢失。若是服务器端没有给你回复OK,那么这种状况就不属于消息队列丢失消息的范畴。
再来看看raft是否能知足这个原则:
leader在某个entry被过半复制了,认为能够提交了,就应用到状态机了,而后向客户端回复OK,以后leader挂了,是能够保证该entry在以后的leader中是存在的
leader在某个entry被过半复制了,而后就挂了,即没有向客户端回复OK,raft的机制下,后来的leader是可能会包含该entry并提交的,或可能直接就覆盖掉了该entry。若是是前者,则该entry是被应用到了状态机中,那么此时就出现一个问题:client没有收到OK回复,可是服务器端居然能够成功保存了
为了掩盖这种状况,就须要在客户端作一次手脚,即客户端对那么没有回复OK的都要进行重试,客户端的请求都带着一个惟一的请求id,重试的时候也是拿着以前的请求id去重试的
服务器端发现该请求id已经存在提交log中了,那么直接回复OK,若是不在的话,那么再执行一次该请求。
follower挂了,只要leader还知足过半条件就,一切正常。他们挂了又恢复以后,leader是会不断进行重试的,该follower仍然是能恢复正常的
follower在接收AppendEntries RPC的时候是幂等操做
目前这一块稍后再说
最后就再说下raft的java版实现,能够看下copycat
#5 后续计划
后续会再详细分析下ZooKeeper中的ZAB协议实现,而后对比下raft,有哪些设计上的不一样点。
欢迎关注微信公众号:乒乓狂魔