在以前的文章中我分享了我的对于Paxos算法的理解和看法,在文章的末尾引出了Raft算法,今天就来填完Raft算法这个坑。git
Raft算法的做者在论文中吐槽了Paxos算法难以以理解且难以实现,因此提出了一个以易于理解且方便构建的分布式一致性算法,并且Raft算法提供了和Paxos算法相同的安全性及相差无几的性能。Raft算法的提出能够说是造福了咱们这些智商普通的下里巴人。github
我的认为Raft算法最难能难得的地方就在于采用了一种工程化的思惟来设计算法,从而使得Raft算法可以普遍地应用在分布式的领域之中。(etcd、SOFAJRaft、TiKV ...)算法
虽然Raft算法相比于Paxos算法更加容易理解,可是在阅读原论文的时候,我仍是在很多地方踩了坑,本文就是疏理和分享我在这些关键点处的疑惑和我的看法。(本文讨论范围仅是Basic Raft算法)安全
不管是Raft算法仍是Paxos算法,都依赖于复制状态机的模型,复制状态机的模型以下图所示:网络
简单的描述下这个模型就是:集群的多个节点上都拥有相同的初始状态(state),而经过执行一系列的操做日志(log),最终仍是可以产生一致的状态。在图中的Consensus模块就是Raft算法起做用的模块,使得在复杂的分布式环境下,仍可以实现复制状态机的一致性。 图中的四个操做相应以下:数据结构
咱们在后面能够看到Raft算法的所有内容都是围绕着这个模型出发的,因此理解这个模型十分的重要,虽然看起来流程很简单,可是仍是要考虑不少的边界条件,这也是为何Raft算法有不少补丁的缘故,好在Raft算法的做者都给出了相应的解决方案,业内也有不少公司也一直在作着对Raft算法进行优化的工做。并发
Raft算法和Paxos算法同样,也提供了如下的分布式算法的特性:app
- 安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误状况下,包括网络延迟、分区、丢包、冗余和乱序等错误均可以保证正确。
- 可用性:集群中只要有大多数的机器可运行而且可以相互通讯、和客户端通讯,就能够保证可用。所以,一个典型的包含 5 个节点的集群能够容忍两个节点的失败。服务器被中止就认为是失败。他们当有稳定的存储的时候能够从状态中恢复回来并从新加入集群。
- 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏状况下才会致使可用性问题。
- 一般状况下,一条指令能够尽量快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统总体的性能。
相对于Paxos来讲,Raft一个重要的简化操做就是使用随机选举超时时间来代替原来Paxos复杂的两阶段提交的策略。使用随机时间虽然会致使选举没有那么高效,可是大大下降了复杂度,而牺牲的仅仅是在选主时的效率,在平常的使用过程当中,选主的时间仅仅只占正常使用时间的不多一部分时间。咱们能够从原论文的这张图中看到:分布式
在正常使用过程当中,Raft算法和Paxos算法性能相差不大,只有在比较极端的状况下,即Leader频繁崩溃,Raft算法才会在选举时间上被Paxos算法甩开。
与随机选举超时时间相辅相成的是定时器和心跳包机制,追随者经过经过定时器来竞争成为候选者,而领导者经过心跳包来“压制”追随者,从而保持本身的领导地位,而经过随机选举超时时间可以尽可能避免在Paxos算法中讨论的选票瓜分的状况的发生。这样的确是简单易行的选主方法。可是仍是留有两个漏洞须要解决:
在Basic Raft算法中一共只有两种RPC请求,分别是附加日志RPC和请求投票RPC,具体的参数以下表所示:
附加日志RPC:
参数 | 解释 |
---|---|
term | 领导人的任期号 |
leaderId | 领导人的 Id,以便于跟随者重定向请求 |
prevLogIndex | 新的日志条目紧随以前的索引值 |
prevLogTerm | prevLogIndex 条目的任期号 |
entries[] | 准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提升效率) |
leaderCommit | 领导人已经提交的日志的索引值 |
其余的参数都比较好理解,可是第一次看prevLogIndex、prevLogIndex这两个参数时,很很差理解。下面来详细说明一下个两个参数。
这两个参数主要是为了保证算法的一致性而存在的。(详细的一致性检查说明能够去看下面的第四小节)
关于这个参数的说明以下:
若是日志在 prevLogIndex 位置处的日志条目的任期号和 prevLogTerm 不匹配,则返回 false
这句话即意为当领导者和追随者的日志列表冲突时,追随者会返回false给领导者,领导者得知后会发送以往的日志,最终覆盖冲突的日志段。从而就可以保证Raft算法的日志匹配特性:
若是两个日志在相同的索引位置的日志条目的任期号相同,那么咱们就认为这个日志从头到这个索引位置之间所有彻底相同
请求投票RPC
参数 | 解释 |
---|---|
term | 候选人的任期号 |
candidateId | 请求选票的候选人的 Id |
lastLogIndex | 候选人的最后日志条目的索引值 |
lastLogTerm | 候选人最后日志条目的任期号 |
下面来详细说明一下lastLogIndex和lastLogTerm这两个参数。首先这两个参数是为了领导者选举服务的,是为了在领导者选举时排除掉不包含最新日志的节点,从而避免已提交日志被覆盖的状况的发生。(可是并不能彻底避免,在下面第五节提出了这样仍会有一个小漏洞)
首先咱们来看一下候选者是在什么状况下才能得到选票。
再结合Raft算法的运行机制:
Raft 算法保证全部已提交的日志条目都是持久化的而且最终会被全部可用的状态机执行。在领导人将建立的日志条目复制到大多数的服务器上的时候,日志条目就会被提交。
由于候选者成为新任领导者必需要得到大多数节点的选票,而若是该候选者没有最新一条日志的信息,必定不会获得大多数节点的选票,这样一来就保证了被选举出来的新领导者必定会包含最新的一条日志,从而保证了系统的安全性。
接下来继续聊一聊Raft算法是怎么保证一致性的。
数据不一致的断定标准
首先得有一个标准来判断数据是否是一致的,Raft算法设计了这个样一个规则:
领导人最多在一个任期里在指定的一个日志索引位置建立一条日志条目,同时日志条目在日志中的位置也历来不会改变。
有了这个规则,就很好判断不一致了,若是须要附加的新日志的上一条日志的任期或索引和领导者所记录的(领导者须要维护这样一个元数据信息)不一致,那么就表明发生了不一致的状况。
Raft算法有一个领导人只附加原则:
领导人绝对不会删除或者覆盖本身的日志,只会增长。
这里引伸出来的意思就是,当数据不一致的状况发生时,一切以领导人的数据为准,领导者会经过强制复制本身的日志到数据不一致的追随者,从而使得集群中的全部节点的数据保持一致性。这里会有一个小疑问:若是新领导者覆盖了旧领导者的日志呢?这种状况是必定会发生的,可是咱们从上一小节得知,新任领导者必定会包含最新一条日志,即新任领导者的数据和旧领导者的数据就算是不一致的,也仅仅是未提交的日志,即客户端还没有获得回复,此时就算是新任领导者覆盖旧领导者的数据,客户端获得回复,持久化日志失败。从客户端的角度来开,并无产生数据不一致的状况。
日志被应用到各个节点的状态机须要通过两个阶段:
这个和两阶段提交协议差很少,区别是不用全部的节点都要复制成功,只要有一半多的节点正常响应,就能维持集群的正常运行,对于那些暂时不可以正常响应的追随者,领导者会持续不断的发送RPC,直到全部的节点都能存储一致的数据。
日志不一致的处理策略
当附加日志RPC的一致性检查失败时,追随者会拒绝这个请求。当领导者检测到附加日志的请求失败后,会减少当前附加日志的索引值,再次尝试附加日志,直至成功。为了减小领导者的附加日志RPC被拒绝的次数,能够作一个小优化,当追随者拒绝领导者的附加日志请求时,追随者能够返回包含冲突的条目的任期号和本身存储的那个任期的最先的索引地址,从而使得领导人和追随者尽快找到最后二者达成一致的地方。在下面的第八小节还能够看到,当追随者和领导者之间的日志相差过大时,领导者会直接发送快照来快速达到一致。
经过以上四条规则,咱们能够看到领导者并不须要耗费不少的资源,就能够管理全部节点的一致性,经过不断发送附加日志RPC(或心跳包),集群的节点就会自动趋于一致。固然从客户端的角度来看,Raft算法提供的强一致性的特性。
下面咱们来看看在第三小节提到的小漏洞,在原论文的5.4.2节中,给出了下面这个例子,演示了一条已经被提交的日志在将来被其余的领导人日志覆盖的状况。
如图的时间序列展现了为何领导人没法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,而后 S5 在任期 3 里经过 S三、S4 和本身的选票赢得选举,而后从客户端接收了一条不同的日志条目放在了索引 2 处。而后到 (c),S5 又崩溃了;S1 从新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,可是尚未被提交。若是 S1 在 (d) 中又崩溃了,S5 能够从新被选举成功(经过来自 S2,S3 和 S4 的选票),而后覆盖了他们在索引 2 处的日志。反之,若是在崩溃以前,S1 把本身主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(由于S5 就不可能选举成功)。 这样在同一时刻就同时保证了,以前的全部老的日志条目就会被提交。
为了不上面的状况,这里又引出了Raft算法的另外一个补丁:Raft 永远不会经过计算副本数目的方式去提交一个以前任期内的日志条目。这个补丁是什么意思呢?注意在时间戳为(c)的时刻,此时S1为领导人,S1发现了在他的上个任期内提交的日志(即任期为2的第一个日志)尚未被大多数的追随者复制。因此S1将该日志发送给S3,而S3检查了该日志发现知足条件:
因此S3复制该日志到本地日志中,修改S3 currentTerm = 2, 而后返回给领导人S1,领导人经过计算已复制的追随者的数量超过了一半,遂发起提交,而后返回给客户端(client):“我已经将该日志保存好了,能够放心使用了。” 然而在时间戳为(d)的时刻,S1崩溃,而S5成为候选人,开始收集选票,这时候来分析S5和关键人S3之间的关系:
那么若是S5向S3收集选票,尽管S5上并无任期号为2的日志,而S3上有S1在任期号为4时传播上个任期的日志,可是按照Raft选举的规则,S3仍是会给S5投一票。又由于在Raft算法中:
领导人处理不一致是经过强制跟随者直接复制本身的日志来解决了。
因此任期为2的第一个日志,会由于这个缘由被覆盖掉了,那么客户端若是来访问,就会发现访问先后的数据是不一致的,这是不可以容忍的。那么若是领导人想要提交之前任期的日志呢?这个关键点就在于在时间戳为(d)的时刻,不可以让缺乏了关键日志的S5成为领导人。那么只要在时间戳为(c)的时刻,有任期号为4的日志可以在系统中提交,而上个任期的日志就可以跟随着这个任期号为4的日志一块儿提交,如时间戳为(d)时所示,即便紧接着领导人S1崩溃,由于Raft算法选举的限制:
候选人为了赢得选举必须联系集群中的大部分节点,这意味着每个已经提交的日志条目在这些服务器节点中确定存在于至少一个节点上。
那么就会使得即便在选举出来的新的领导人中,也会有这个任期号为2的这个日志,从而避免了领导人日志覆盖提交日志的状况。
因为Raft算法和集群成员的数量的关系联系的十分紧密,因此在集群扩容的时候,要十分的谨慎,若是不使用暂停整个集群更换配置的方案,而莽撞的直接添加机器会致使种种问题,如在同一个时间内,在集群中选举出了两个领导者,在原论文中给出了这样的实例:
直接从一种配置转到新的配置是十分不安全的,由于各个机器可能在任何的时候进行转换。在这个例子中,集群配额从 3 台机器变成了 5 台。不幸的是,存在这样的一个时间点,两个不一样的领导人在同一个任期里均可以被选举成功。一个是经过旧的配置,一个经过新的配置。
这个问题就衍生为Raft算法在不暂停服务的状况下,完成配置变动。所以Raft算法提出了一个称之为共同一致的方案,这个方案指出在配置变动过程当中,对安全性最有影响的是新老配置互相没法感知,而配置更替也没法一蹴而就。因此在配置更替前,将集群引导入一个过渡阶段,使得适用新的配置和老的配置的机器都不会独立地处理日志。
上图是原论文的图示,咱们能够看到在配置更替的中间阶段,会写入一个特殊的日志C(old,new),这个日志包含了新配置和老配置,当咱们向领导者提出要采用新配置时,领导者就会在集群内传播这个特殊日志,当追随者收到这个日志的第一时间,就会应用该配置。当大多数的追随者都应用了该配置后,领导者就会回应咱们已经成功应用配置了。在此之后 虽然配置没有彻底更替完毕,可是即便在领导者崩溃的状况下,因为Raft算法选举限制,最终选出的新领导者也必定会包含新的配置。最终当全部追随者的配置更新完毕,若是旧的领导者并不包含在新的集群当中,旧的领导者会退位让贤,主动放弃本身的领导。
在新节点加入集群的时候,还有一个小补丁,咱们再来看看上面图四的场景,这里先排除掉会选出多个领导者的状况,咱们假设此时有一个客服端发起写入日志的请求,领导者收到后经过附加日志RPC发送给各个节点,由于此时4节点和5节点是新加入的节点,因此确定会返回附加日志失败给领导者,那么此时若是日志要提交成功,原来的一、二、3节点必须所有复制成功,这至关于在没有机器宕机或网络异常的状况下,自行下降了系统的可用性。
为了解决这个问题,又提出了一个新补丁,当有新的节点加入到集群中时,只会被动地从领导者那里接收日志,而不会参与到日志复制的决策当中,即没有投票权。当新的节点追遇上了其余的节点后,才会拥有投票权。
Raft日志快照主要有两种用途:
压缩日志
当系统中的日志愈来愈多后,会占用大量的空间,Raft算法采用了快照机制来压缩庞大的日志,在某个时间点,将整个系统的全部状态稳定地写入到可持久化存储中,而后这个时间点后的全部日志所有清除。
快照RPC
当咱们拥有了快照以后,就能经过快照直接将领导者的状态复制到那些过于落后追随者上,从而使得追随者和领导者的状态可以快速到达一致。
咱们能够看到Raft的快照机制和Redis的持久化存储是很相像的。因此一些Redis的优化机制能够有选择地应用到Raft之中,如快照自动触发、使用fork机制来下降建立快照的资源占用、使用特殊的数据结构来保证快照的可检测性等。
这部分的内容实际上不太属于Raft算法,可是仍是十分的重要,我在这里简单的作一点介绍。
只要是经过网络进行交互,就要考虑到容错和重发,若是由于瞬间的网络波动致使客户端重发请求,而服务端若是没有正确处理请求,就会致使数据混乱的状况发生,因此这里就要引入幂等性的这个概念,幂等性是指一个操做不管执行多少遍,都会产生相同的状态,如绝对值操做就是属于幂等操做,而加减法就不属于幂等操做。
而不少请求都是不属于幂等操做,因此咱们须要在这些请求外设置一些限制,从而达到幂等操做的效果。 就拿Raft算法为例,咱们须要给每个客户端分配一个全局惟一的ID,而领导者记录每一个客户端已处理的请求的最大的序号(这个序号须要在客户端组范围内是递增的),当领导者收到一个序号小于或等于已记录最大序号的请求时,就要拒绝请求并返回异常给客户端,客户端捕获异常后,自行处理。
如此一来,就能够从入口和内部两方面都可以保证数据一致性和安全性。
这篇文章是我我的的一些对于Raft算法的理解,在写文章的过程当中,发现了一些大牛的文章,这些国内的大牛(知乎上的我作分布式系统
、TiKV的唐刘
....)是真滴强,在我还在研究Basic Raft的时候,他们已经在Raft算法优化的路上走了很远了。看了这么多资料,就愈是以为本身菜,因此写的就愈是艰难。若是个人理解有什么纰漏,还请不吝赐教。
邀舞卡:王老魔的代码备忘录