摘要算法
Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,可是它的算法结构和 Paxos 不一样,使得 Raft 算法更加容易理解而且更容易构建实际的系统。为了提高可理解性,Raft 将一致性算法分解成了几个关键模块,例如领导人选举、日志复制和安全性。同时它经过实施一个更强的一致性来减小须要考虑的状态的数量。从一个用户研究的结果能够证实,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来容许集群成员的动态改变,它利用重叠的大多数来保证安全性。安全
一致性算法容许一组机器像一个总体同样工做,即便其中一些机器出现故障也可以继续工做下去。正由于如此,一致性算法在构建可信赖的大规模软件系统中扮演着重要的角色。在过去的 10 年里,Paxos 算法统治着一致性算法这一领域:绝大多数的实现都是基于 Paxos 或者受其影响。同时 Paxos 也成为了教学领域里讲解一致性问题时的示例。服务器
可是不幸的是,尽管有不少工做都在尝试下降它的复杂性,可是 Paxos 算法依然十分难以理解。而且,Paxos 自身的算法结构须要进行大幅的修改才可以应用到实际的系统中。这些都致使了工业界和学术界都对 Paxos 算法感到十分头疼。网络
和 Paxos 算法进行过努力以后,咱们开始寻找一种新的一致性算法,能够为构建实际的系统和教学提供更好的基础。咱们的作法是不寻常的,咱们的首要目标是可理解性:咱们是否能够在实际系统中定义一个一致性算法,而且可以比 Paxos 算法以一种更加容易的方式来学习。此外,咱们但愿该算法方便系统构建者的直觉的发展。不只一个算法可以工做很重要,并且可以显而易见的知道为何能工做也很重要。并发
Raft 一致性算法就是这些工做的结果。在设计 Raft 算法的时候,咱们使用一些特别的技巧来提高它的可理解性,包括算法分解(Raft 主要被分红了领导人选举,日志复制和安全三个模块)和减小状态机的状态(相对于 Paxos,Raft 减小了非肯定性和服务器互相处于非一致性的方式)。一份针对两所大学 43 个学生的研究代表 Raft 明显比 Paxos 算法更加容易理解。在这些学生同时学习了这两种算法以后,和 Paxos 比起来,其中 33 个学生可以回答有关于 Raft 的问题。分布式
Raft 算法在许多方面和现有的一致性算法都很类似(主要是 Oki 和 Liskov 的 Viewstamped Replication),可是它也有一些独特的特性:性能
咱们相信,Raft 算法不论出于教学目的仍是做为实践项目的基础都是要比 Paxos 或者其余一致性算法要优异的。它比其余算法更加简单,更加容易理解;它的算法描述足以实现一个现实的系统;它有好多开源的实现而且在不少公司里使用;它的安全性已经被证实;它的效率和其余算法比起来也不相上下。学习
接下来,这篇论文会介绍如下内容:复制状态机问题(第 2 节),讨论 Paxos 的优势和缺点(第 3 节),讨论咱们为了可理解性而采起的方法(第 4 节),阐述 Raft 一致性算法(第 5-8 节),评价 Raft 算法(第 9 节),以及一些相关的工做(第 10 节)。区块链
一致性算法是从复制状态机的背景下提出的(参考英文原文引用37)。在这种方法中,一组服务器上的状态机产生相同状态的副本,而且在一些机器宕掉的状况下也能够继续运行。复制状态机在分布式系统中被用于解决不少容错的问题。例如,大规模的系统中一般都有一个集群领导者,像 GFS、HDFS 和 RAMCloud,典型应用就是一个独立的的复制状态机去管理领导选举和存储配置信息而且在领导人宕机的状况下也要存活下来。好比 Chubby 和 ZooKeeper。优化
图 1 :复制状态机的结构。一致性算法管理着来自客户端指令的复制日志。状态机从日志中处理相同顺序的相同指令,因此产生的结果也是相同的。
复制状态机一般都是基于复制日志实现的,如图 1。每个服务器存储一个包含一系列指令的日志,而且按照日志的顺序进行执行。每个日志都按照相同的顺序包含相同的指令,因此每个服务器都执行相同的指令序列。由于每一个状态机都是肯定的,每一次执行操做都产生相同的状态和一样的序列。
保证复制日志相同就是一致性算法的工做了。在一台服务器上,一致性模块接收客户端发送来的指令而后增长到本身的日志中去。它和其余服务器上的一致性模块进行通讯来保证每个服务器上的日志最终都以相同的顺序包含相同的请求,尽管有些服务器会宕机。一旦指令被正确的复制,每个服务器的状态机按照日志顺序处理他们,而后输出结果被返回给客户端。所以,服务器集群看起来造成一个高可靠的状态机。
实际系统中使用的一致性算法一般含有如下特性:
在过去的 10 年里,Leslie Lamport 的 Paxos 算法几乎已经成为一致性的代名词:Paxos 是在课程教学中最常用的算法,同时也是大多数一致性算法实现的起点。Paxos 首先定义了一个可以达成单一决策一致的协议,好比单条的复制日志项。咱们把这一子集叫作单决策 Paxos。而后经过组合多个 Paxos 协议的实例来促进一系列决策的达成。Paxos 保证安全性和活性,同时也支持集群成员关系的变动。Paxos 的正确性已经被证实,在一般状况下也很高效。
不幸的是,Paxos 有两个明显的缺点。第一个缺点是 Paxos 算法特别的难以理解。完整的解释是出了名的不透明;经过极大的努力以后,也只有少数人成功理解了这个算法。所以,有了几回用更简单的术语来解释 Paxos 的尝试。尽管这些解释都只关注了单决策的子集问题,但依然很具备挑战性。在 2012 年 NSDI 的会议中的一次调查显示,不多有人对 Paxos 算法感到满意,甚至在经验老道的研究者中也是如此。咱们本身也尝试去理解 Paxos;咱们一直没能理解 Paxos 直到咱们读了不少对 Paxos 的简化解释而且设计了咱们本身的算法以后,这一过程花了近一年时间。
咱们假设 Paxos 的不透明性来自它选择单决策问题做为它的基础。单决策 Paxos 是晦涩微妙的,它被划分红了两种没有简单直观解释和没法独立理解的情景。所以,这致使了很难创建起直观的感觉为何单决策 Paxos 算法可以工做。构成多决策 Paxos 增长了不少错综复杂的规则。咱们相信,在多决策上达成一致性的问题(一份日志而不是单一的日志记录)可以被分解成其余的方式而且更加直接和明显。
Paxos算法的第二个问题就是它没有提供一个足够好的用来构建一个现实系统的基础。一个缘由是尚未一种被普遍认同的多决策问题的算法。Lamport 的描述基本上都是关于单决策 Paxos 的;他简要描述了实施多决策 Paxos 的方法,可是缺少不少细节。固然也有不少具体化 Paxos 的尝试,可是他们都互相不同,和 Paxos 的概述也不一样。例如 Chubby 这样的系统实现了一个相似于 Paxos 的算法,可是大多数的细节并无被公开。
并且,Paxos 算法的结构也不是十分易于构建实践的系统;单决策分解也会产生其余的结果。例如,独立的选择一组日志条目而后合并成一个序列化的日志并无带来太多的好处,仅仅增长了很多复杂性。围绕着日志来设计一个系统是更加简单高效的;新日志条目以严格限制的顺序增添到日志中去。另外一个问题是,Paxos 使用了一种对等的点对点的方式做为它的核心(尽管它最终提议了一种弱领导人的方法来优化性能)。在只有一个决策会被制定的简化世界中是颇有意义的,可是不多有现实的系统使用这种方式。若是有一系列的决策须要被制定,首先选择一个领导人,而后让他去协调全部的决议,会更加简单快速。
所以,实际的系统中不多有和 Paxos 类似的实践。每一种实现都是从 Paxos 开始研究,而后发现不少实现上的难题,再而后开发了一种和 Paxos 明显不同的结构。这样是很是费时和容易出错的,而且理解 Paxos 的难度使得这个问题更加糟糕。Paxos 算法在理论上被证实是正确可行的,可是现实的系统和 Paxos 差异是如此的大,以致于这些证实没有什么太大的价值。下面来自 Chubby 实现很是典型:
在Paxos算法描述和实现现实系统中间有着巨大的鸿沟。最终的系统创建在一种没有通过证实的算法之上。
因为以上问题,咱们认为 Paxos 算法既没有提供一个良好的基础给实践的系统,也没有给教学很好的帮助。基于一致性问题在大规模软件系统中的重要性,咱们决定看看咱们是否能够设计一个拥有更好特性的替代 Paxos 的一致性算法。Raft算法就是此次实验的结果。
设计 Raft 算法咱们有几个初衷:它必须提供一个完整的实际的系统实现基础,这样才能大大减小开发者的工做;它必须在任何状况下都是安全的而且在大多数的状况下都是可用的;而且它的大部分操做必须是高效的。可是咱们最重要也是最大的挑战是可理解性。它必须保证对于广泛的人群均可以十分容易的去理解。另外,它必须可以让人造成直观的认识,这样系统的构建者才可以在现实中进行必然的扩展。
在设计 Raft 算法的时候,有不少的点须要咱们在各类备选方案中进行选择。在这种状况下,咱们评估备选方案基于可理解性原则:解释各个备选方案有多大的难度(例如,Raft 的状态空间有多复杂,是否有微妙的暗示)?对于一个读者而言,彻底理解这个方案和暗示是否容易?
咱们意识到对这种可理解性分析上具备高度的主观性;尽管如此,咱们使用了两种一般适用的技术来解决这个问题。第一个技术就是众所周知的问题分解:只要有可能,咱们就将问题分解成几个相对独立的,可被解决的、可解释的和可理解的子问题。例如,Raft 算法被咱们分红领导人选举,日志复制,安全性和角色改变几个部分。
咱们使用的第二个方法是经过减小状态的数量来简化须要考虑的状态空间,使得系统更加连贯而且在可能的时候消除不肯定性。特别的,全部的日志是不容许有空洞的,而且 Raft 限制了日志之间变成不一致状态的可能。尽管在大多数状况下咱们都试图去消除不肯定性,可是也有一些状况下不肯定性能够提高可理解性。尤为是,随机化方法增长了不肯定性,可是他们有利于减小状态空间数量,经过处理全部可能选择时使用类似的方法。咱们使用随机化去简化 Raft 中领导人选举算法。
Raft 是一种用来管理章节 2 中描述的复制日志的算法。图 2 为了参考之用,总结这个算法的简略版本,图 3 列举了这个算法的一些关键特性。图中的这些元素会在剩下的章节逐一介绍。
Raft 经过选举一个高贵的领导人,而后给予他所有的管理复制日志的责任来实现一致性。领导人从客户端接收日志条目,把日志条目复制到其余服务器上,而且当保证安全性的时候告诉其余的服务器应用日志条目到他们的状态机中。拥有一个领导人大大简化了对复制日志的管理。例如,领导人能够决定新的日志条目须要放在日志中的什么位置而不须要和其余服务器商议,而且数据都从领导人流向其余服务器。一个领导人能够宕机,能够和其余服务器失去链接,这时一个新的领导人会被选举出来。
经过领导人的方式,Raft 将一致性问题分解成了三个相对独立的子问题,这些问题会在接下来的子章节中进行讨论:
在展现一致性算法以后,这一章节会讨论可用性的一些问题和计时在系统的做用。
状态:
状态 | 全部服务器上持久存在的 |
---|---|
currentTerm | 服务器最后一次知道的任期号(初始化为 0,持续递增) |
votedFor | 在当前得到选票的候选人的 Id |
log[] | 日志条目集;每个条目包含一个用户状态机执行的指令,和收到时的任期号 |
状态 | 全部服务器上常常变的 |
---|---|
commitIndex | 已知的最大的已经被提交的日志条目的索引值 |
lastApplied | 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增) |
状态 | 在领导人里常常改变的 (选举后从新初始化) |
---|---|
nextIndex[] | 对于每个服务器,须要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一) |
matchIndex[] | 对于每个服务器,已经复制给他的日志的最高索引值 |
附加日志 RPC:
由领导人负责调用来复制日志指令;也会用做heartbeat
参数 | 解释 |
---|---|
term | 领导人的任期号 |
leaderId | 领导人的 Id,以便于跟随者重定向请求 |
prevLogIndex | 新的日志条目紧随以前的索引值 |
prevLogTerm | prevLogIndex 条目的任期号 |
entries[] | 准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提升效率) |
leaderCommit | 领导人已经提交的日志的索引值 |
返回值 | 解释 |
---|---|
term | 当前的任期号,用于领导人去更新本身 |
success | 跟随者包含了匹配上 prevLogIndex 和 prevLogTerm 的日志时为真 |
接收者实现:
term < currentTerm
就返回 false (5.1 节)leaderCommit > commitIndex
,令 commitIndex 等于 leaderCommit 和 新日志条目索引值中较小的一个请求投票 RPC:
由候选人负责调用用来征集选票(5.2 节)
参数 | 解释 |
---|---|
term | 候选人的任期号 |
candidateId | 请求选票的候选人的 Id |
lastLogIndex | 候选人的最后日志条目的索引值 |
lastLogTerm | 候选人最后日志条目的任期号 |
返回值 | 解释 |
---|---|
term | 当前任期号,以便于候选人去更新本身的任期号 |
voteGranted | 候选人赢得了此张选票时为真 |
接收者实现:
term < currentTerm
返回 false (5.2 节)全部服务器需遵照的规则:
全部服务器:
commitIndex > lastApplied
,那么就 lastApplied 加一,并把log[lastApplied]
应用到状态机中(5.3 节)T > currentTerm
,那么就令 currentTerm 等于 T,并切换状态为跟随者(5.1 节)跟随者(5.2 节):
候选人(5.2 节):
领导人:
N > commitIndex
的 N,而且大多数的matchIndex[i] ≥ N
成立,而且log[N].term == currentTerm
成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节)图 2:一个关于 Raft 一致性算法的浓缩总结(不包括成员变换和日志压缩)。
特性 | 解释 |
---|---|
选举安全特性 | 对于一个给定的任期号,最多只会有一个领导人被选举出来(5.2 节) |
领导人只附加原则 | 领导人绝对不会删除或者覆盖本身的日志,只会增长(5.3 节) |
日志匹配原则 | 若是两个日志在相同的索引位置的日志条目的任期号相同,那么咱们就认为这个日志从头到这个索引位置之间所有彻底相同(5.3 节) |
领导人彻底特性 | 若是某个日志条目在某个任期号中已经被提交,那么这个条目必然出如今更大任期号的全部领导人中(5.4 节) |
状态机安全特性 | 若是一个领导人已经在给定的索引值位置的日志条目应用到状态机中,那么其余任何的服务器在这个索引位置不会提交一个不一样的日志(5.4.3 节) |
图 3:Raft 在任什么时候候都保证以上的各个特性。
一个 Raft 集群包含若干个服务器节点;一般是 5 个,这容许整个系统容忍 2 个节点的失效。在任什么时候刻,每个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。在一般状况下,系统中只有一个领导人而且其余的节点所有都是跟随者。跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。领导人处理全部的客户端请求(若是一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。第三种状态,候选人,是用来在 5.2 节描述的选举新领导人时使用。图 4 展现了这些状态和他们之间的转换关系;这些转换关系会在接下来进行讨论。
图 4:服务器状态。跟随者只响应来自其余服务器的请求。若是跟随者接收不到消息,那么他就会变成候选人并发起一次选举。得到集群中大多数选票的候选人将成为领导者。在一个任期内,领导人一直都会是领导人直到本身宕机了。
图 5:时间被划分红一个个的任期,每一个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换能够在不一样的时间不一样的服务器上观察到。
Raft 把时间分割成任意长度的任期,如图 5。任期用连续的整数标记。每一段任期从一次选举开始,就像章节 5.2 描述的同样,一个或者多个候选人尝试成为领导者。若是一个候选人赢得选举,而后他就在接下来的任期内充当领导人的职责。在某些状况下,一次选举过程会形成选票的瓜分。在这种状况下,这一任期会以没有领导人结束;一个新的任期(和一次新的选举)会很快从新开始。Raft 保证了在一个给定的任期内,最多只有一个领导者。
不一样的服务器节点可能屡次观察到任期之间的转换,但在某些状况下,一个节点也可能观察不到任何一次选举或者整个任期全程。任期在 Raft 算法中充当逻辑时钟的做用,这会容许服务器节点查明一些过时的信息好比陈旧的领导者。每个节点存储一个当前任期号,这一编号在整个时期内单调的增加。当服务器之间通讯的时候会交换当前任期号;若是一个服务器的当前任期号比其余人小,那么他会更新本身的编号到较大的编号值。若是一个候选人或者领导者发现本身的任期号过时了,那么他会当即恢复成跟随者状态。若是一个节点接收到一个包含过时的任期号的请求,那么他会直接拒绝这个请求。
Raft 算法中服务器节点之间通讯使用远程过程调用(RPCs),而且基本的一致性算法只须要两种类型的 RPCs。请求投票(RequestVote) RPCs 由候选人在选举期间发起(章节 5.2),而后附加条目(AppendEntries)RPCs 由领导人发起,用来复制日志和提供一种心跳机制(章节 5.3)。第 7 节为了在服务器之间传输快照增长了第三种 RPC。当服务器没有及时的收到 RPC 的响应时,会进行重试, 而且他们可以并行的发起 RPCs 来得到最佳的性能。
Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是跟随者身份。一个服务器节点继续保持着跟随者状态只要他从领导人或者候选者处接收到有效的 RPCs。领导者周期性的向全部跟随者发送心跳包(即不包含日志项内容的附加日志项 RPCs)来维持本身的权威。若是一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,那么他就会认为系统中没有可用的领导者,而且发起选举以选出新的领导者。
要开始一次选举过程,跟随者先要增长本身的当前任期号而且转换到候选人状态。而后他会并行的向集群中的其余服务器节点发送请求投票的 RPCs 来给本身投票。候选人会继续保持着当前状态直到如下三件事情之一发生:(a) 他本身赢得了此次的选举,(b) 其余的服务器成为领导者,(c) 一段时间以后没有任何一个获胜的人。这些结果会分别的在下面的段落里进行讨论。
当一个候选人从整个集群的大多数服务器节点得到了针对同一个任期号的选票,那么他就赢得了此次选举并成为领导人。每个服务器最多会对一个任期号投出一张选票,按照先来先服务的原则(注意:5.4 节在投票上增长了一点额外的限制)。要求大多数选票的规则确保了最多只会有一个候选人赢得这次选举(图 3 中的选举安全性)。一旦候选人赢得选举,他就当即成为领导人。而后他会向其余的服务器发送心跳消息来创建本身的权威而且阻止新的领导人的产生。
在等待投票的时候,候选人可能会从其余的服务器接收到声明它是领导人的附加日志项 RPC。若是这个领导人的任期号(包含在这次的 RPC中)不小于候选人当前的任期号,那么候选人会认可领导人合法并回到跟随者状态。 若是这次 RPC 中的任期号比本身小,那么候选人就会拒绝此次的 RPC 而且继续保持候选人状态。
第三种可能的结果是候选人既没有赢得选举也没有输:若是有多个跟随者同时成为候选人,那么选票可能会被瓜分以致于没有候选人能够赢得大多数人的支持。当这种状况发生的时候,每个候选人都会超时,而后经过增长当前任期号来开始一轮新的选举。然而,没有其余机制的话,选票可能会被无限的重复瓜分。
Raft 算法使用随机选举超时时间的方法来确保不多会发生选票瓜分的状况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样能够把服务器都分散开以致于在大多数状况下只有一个服务器会选举超时;而后他赢得选举并在其余服务器超时以前发送心跳包。一样的机制被用在选票瓜分的状况下。每个候选人在开始一次选举的时候会重置一个随机的选举超时时间,而后在超时时间内等待投票的结果;这样减小了在新的选举中另外的选票瓜分的可能性。9.3 节展现了这种方案可以快速的选出一个领导人。
领导人选举这个例子,体现了可理解性原则是如何指导咱们进行方案设计的。起初咱们计划使用一种排名系统:每个候选人都被赋予一个惟一的排名,供候选人之间竞争时进行选择。若是一个候选人发现另外一个候选人拥有更高的排名,那么他就会回到跟随者状态,这样高排名的候选人可以更加容易的赢得下一次选举。可是咱们发现这种方法在可用性方面会有一点问题(若是高排名的服务器宕机了,那么低排名的服务器可能会超时并再次进入候选人状态。并且若是这个行为发生得足够快,则可能会致使整个选举过程都被重置掉)。咱们针对算法进行了屡次调整,可是每次调整以后都会有新的问题。最终咱们认为随机重试的方法是更加明显和易于理解的。
一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每个请求都包含一条被复制状态机执行的指令。领导人把这条指令做为一条新的日志条目附加到日志中去,而后并行的发起附加条目 RPCs 给其余的服务器,让他们复制这条日志条目。当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中而后把执行的结果返回给客户端。若是跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到全部的跟随者都最终存储了全部的日志条目。
图 6:日志由有序序号标记的条目组成。每一个条目都包含建立时的任期号(图中框中的数字),和一个状态机须要执行的指令。一个条目当能够安全的被应用到状态机中去的时候,就认为是能够提交了。
日志以图 6 展现的方式组织。每个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的状况,同时也用来保证图 3 中的某些性质。每一条日志条目同时也都有一个整数索引值来代表它在日志中的位置。
领导人来决定何时把日志条目应用到状态机中是安全的;这种日志条目被称为已提交。Raft 算法保证全部已提交的日志条目都是持久化的而且最终会被全部可用的状态机执行。在领导人将建立的日志条目复制到大多数的服务器上的时候,日志条目就会被提交(例如在图 6 中的条目 7)。同时,领导人的日志中以前的全部日志条目也都会被提交,包括由其余领导人建立的条目。5.4 节会讨论某些当在领导人改变以后应用这条规则的隐晦内容,同时他也展现了这种提交的定义是安全的。领导人跟踪了最大的将会被提交的日志项的索引,而且索引值会被包含在将来的全部附加日志 RPCs (包括心跳包),这样其余的服务器才能最终知道领导人的提交位置。一旦跟随者知道一条日志条目已经被提交,那么他也会将这个日志条目应用到本地的状态机中(按照日志的顺序)。
咱们设计了 Raft 的日志机制来维护一个不一样服务器的日志之间的高层次的一致性。这么作不只简化了系统的行为也使得更加可预计,同时他也是安全性保证的一个重要组件。Raft 维护着如下的特性,这些同时也组成了图 3 中的日志匹配特性:
第一个特性来自这样的一个事实,领导人最多在一个任期里在指定的一个日志索引位置建立一条日志条目,同时日志条目在日志中的位置也历来不会改变。第二个特性由附加日志 RPC 的一个简单的一致性检查所保证。在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着以前的条目的索引位置和任期号包含在里面。若是跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个概括步骤:一开始空的日志状态确定是知足日志匹配特性的,而后一致性检查保护了日志匹配特性当日志扩展的时候。所以,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志必定是和本身相同的了。
在正常的操做中,领导人和跟随者的日志保持一致性,因此附加日志 RPC 的一致性检查历来不会失败。然而,领导人崩溃的状况会使得日志处于不一致的状态(老的领导人可能尚未彻底复制全部的日志条目)。这种不一致问题会在领导人和跟随者的一系列崩溃下加重。图 7 展现了跟随者的日志可能和新的领导人不一样的方式。跟随者可能会丢失一些在新的领导人中有的日志条目,他也可能拥有一些领导人没有的日志条目,或者二者都发生。丢失或者多出日志条目可能会持续多个任期。
图 7:当一个领导人成功当选时,跟随者多是任何状况(a-f)。每个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺乏一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种状况都存在(e-f)。例如,场景 f 可能会这样发生,某服务器在任期 2 的时候是领导人,已附加了一些日志条目到本身的日志中,但在提交以前就崩溃了;很快这个机器就被重启了,在任期 3 从新被选为领导人,而且又增长了一些日志条目到本身的日志中;在任期 2 和任期 3 的日志被提交以前,这个服务器又宕机了,而且在接下来的几个任期里一直处于宕机状态。
在 Raft 算法中,领导人处理不一致是经过强制跟随者直接复制本身的日志来解决了。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。5.4 节会阐述如何经过增长一些限制来使得这样的操做是安全的。
要使得跟随者的日志进入和本身一致的状态,领导人必须找到最后二者达成一致的地方,而后删除从那个点以后的全部日志条目,发送本身的日志给跟随者。全部的这些操做都在进行附加日志 RPCs 的一致性检查时完成。领导人针对每个跟随者维护了一个 nextIndex,这表示下一个须要发送给跟随者的日志条目的索引地址。当一个领导人刚得到权力的时候,他初始化全部的 nextIndex 值为本身的最后一条日志的index加1(图 7 中的 11)。若是一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。在被跟随者拒绝以后,领导人就会减少 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种状况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目所有删除而且加上领导人的日志。一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,而且在接下来的任期里一直继续保持。
若是须要的话,算法能够经过减小被拒绝的附加日志 RPCs 的次数来优化。例如,当附加日志 RPC 的请求被拒绝的时候,跟随者能够包含冲突的条目的任期号和本身存储的那个任期的最先的索引地址。借助这些信息,领导人能够减少 nextIndex 越过全部那个任期冲突的全部日志条目;这样就变成每一个任期须要一次附加条目 RPC 而不是每一个条目一次。在实践中,咱们十分怀疑这种优化是不是必要的,由于失败是不多发生的而且也不大可能会有这么多不一致的日志。
经过这种机制,领导人在得到权力的时候就不须要任何特殊的操做来恢复一致性。他只须要进行正常的操做,而后日志就能自动的在回复附加日志 RPC 的一致性检查失败的时候自动趋于一致。领导人历来不会覆盖或者删除本身的日志(图 3 的领导人只附加特性)。
日志复制机制展现出了第 2 节中形容的一致性特性:Raft 可以接受,复制并应用新的日志条目只要大部分的机器是工做的;在一般的状况下,新的日志条目能够在一次 RPC 中被复制给集群中的大多数机器;而且单个的缓慢的跟随者不会影响总体的性能。
前面的章节里描述了 Raft 算法是如何选举和复制日志的。然而,到目前为止描述的机制并不能充分的保证每个状态机会按照相同的顺序执行相同的指令。例如,一个跟随者可能会进入不可用状态同时领导人已经提交了若干的日志条目,而后这个跟随者可能会被选举为领导人而且覆盖这些日志条目;所以,不一样的状态机可能会执行不一样的指令序列。
这一节经过在领导选举的时候增长一些限制来完善 Raft 算法。这一限制保证了任何的领导人对于给定的任期号,都拥有了以前任期的全部被提交的日志条目(图 3 中的领导人完整特性)。增长这一选举时的限制,咱们对于提交时的规则也更加清晰。最终,咱们将展现对于领导人完整特性的简要证实,而且说明领导人是如何领导复制状态机的作出正确行为的。
在任何基于领导人的一致性算法中,领导人都必须存储全部已经提交的日志条目。在某些一致性算法中,例如 Viewstamped Replication,某个节点即便是一开始并无包含全部已经提交的日志条目,它也能被选为领导者。这些算法都包含一些额外的机制来识别丢失的日志条目并把他们传送给新的领导人,要么是在选举阶段要么在以后很快进行。不幸的是,这种方法会致使至关大的额外的机制和复杂性。Raft 使用了一种更加简单的方法,它能够保证全部以前的任期号中已经提交的日志条目在选举的时候都会出如今新的领导人中,不须要传送这些日志条目给领导人。这意味着日志条目的传送是单向的,只从领导人传给跟随者,而且领导人从不会覆盖自身本地日志中已经存在的条目。
Raft 使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了全部已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每个已经提交的日志条目在这些服务器节点中确定存在于至少一个节点上。若是候选人的日志至少和大多数的服务器节点同样新(这个新的定义会在下面讨论),那么他必定持有了全部已经提交的日志条目。请求投票 RPC 实现了这样的限制: RPC 中包含了候选人的日志信息,而后投票人会拒绝掉那些日志没有本身新的投票请求。
Raft 经过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。若是两份日志最后的条目的任期号不一样,那么任期号大的日志更加新。若是两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。
如同 5.3 节介绍的那样,领导人知道一条当前任期内的日志记录是能够被提交的,只要它被存储到了大多数的服务器上。若是一个领导人在提交日志条目以前崩溃了,将来后续的领导人会继续尝试复制这条日志记录。然而,一个领导人不能判定一个以前任期里的日志条目被保存到大多数服务器上的时候就必定已经提交了。图 8 展现了一种状况,一条已经被存储到大多数节点上的老日志条目,也依然有可能会被将来的领导人覆盖掉。
图 8:如图的时间序列展现了为何领导人没法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导者,部分的复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,而后 S5 在任期 3 里经过 S三、S4 和本身的选票赢得选举,而后从客户端接收了一条不同的日志条目放在了索引 2 处。而后到 (c),S5 又崩溃了;S1 从新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,可是尚未被提交。若是 S1 在 (d) 中又崩溃了,S5 能够从新被选举成功(经过来自 S2,S3 和 S4 的选票),而后覆盖了他们在索引 2 处的日志。反之,若是在崩溃以前,S1 把本身主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(由于S5 就不可能选举成功)。 这样在同一时刻就同时保证了,以前的全部老的日志条目就会被提交。
为了消除图 8 里描述的状况,Raft 永远不会经过计算副本数目的方式去提交一个以前任期内的日志条目。只有领导人当前任期里的日志条目经过计算副本数目能够被提交;一旦当前任期的日志条目以这种方式被提交,那么因为日志匹配特性,以前的日志条目也都会被间接的提交。在某些状况下,领导人能够安全的知道一个老的日志条目是否已经被提交(例如,该条目是否存储到全部服务器上),可是 Raft 为了简化问题使用一种更加保守的方法。
当领导人复制以前任期里的日志时,Raft 会为全部日志保留原始的任期号, 这在提交规则上产生了额外的复杂性。在其余的一致性算法中,若是一个新的领导人要从新复制以前的任期里的日志时,它必须使用当前新的任期号。Raft 使用的方法更加容易辨别出日志,由于它能够随着时间和日志的变化对日志维护着同一个任期编号。另外,和其余的算法相比,Raft 中的新领导人只须要发送更少日志条目(其余算法中必须在他们被提交以前发送更多的冗余日志条目来为他们从新编号)。
在给定了完整的 Raft 算法以后,咱们如今能够更加精确的讨论领导人完整性特性(这一讨论基于 9.2 节的安全性证实)。咱们假设领导人彻底性特性是不存在的,而后咱们推出矛盾来。假设任期 T 的领导人(领导人 T)在任期内提交了一条日志条目,可是这条日志条目没有被存储到将来某个任期的领导人的日志中。设大于 T 的最小任期 U 的领导人 U 没有这条日志条目。
图 9:若是 S1 (任期 T 的领导者)提交了一条新的日志在它的任期里,而后 S5 在以后的任期 U 里被选举为领导人,而后至少会有一个机器,如 S3,既拥有来自 S1 的日志,也给 S5 投票了。
经过领导人彻底特性,咱们就能证实图 3 中的状态机安全特性,即若是服务器已经在某个给定的索引值应用了日志条目到本身的状态机里,那么其余的服务器不会应用一个不同的日志到同一个索引值上。在一个服务器应用一条日志条目到他本身的状态机中时,他的日志必须和领导人的日志,在该条目和以前的条目上相同,而且已经被提交。如今咱们来考虑在任何一个服务器应用一个指定索引位置的日志的最小任期;日志彻底特性保证拥有更高任期号的领导人会存储相同的日志条目,因此以后的任期里应用某个索引位置的日志条目也会是相同的值。所以,状态机安全特性是成立的。
最后,Raft 要求服务器按照日志中索引位置顺序应用日志条目。和状态机安全特性结合起来看,这就意味着全部的服务器会应用相同的日志序列集到本身的状态机中,而且是按照相同的顺序。
到目前为止,咱们都只关注了领导人崩溃的状况。跟随者和候选人崩溃后的处理方式比领导人要简单的多,而且他们的处理方式是相同的。若是跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的经过无限的重试;若是崩溃的机器重启了,那么这些 RPC 就会完整的成功。若是一个服务器在完成了一个 RPC,可是尚未响应的时候崩溃了,那么在他从新启动以后就会再次收到一样的请求。Raft 的 RPCs 都是幂等的,因此这样重试不会形成任何问题。例如一个跟随者若是收到附加日志请求可是他已经包含了这一日志,那么他就会直接忽略这个新的请求。
Raft 的要求之一就是安全性不能依赖时间:整个系统不能由于某些事件运行的比预期快一点或者慢一点就产生了错误的结果。可是,可用性(系统能够及时的响应客户端)不可避免的要依赖于时间。例如,若是消息交换比服务器故障间隔时间长,候选人将没有足够长的时间来赢得选举;没有一个稳定的领导人,Raft 将没法工做。
领导人选举是 Raft 中对时间要求最为关键的方面。Raft 能够选举并维持一个稳定的领导人,只要系统知足下面的时间要求:
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
在这个不等式中,广播时间指的是从一个服务器并行的发送 RPCs 给集群中的其余服务器并接收响应的平均时间;选举超时时间就是在 5.2 节中介绍的选举的超时时间限制;而后平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。广播时间必须比选举超时时间小一个量级,这样领导人才可以发送稳定的心跳消息来阻止跟随者开始进入选举状态;经过随机化选举超时时间的方法,这个不等式也使得选票瓜分的状况变得不可能。选举超时时间应该要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定的运行。当领导人崩溃后,整个系统会大约至关于选举超时的时间里不可用;咱们但愿这种状况在整个系统的运行中不多出现。
广播时间和平均故障间隔时间是由系统决定的,可是选举超时时间是咱们本身选择的。Raft 的 RPCs 须要接收方将信息持久化的保存到稳定存储中去,因此广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术。所以,选举超时时间可能须要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,很容易知足时间的需求。
本文经TopJohn受权转自TopJohn's Blog
深刻浅出区块链 - 系统学习区块链,打造最好的区块链技术博客。