原文地址: https://qeesung.github.io/202...
Raft 论文地址:https://ramcloud.atlassian.ne...html
Raft论文中分为三块:git
本文中主要介绍日志复制
github
领导人必须从客户端接收日志而后复制到集群中的其余节点,而且强制要求其余节点的日志保持和本身相同。数组
鉴于日志复制这一块比较复杂,能够结合下面两个网页来理解:安全
复制状态机一般都是基于复制日志实现的,每个服务器存储一个包含一系列指令的日志,而且按照日志的顺序进行执行。以下图所示:服务器
PUT KEY VALUE
状态机是按照同步到服务器上的指令的顺序,一个一个的去Apply
指令,因此指令的顺序很重要,若是指令Apply
的顺序不一致,或者丢失部分指令,那么最终状态机的状态也会不一致。网络
而咱们知道网络是不稳定的,好比延迟,分区,丢包,荣誉和乱序等错误。若是不保证状态机Apply
指令彻底如出一辙,那么将会致使不一致的结果。而Raft的日志复制机制则能保证在发生上述网络问题的时候,全部的服务器都能同步到彻底如出一辙的日志,也就是说能保证每个服务器的状态机Apply
到彻底如出一辙的指令。并发
全部服务器上都维持的状态:优化
commitIndex
:最大的已经被提交的日志索引lastApplied
:最后被应用到状态的日志条目索引lastApplied
应该小于等于commitIndex
,由于只有在日志被提交之后才能被Apply
到状态机中spa
领导人常常改变的:
nextIndex[]
:对于每个服务器,须要发送给他的下一个日志条目的索引值,初始化为当前领导人的最后的日志索引值加一matchIndex[]
:对于每个服务器,已经复制给他的日志的最高索引值nextIndex[]
和 matchIndex[]
的长度等于整个集群中的服务器数量。
附加日志RPC的请求结构:
term
:附加日志的领导人任期号leaderId
:当前领导人的IdpreLogIndex
:当前要附加的日志entries
的上一条的日志索引preLogTerm
:当前要附加的日志entries
的上一条的日志任期号entries[]
:须要附加的日志条目(心跳时为空)leaderCommit
:当前领导人已经提交的最大的日志索引值preLogIndex
和preLogTerm
主要是用于跟随者检测当前领导人要附加的日志是否和跟随者当前的日志匹配,若是不匹配的话,那么就须要继续向前搜寻和领导人匹配的日志(下面章节会介绍)
而leaderCommit
的话用于告诉跟随者当前提交到什么位置了(由于收到附加日志还不能立刻提交,不然可能存在日志丢失的状况),以便跟随者将已经提交的日志Apply
到状态机中
附加日志RPC的响应结构:
term
:跟随者的当前的任务号success
:跟随者是否接收了当前的日志,在preLogIndex
和preLogTerm
匹配的状况下为true
,不然返回false
每个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的状况,每一条日志条目同时也都有一个整数索引值来代表它在日志中的位置。
一旦成为领导人,那么领导人就会在固定在必定时间内发送空的附加日志,也就是心跳,以组织更随着超时。
若是领导人收到来本身客户端的请求,那么首先将请求的指令附加到本身的日志队列中,而后领导人会将新附加的日志条目经过附加日志的RPC发送到全部的跟随者。
领导人中维护了两个常常变更的属性nextIndex[]
和matchIndex[]
,用于记录要发什么日志和跟随者收到了什么日志。
其中nextIndex[]
记录了须要发送给每个服务器的日志的下一个索引值,这个数组会在领导人被选举出来的时候初始化为领导人最后的索引加一。注意这个nextIndex[]
只是记录了要发给下一次附加日志要发给服务器的索引值,这个索引值可能并不必定准确,好比在跟随者和领导人的日志不一致的状况下,nextIndex[]
的值就须要进行递减以找到和跟随者最大的匹配的日志,具体流程后文中会解释。
而matchIndex[]
主要是用来记录跟随者收到了那些日志,以方便领导人确认是否当前的日志能够进行提交,由于必需要至少N/2+1
的集群成员(包括领导人本身)都确认收到了日志才能够进行的日志的提交。
咱们知道领导人在附加日志的时候是并发的向全部跟随者发起的,而且是在固定周期内定时发送的,因此可能在上一个RPC没有收到响应的状况下,发出下一个RPC请求,或者是收到过时的日志追加请求等等。
比对响应的Term
和当前的Term
以确认本身是否过时:
Term
大于当前的Term
,那么说明当前的领导人已通过期,立刻将本身切换为跟随者Term
小于当前的Term
,那么说明当前的收到了过时的响应(可能网路延迟致使),那么忽略判断附加RPC时的preLogIndex
和当前的nextIndex[peer]-1
是否相等,用于判断是不是过时的响应,或者是nextIndex[peer]
是否被更改了:
判断响应的success
是否为真:
preLogIndex
和preLogTerm
和跟随者的日志不匹配,进行步骤5附加日志成功之后,须要更新matchIndex[peer]
中的值为已经追加日志的索引值,表示追加日志成功,判断整个matchIndex[]
数组中的是否有一半都大于等于追加日志的索引:
commitIndex
,进行日志的提交nextIndex[peer]
来实现。跟随者收到附加日志的请求,不能简单的将日志追加到本身的日志后面,由于跟随者的日志可能和领导人有冲突,或者跟随者缺失更多的日志,入下图所示。
那么必定要确保本次附加日志的以前的全部日志都相同,也就是说附加当前的日志以前,缺日志就要把缺失的日志补上,日志冲突了,就要把冲突的日志覆盖(领导人能够强行覆盖跟随者的日志)
判断附加日志任期Term
和当前的Term
是否相同:
Term
小于当前的Term
,那么说明收到了来自过时的领导人的附加日志请求,那么拒接处理。Term
大于当前的Term
,那么更新当前的Term
为请求的Term
,进行步骤2Term
和当前的Term
相等,那么说明请求合法,进行步骤2判断preLogIndex
是否大于当前的日志长度或者preLogIndex
位置处的任期是否和preLogTerm
相等以检测要附加的日志以前的日志是否匹配:
preLogIndex
的长度大于当前的日志的长度,那么说明跟随者缺失日志,那么拒绝附加日志,返回false
preLogIndex
处的任期和preLogTerm
不相等,那么说明日志有冲突,拒绝附加日志,返回false
preLogIndex
以后的日志是否匹配。逐一比对要附加的日志entries[]
是否和本身preLogIndex
以后的日志是否有冲突:
entries[]
中的任何一位置的日志发生冲突,那么须要将以后的日志进行截断,并追加为entries[]
中以后的日志若是没有发生冲突,那么存在两种状况:
entries[]
的日志所有匹配,这种状况多是重复的附加日志RPC,那么这种状况只会简单的校验一遍全部日志entries[]
的日志匹配当前的全部日志,那么将没有匹配的日志全都追加到当前的日志后面。上述的步骤3是很重要的,若是不对现有的日志进行比对,并且简单的进行截断追加日志,那么是很危险,由于可能收到延时的重复日志附加请求而致使日志没必要要的截断,从而致使已经提交的日志丢失。
另外还有一个优化点,若是存在大量的冲突日志,那么若是经过递减nextIndex[peer]
将会很慢,因此能够经过批量跳过冲突日志方式来作到,能够再响应中添加conflictIndex
和conflictTerm
来作到,这里不展开详细讨论。
全部的服务器都有两个常常变更的值commitIndex
和lastApplied
,commitIndex
表示已经提交的日志索引,而lastApplied
表示最后Apply
到状态机中的日志索引。commtiIndex
会在日志成功附加到集群中的N/2+1
的节点上更新。
通常应用日志到状态机中是经过一个独立的线程来作到的,经过监控是否有新提交的日志,若是有新提交的日志,那么就将日志Apply
到状态机中,而且更新lastApplied
。因此通常commitIndex >= lastApplied
在实际的应用中,通常将Raft单独做为独立的一层共识模块,上层模块将须要达到共识的指令下发给Raft共识模块,在Raft模块达到共识之后,就将达成共识的指令Apply
到上层模块中。好比Etcd
,TiKV
等等