Raft论文 寻找一种易于理解的共识算法(扩展版)(上)

197 阅读41分钟

0 Abstract 概要

Raft是一个用于管理复制日志的共识算法。 它提供了和Paxos算法同样的结果和性能,但是它的结构和Paxos不同;这使得Raft比Paxos更易于理解和更易于构建实际的系统。 为了加强可理解性,Raft将一致性算法分解为多个模块,包括leader选举,log复制,安全性等,它通过更强的一致性来减少必须考虑的状态的数量。 一项用研究表明对于学生而言,Raft算法比Paxos更易于学习。 Raft算法还包括一种全新的集群成员变更机制,利用重叠的主体来保证安全性。

1 Introduction 介绍

共识算法允许一组机器作为一个整体共工作(即使其中的一些出现了故障)。 正因如此,共识算法在构建可靠大规模软件系统中扮演重要的角色。 Paxos在过去十年中在共识算法领域处于统治地位:绝大部分的实现都是基于或者受到Paxos的影响,并且Paxos也成为教学中关于共识问题的主要例子。

不幸的是,尽管已经通过无数次尝试去降低它的复杂性,Paxos仍然相当难以理解。 而且,Paxos的结构需要复杂的修改才能支持实际的系统。 因此,无论是学术界还是工业界都对此感到棘手。

在和Paxos的斗争中,我们开始寻找寻找新的共识算法,可以为学术界和工业界提供更好的基础。 同Paxos不同,我们的首要目标是可理解性:我们能否在实际系统中定义一个共识算法,并且比Paxos更易于学习? 此外,我们希望这个算法能够促进直觉的发展,这对于系统构建者很重要。 重要的不仅仅是算法可以运作,而是能够理解为什么可以它可以生效。

Raft算法就是这些工作的结果。在设计Raft时,我们使用了一些特定的技巧来提高它的可以理解性,包括模块分解(Raft主要分为Leader选举,Log复制,安全三个模块),减少状态(相对于Paxos,Raft减少了非确定性和服务器处于非一致性的状态)。 一份针对两所大学的43名学生的研究表明,Raft相比Paxos更易于学习,这些学生学习两种算法后,33名能回答关于Raft的问题,效果优于Paxos。

Raft算法在许多方面和现在的共识算法相似(主要是Oki和Liskov的Viewstamped Replication),但是它也有一些独特的特性:

  • 强Leader:和其他的共识算法相比,Raft使用一种更强的领导力形式。例如,日志条目只从Leader发送到其他的服务器。这种方式简化了对于复制日志的管理,并使得Raft算法更易于理解。
  • Leader选举:Raft使用一个随机计时器来选举Leader。这只是在所有共识算法必备的心跳机制上添加了一些内容,可以更方便快捷地解决冲突。
  • 成员关系调整:Raft通过一种全新的联合共识算法来解决集群中的成员变换问题,在这种方式下,处理调整过程中的两种不同配置的集群得到大部分机器会有重叠,这就使得集群在成员变更时也能继续运作。

我们认为Raft比Paxos和其他的共识算法出色,无论是在教学中还是作为实践的基石。它比其他算法更加简单和易于理解;它的算法描述足以去实现一个真实的系统;他有好多开源的实现,并且应用于多个企业;它的安全性已经被正式声明和证实;它的效率也和其他的算法相当。

接下来,论文会介绍以下内容:复制状态机问题,讨论Paxos的优缺点,讨论我们达成可理解性的方式,阐述Raft共识算法,评估Raft算法,以及一些相关的工作。

2 Replicated state machines 复制状态机

共识算法是在复制状态机的背景下提出的。 在这种方式下,一组服务器的状态机产生相同状态的副本,并且在一些机器宕机的情况下也可以继续运行。

复制状态机在分布式系统中通常被用于解决很多容错性的问题。例如,一个大规模系统中通常有一个集群Leader,像GFS,HDFS,和RAMCloud,通常是使用一个独立的复制状态机去管理Leadler选举和存储配置信息(在Leader宕机时也需要存活)。例如Chubby和Zookeeper。

Figure 1

Figure 1: 复制状态机的结构。共识算法管理一个复制的日志,其中包含来自客户端的状态机命令。状态机处理来自日志的相同命令序列,因此产生相同的输出。

复制状态机通常是基于复制日志实现的,如图Figure 1。每个服务器存储一个包含一系列命令的日志,并按照日志的顺序执行。每个日志包含相同的指令(顺序也一致),所以每个服务器都执行相同的指令序列。因为状态机都是确定的,每次都会得出相同的状态和输出序列。

保持复制日志的一致性是共识算法的任务。服务器上的共识模块接手客户端的命令,并且将它们添加到日志中。 它和其他的服务器上的共识模块通信,以保证每份日志最终都包含相同的请求序列(即使有的服务器发生了故障)。 一旦命令被正确的复制,每个服务器的状态机会按照日志顺序执行,然后给客户端返回输出。 因此,服务器集群就像是一个高可靠的状态机。

实际系统中的共识算法通常有如下的特征:

  • 安全性(绝不会返回错误的结果):在非拜占庭错误的情况下,包括网络延迟,分区,丢包,重复,乱序等,可以保证正确。
  • 功能性:只要有绝大多数的机器可以运行,并能够互相通信、和客户端通信,就可以保证系统的可用性。因此,一般一个包含5个节点的集群可以容忍2个节点的错误。服务器被停止就认为是失败,之后可能从可靠存储的状态中恢复并重新加入集群。
  • 一致性:它们不依赖时序来保证一致性,物理时钟错误或极端的消息延迟只有最坏情况下可能导致可用性问题。
  • 通常情况下,一条命令可以尽快的在集群的大多数接种响应一轮RPC时完成。小部门较慢的节点并不会影响整体的性能。

3 What's wrong with Paxos? Paxos算法的缺点

在过去的10年中,Leslie Lamport的Paxos算法几乎成为了共识算法的代表:Paxo是课程教学中最普遍的算法,同时也是大多数共识算法的基础。 Paxos首先定义了一个能够达成单一决策一致的协议,比如单条的复制日志项。我们把这一子集称作单决策Paxos。然后Paxos通过组合多个这样的实力来促进一系列决策(例如log)。 Paxos保证安全性和有效性,同时支持集群成员的变化。它已经被证明在正常情况下都是有效的。

不幸的是,Paxos算法有两个明显的缺陷。 第一个问题就是Paxos相当难以理解。它的完整的解释及其不透明,即便付出了巨大的努力,也只有少数人成功理解了它。因此,人们多次尝试用更简单的术语来解释Paxos。 尽管这个解释都只关注了单决策子集,但仍然充满挑战。 在2012年的NSDI会议中,我们发现只有极少数人对Paxos感到舒适,即便是在一群经验丰富的研究者中。 我们也尝试去理解Paxos,知道我们读了很多简化的解释和设计了我们自己的协议后,我们才理解了Paxos,这个过程花费了近一年时间。

我们猜想Paxos算法的不透明性是由它选择单决策问题作为基础导致的。 单决策Paxos是晦涩的,微妙的:它划分成两种没有简单直观解释和无法独立理解的情形。因此,这导致了很难对为什么单决策Paxos算法有效建立起直观的感受。多决策Paxos的构成规则中增加了很多错综复杂的内容。我们相信,在多决策上能够达成共识的问题(一份日志,而非单一的记录)能够被分解为其他的更加直接和显然的方法。

第二个问题就是Paxos没有提供一个良好的用于构建现实系统的基础。 一个原因是目前还没有被广泛接受的多决策问题的Paxos算法。Lamport的描述大部分都是关于单决策Paxos的;他简要描述了实施多决策Paxos算法的可能方式,但是缺少了很多细节。当然,也有很多扩充和优化Paxos的尝试,但是它们各不相同。例如Chubby这样的系统实现了一个类Paxos算法,但是大部分的细节没有被公开。

而且,Paxos算法在构建现实系统方面并不理想;这是单决策分解所导致的。例如,独立选择日志条目的集合,然后将它们合并成一个日志序列并没有任何好处,只会徒增复杂度。围绕日志设计系统会更加简单高效,新的日志按照顺序依次附加即可。另一个问题是,Paxos使用了一种点对点的对等方式作为它的核心(尽管它最终建议采用一种弱Leader的方式来优化性能)。这在只有一个决策会被制定的简化模型中是有效的,但是在现实系统中很少使用这样的方式。如果有一系列的决策需要被制定,首先选举一个Leader,然后让它去协调决策会更加有效和快速。

因此,实际的系统总很少有和Paxos相似的场景。 每一种实现都是从Paxos开始起步,然后发现了很多实现方面的难题,接着开发一种和Paxos结构不同的算法。 这样是非常耗时和容易出错的,并且理解Paxos算法的难度加剧了这个问题。 Paxos算法在理论上被证明是可行的,但是现实的系统和Paxos的差异是如此巨大,以至于这些证明价值有限。下面来自Chubby的实现者的评价很经典:

在Paxos算法的描述和现实系统的需求中间存在着巨大的鸿沟。...最终的系统将会建立在一个没有严格证明的基础上。

因此,我们任务Paxos算法既没有提供一个良好的基础给现实系统,也没有在教学方面有很好的作用。考虑到共识问题在大规模软件集群系统中的照耀性,我们决定尝试我们是否可以设计一个更优质的,可以替代Paxos的共识算法,Raft即使这次尝试的结果。

4 Designing for understandability 为了可理解性的设计

设计Raft时我们有如下目标:

  1. 必须为系统构建提供一个完整和实用的基础,这样可以大大减少开发者的设计工作。
  2. 必须在任何情况下都是安全的,必须在大多数情况下都是可用的,并且对于大部分操作都是高效的。
  3. 必须能够让广大受众能容易地理解算法。(最重要的,最大的挑战)
  4. 必能让人形成直观的认识,这样系统构建者才能在现实中进行必要的拓展。

在设计Raft算法时,我们需要在多个方案中进行抉择。 在这种情况下,我们基于可理解性原则评估各个方案:解释每种备选方案有多难(例如,其状态空间有多复杂,是否有微妙的影响?)以及读者完全理解该方法机器含义的难易程度。

我们意识到这种分析具有高度的主观性;尽管如此,我们使用了两种普遍适用的技术来解决这个问题。 首先就是众所周知的问题分解:我们尽可能地将问题拆分成几个相对独立的,可以被解决的,可解释的,可理解的子问题。例如Raft算法被我们分成Leader选举,Log复制,安全性和成员变更几个部分。

第二个方法就是通过减少需要考虑的状态数来简化状态空间,使得系统更加连贯并且尽可能消除不确定性。 特别的,所有的日志是不允许有空洞的,并且Raft限制了日志之间变不一致的可能。尽管大多数情况下我们都尝试去消除不确定性,但是也有一些场景下不确定性可以帮助我们理解。 尤其是,随机方法引入了非确定性,但它们倾向于以类似的方式处理所有的选择,从而减少状态空间。我们使用随机化方法来简化Raft的Leader选举。

5 The Raft consensus algorithm Raft 共识算法

Raft是一种用来管理第2章中描述的Log复制算法。Figure 2简单概括了这个算法,Figure 3列出了算法的关键属性;这些图中的元素将会在本部分进行逐一讨论。

Raft通过选举一个杰出的Leader,然后赋予Leader全部的管理复制日志的职责,从而达成共识。 Leader从客户端接手日志条目(Log entries),把日志条目复制到其他服务器上,并且通知其他的服务器可以安全的将条目应用到状态机中。 拥有一个Leader大大简化了对复制日志的管理。例如,Leader可以决定新的条目存放的位置而不需要和其他的服务器协商,并且数据流都从Leader指向其他的服务器。 一个Leader可能会发生故障,或者与其他的服务器断开连接,这种情况下需要选举一个新的Leader。

通过Leader 的方式,Raft将共识问题拆分成三个相对独立的子问题,下面将会一一讨论:

  • Leader选举:当现存的Leader故障时必须选举出新的Leader。(5.2)
  • 日志复制:Leader必须从客户端接受日志条目,然后复制到集群的其他节点中,并强制其他节点和自己保持一致。(5.3)
  • 安全性:Raft中安全性的关键在于Figure 3中的状态机安全:如果有任何的节点已经在其状态集中应用某条日志,则其他服务器不得对同一个日志索引应用不同的命令。6.4中将描述Raft如何确保这一特性:该方案涉及到对5.2中选举机制的额外限制。

Figure 2

Figure 2:Raft 共识算法(不包括成员变更和日志压缩)的概要。左上角方框中的服务器行为被描述为一组独立重复触发的规则。章节编号(如§5.2)表示讨论特定功能的位置。正式规范[31]更详细地描述了算法。

State 状态

Persistent state on all servers 所有的服务器上的持续性状态

响应RPCs之前已经更新到稳定存储了

StateContent
currentTerm服务器已知的最新的任期(初始为0,自增)
votedFor当前任期内收到选票候选者Id(如果没有则为null)
log[]日志条目;每个条目包括了状态机的命令,以及Leader接收到该条目时的任期(初始为1)

Volatile state on all servers 所有的服务器上的易失性状态

StateContent
commitIndex已提交的最大日志索引(初始化为0,单调自增)
lastApplied作用于状态机的最大日志条目(初始化为0,单调自增)

Volatile state on leaders Leader节点的易失性状态

在选举后重新初始化

StateContent
nextIndex[]对于每个服务器,下一条发送的日志条目索引(初始化微Leader的最后一条日志的索引+1)
matchIndex[]对于每个服务器,已知的复制完成的最大日志条目索引(初始化为0,单调自增)

AppendEntries RPC 附加条目RPC

由Leader发起,用于复制日志条目,同时可以用于维持心跳

Arguments 参数

  • term:Leader的任期
  • leaderId:Follower根据ID将客户端的请求重定向到Leader节点
  • prevLogIndex:紧邻新日志条目的前继条目索引
  • preLogTerm:紧邻新日志条目的前继条目任期
  • entries[]:日志条目(心跳条目为空)
  • leaderCommit:Leader的commitIndex

Results 返回值

  • term:当前任期
  • success:如果Follower的条目和参数中的匹配,则响应成功

Receiver implementation 接受者的实现

  1. 如果 term < currentTerm,返回false
  2. 如果日志不匹配prevLogIndex和prevLogTerm,返回false
  3. 如果一个已有条目和新条目冲突,删除已有条目
  4. 追加日志中不存在的新条目
  5. 如果leaderCommit > commitIndex,将commitIndex改成min(leaderCommit, index of last new entry)

RequestVote RPC 请求投票RPC

由candidates发起用于收集选票

Arguments 参数

  • term:candidate的任期
  • candidateId:请求投票的服务器Id
  • lastLogIndex:candidate的最后一条日志条目索引
  • lastLogTerm:candidate的最后一条日志条目的任期

Results 返回值

  • term:当前任期,以便candidate更新任期号
  • voteGranted:若投票给该节点,则为真

Receiver implementation 接受者实现

  1. 如果term < currentTerm,返回false
  2. 如果votedFor为空,或者为candidateId,且candidate的日志和自己一样新,那么投票给它

Rules for Servers 所有服务器需要遵循的规则

所有服务器

  • 如果commitIndex > lastApplied,则lastApplied递增,并将log[lastApplied]作用于状态机
  • 如果接受的RPC请求/响应中,任期号T > currentTerm,则另currentTerm = T,并切换为Follower状态

Follower

  • 响应来自Candidates和Leader的请求
  • 如果在超过选举时间的情况下没有收到当前Leader的心跳,或者给某个Candidate投票,那么自己就成为Candidate

Candidate

  • 转变为Candidate后就立刻发起选举过程

    • 自增任期号
    • 给自己投票
    • 重置选举超时计时器
    • 发送RequestVoteRPC给其他服务器
  • 如果接受大部分选票,成为Leader

  • 如果收到新Leader的AppendEntriesRPC,则变为Follower

  • 如果选举过程超时,发起新一轮选举

Leader

  • 一旦成为Leader:先发送心跳给其他节点,并且间隔一段时间就会重新发送

  • 如果接收到客户端的请求,附加条目到本地日志,在该条目应用到状态机后,响应客户端

  • 对于一个Follower,如果lastLogIndex > nextIndex,则发送从nextIndex开始的所有与条目

    • 成功:更新Follower的nextIndex和matchIndex
    • 失败:减小nextIndex并重试
  • 假设存在N > commitIndex,使得大多数的matchIndex[i] >= N,log[N].term == currentTerm,则commitIndex = N

Figure 3

Figure 3:Raft 保证这些属性在任何时候都是正确的。章节编号表示讨论每个属性的位置。

在展示共识算法之后,这一章会讨论一些可用性相关的问题和系统中计时的角色。

5.1 Raft basics Raft 基础

一个Raft集群包括若干个服务器节点;5个是一个典型的例子,这允许系统中有两个节点失效。在任意时刻,每一个服务器都处于以下三个状态之一:Leader,Follower或者Candidate。 在正常运行的情况下,系统中只有一个Leader,其他节点都是Follower。 Follower都是被动的:他们不会发出任何请求,只能单纯的响应来自Leader和Candidate的请求。 Leader会处理所有的客户端请求(如果客户端和Follower通信,那么Follower节点会将请求转发给Leader)。 第三种状态是Candidate,用于选举新的Leader,详见5.2。 Figure 4展示了这些状态及其转换;下面将介绍这些转换。

Raft将时间分割为任意长度的任期,如图Figure 5。 任期用连续的整数标。每段任期都从一次选举开始,一个或者多个Candidate尝试成为Leader。 如果一个Candidate赢得了选举,那么他就会在接下来的任期内担任Leader的角色。 某些时候,一次选举会出现选票的瓜分。这种情况下,这段任期将会没有Leader;一段新的任期很快就会开始(一次新的选举)。 Raft保证了在给定的任期内最多只有一个Leader。

Figure 4

Figure 4:服务器状态。Followers只响应来自其他服务器的请求。如果一个Follower没有收到任何通信,它将成为Candidate并发起选举。获得整个集群多数票的Candidate将成为新的Leader。Leader通常运行到失败为止

Figure 5

Figure 5: 时间被划分为几个任期,每个任期从选举开始。选举成功后,由一个Leader管理集群,直到任期结束。有些选举失败,在这种情况下,任期结束时没有选择Leader。在不同的服务器上,可以观察到任期之间的不同时间转换。

不同的服务器节点可能在不同的时间观察到任期之间的转换。在某些情况下,一个服务器可能察觉到一次选举,甚至察觉不到整个任期。 任期在Raft算法中充当逻辑时钟的角色,任期使得服务器可以检测一些过期的信息,例如过期的Leader。 每个服务器节点存储一个当前的任期号,该编号随时间自增。当服务器之间通信时它们会交换当前的任期号;如狗哦一个服务器的当前任期号小于另一个,那么它会更新自己的任期号为另一个的值。 如果一个Candidate或者Leader发现自己的任期号过期了,它会立即变为Follower的状态。 如果一个节点接收到一个包含过期任期号的请求,它会拒绝这个请求。

Raft算法中服务器通过RPC进行通信,并且基本的共识算法只需要两种RPC。 请求投票RPC(RequestVote RPCs)由Candidates在选举期间发起,附加条目RPC(AppendEntries RPCs)由Leader发起,用于复制日志或提供一种心跳形式。第七节为了在服务器之间传输快照,新增了第三种RPC。当服务器没有即使收到RPC的响应时,会进行重试,并且它们能够并行的发起RPC来获取最佳的性能。

5.2 Leader election 领导人选举

Raft采用一种心跳机制来触发Leader选举。 当服务器启动时,它们都是Follower。只要能从Leader或者Candidate接收到有效的RPC,一个节点就会一直保持Follower的状态。 Leader周期性的向所有Follower发送心跳包(不包含日志项内容的AppendEntries RPCs)来维持自己的权威。 如果一个Follower在一段时间内没有收到任何消息,即选举超时,它就会任务系统中没有可用的Leader并且会发起新的选举以选举新的Leader。

要开始一次选举过程,跟随者要自增当前任期号并且转变为Candidate状态。 接着它会并行的向集群中的其他服务器节点发送RequestVote RPCs来给自己投票, Candidate的状态一直保持,直到以下三件事之一发生:(a)它自己赢得了这次选举,(b)其他的服务器成为了Leader,(c)一段时间之后没有一个获胜者。这些结果分别会在下面讨论。

当一个Candidate获得了整个集群的大多数服务器节点的针对同一个任期号的选票,那么它就赢得了选举。每个服务器最多会针对一个任期号投出一张选票,按照First-come-first-serve的原则(5.4中增加一点额外的限制)。 要求大多数选票的规则确保了最多只有一个Candidate会赢得此次选举(Figure 3中要求的安全性)。 一旦一个Candidate赢得了选举,它就会成为Leader。然后它会向其他的服务器节点发送心跳来建立自己的权威,并且阻止发起新的选举。

在等待投票时,一个Candidate可能会从其他的服务器收到声明它是Leader的AppendEntries RPC。 如果这个Leader的任期号(会包含在这次RPC中)不小于当前Candidate的任期号,那么Candidate就会承认Leader的合法性,并且恢复Follower的状态。 如果此次RPC中的任期号小于自己,那么Candidate就会拒绝这次RPC并且继续保持Candidate的状态。

第三种可能的结果就是Candidate既没有赢得选举,也没有输:当多个Follower同时成为Candidate,那么投票可能会被瓜分以至于没有Candidate能获得大部分节点的选票。 当这种情况发生时,每个Candidate都会超时,然后通过自增任期号来开启一轮新的选举。 然而,选票可能会被无限瓜分,如果没有其他的附加机制的话。

Raft算法通过随机选举超时的方式来确保很少发生选票瓜分的情况,即使出现了也能很快解决。为了阻止选票被瓜分,选举超时时间是从一个固定的区间随机选取的。 这样可能将服务器都分散,从而大多数情况下只有一个服务器会选举超时;它会赢得选举并且在其他的服务器超时之前发送心跳包。 同样的机制被用于选票瓜分的情况下。 每个Candidate在开始选举的时候重置一个随机的选举超时时间,然后再超时时间内等待选举的结果;这样减少在新的选举中新出现选票瓜分的可能性。9.3中表明这种方式可以快速选举出Leader。

Figure 6

Figure 6:日志由按顺序编号的条目组成。每个条目包含创建时的术语(每个方框中的数字)和状态机的命令。如果一个条目可以安全地应用于状态机,那么这个条目就被认为是已提交的。

选举就是一个例子,说明了可理解性是如何指导我们在不同的方案之间做出选择的。最初我们打算使用一种排名系统:每个Candidate都被赋予一个唯一排名,用于在Candidates中进行选择。 如果一个Candidate发现另一个Candidate有更高的排名,它将会恢复Follower的状态,这样高排名的Candidate能更容易赢得下次选举。 但是我们发现,这种方式在可用性方面产生了一些微妙的问题(如果高排名的服务器宕机了,那么低排名的服务器会超时并再次进入Candidate状态。而且如果这个行为发生的够快,可能会重置整个选举过程)。 我们针对整个算法做了多次调整,但是每次调整之后都会带来新问题。最终我们认为随机重试的方法更加清晰和易于理解。

5.2 Log replication 日志复制

一旦一个Leader被选举出来,它就开始为客户端提供服务。 客户端的每一条请求都包含一条被复制状态机执行的指令。 Leader把这条指令作为作为一条新的条目添加到日志中,然后并行地发起AppendEntries RPCs给其他的节点,让他们复制这条日志。当这条日志条目被安全地复制,Leader会应用这条日志条目到其状态集中,然后将执行的结果返回给客户端。 如果Follower崩溃或者运行缓慢,又或者网络丢包,Leader会不断的重试,直到所有的Follower都存储了所有的日志条目。 // Figure 6

日志以Figure 6展示的方式组织。每一条日志条目存储一条状态机指令以及Leader传递的任期号。 日志中的任期号用来检查是否出现不一致的情况,同时也能保证Figure 3中的部分性质。 每一条日志条目同时也有一个整数索引值来表明它在日志中的位置。

Leader决定何时将日志条目应用到状态机是安全的,这样的条目被称为committed。Raft算法保证所有committed的条目都是持久化的并且最终都会被所有可用的状态机执行。 一旦创建日志条目的Leader在大多数的服务器上复制了该日志条目(如Figure 6中的entry 7),该日志条目即为committed。 同时,Leader日志所有之前的条目,包括由之前的Leader创建的条目也会被commit。 5.4中会讨论某些当Leader变更之后应用这条规则的一些细节,同时它也表明这样的提交的定义是安全的。 Leader跟踪了最大的将会被提交的日志项的索引,并且索引值将会被包含在未来的所有AppendEntriesRPCs中,这样其他的服务器才能最终知道Leader的已提交位置。一旦Follower知道一条日志已被commit,那么它也会将这条日志应用到本地的状态机中(按照日志的顺序)。

我们设计了Raft的日志机制来维护不同的服务器日志之间的高度一致性。这样不仅简化了系统的行为也使其更可预测,同时这也是确保安全性的一个重要组成。 Raft维护着以下的特性,这些特征共同组成了Figure 3中的日志匹配特性

  • 如果在不同的日志中两个条目拥有相同的索引和任期号,那么它们存储了相同的指令。
  • 如果不同日志中的两个条目拥有相同的索引和任期号,那么它们之前的所有条目也相同。

第一个特性来自这样一个事实,即在一个给定任期内,Leader最多创建一个具有给定日志索引的条目,并且该条目在日志中的位置永远不会改变。 第二个特征是由AppendEntriesRPC的一个简单地一致性检查所保证的。在发送AppendEntriesRPCs时,Leader会把新的条目前继条目的索引位置和任期号包含在日志内。如果Follower在它的日志中找不到包含着相同索引位置和任期号的条目,那么它就会拒绝接受新的条目。 一致性检查作为一个归纳步骤:一个初始为空的日志状态是满足日志匹配特征的,然后一致性检查在日志拓展时保证了日志匹配特征。因此每当AppendEntriesRPC返回成功,Leader直到Follower的日志一定是和自己保持相同。

在正常操作中,Leader的日志和Follower的日志保持着一致,所以AppendEntriesRPC的一致性检查不会失败。 但是,Leader崩溃时可能导致日志的不一致性(旧Leader没有完全复制所有日志条目)。这种不一致性可能会在Leader和Follower的一系列崩溃下加剧。Figure 7展示了Follower的日志和新的Leader不同的情形。Follower可能会丢失一些新Leader中存在的的日志,它也有可能存在有一些Leader中不存在的日志条目,或者两者都有。丢失或额外的日志条目可能持续多个任期。

Figure 7

在Raft算法中,Leader是通过强制Follower复制自己的日志来解决不一致的问题的。这意味着Follower日志中冲突的条目会被Leader的日志覆盖。5.4会阐述这样操作的安全性(通过额外的限制)。

要使得Follower的日志和自己保持一致,Leader必须找到它们达成一致的最后一条日志条目,然后删除Follower中之后的条目,并将自己的条目拷贝给它。 所有的这些操作都在AppendEntriesRPCs的一致性检查中完成。Leader会为每一个Follower维护一个nextIndex,它是Leader将发送给该Follower的下一条日志条目的索引。当一个Leader刚获得权力的时候,它会初始化所有的nextIndex的值为自己的最后一条日志的index+1(Figure 7中的11)。如果一个Follower的日志和Leader的不一致,那么在下一次的AppendEntriesRPC时一致性检查就会失败。 在被Follower拒绝之后,Leader会减小nextIndex的值并进行重试。最终这个nextIndex值会在某个位置和Follower的日志达成一致。当这种情况发生,AppendEntriesRPC就会成功,这是就会把Follower中冲突的条目移除,并拷贝Leader的日志。 一旦AppendEntriesRPC成功,那么Follower的日志就会和Leader保持一致,并且在任期中一直如此。

如果需要的话,算法可以通过减少被拒绝的AppendEntriesRPC次数来进行优化。例如,当AppendEntriesRPC的请求被拒绝的时候,Follower可以返回冲突条目的任期号和该任期号对应的最小index。这样子,Leader可以减小nextIndex一次性越过该冲突任期的所有条目;这样就变成每个任期只需要一次AppendEntriesRPC。在实验中,我们对这种优化持怀疑态度,因为失败几乎不会发生,并且也几乎不能存在这么多不一致的日志。

通过这种机制,Leader在获得权力时不同要通过任何特殊的措施来恢复日志的一致性。它只需要进行正常的操作,然后日志就能通过AppendEntriesRPC的一致性检查自动趋于一直。Leader从来不会覆写或者删除自己的条目(Figure 3的Leader的Append-Only特性)。

日志复制机制体现了第2节中的共识特征:Raft能够接受,复制,并应用新的条目只要大部分的服务器时正常的;在正常的情况下,新的条目可以再一轮RPCs中被复制给集群的大多数机器;并且单个运行缓慢的Follower不会影响整体的性能。

5.4 Safety 安全性

前面的章节描述了Raft算法是如何选举Leader和复制日志条目的。 然而,到目前为止描述的机制并不能充分保证每一个状态机都按照相同的顺序执行相同的指令。例如:一个Follower可能会在Leader提交若干条目时进入不可用状态,然后这个Follower被选举为Leader并覆盖了这些条目;因此,不同的状态机可能会执行不同的指令序列。

这一节通过在Leader选举的时候增加一些限制来完善Raft算法。 这样的限制保证了任何的Leader对于给定的任期号,都拥有了之前的任期中所有committed 的条目(Figure 3中的Leader完整特性)。增加这一选举时,我们对于提交的规则也更加清晰。 最终,我们将会呈现对于Leader完整性特征的简要证明,以及说明该特性是如何保证复制状态机做出正确的行为的。

5.4.1 Election restriction 选举限制

在任何基于Leader的共识算法中,Leader都必须存储所有的committed的日志。 在某些共识算法中,例如Viewstamped Replication,即使一个节点一开始没有包含所有的committed日志,它也能被选举为Leader。 这些算法都包含了一些额外的机制来识别丢失的条目并将它们传送给新的Leader,要么是在选举阶段,要么是在之后很快地进行。 不幸的是,这导致了相当庞大的额外机制和复杂性。 Raft使用一种更简单地方式,它可以保证选举得到时候新的Leader拥有所有之前的任期中committed 的条目,而不需要传送这些条目给Leader。 这意味着日志条目的传送是单向的,只从Leader发送给Follower,并且Leader不会覆盖本身已有的日志。

Raft通过投票过程来阻止Candidate赢得选举,除非其日志已包含所有committed的条目。一个Candidate为了赢得选举必须和集群中的大部分节点通信,这意味着每一个committed日志在这些服务器节点中至少存在于一个节点上。 如果Candidate的日志和大多数的节点一样新,那么它一定持有了所有committed的条目。 RequestRPC实现了这种限制:RPC中包含了Candidate的日志信息,然后投票者会拒绝那些日志旧于自己的请求。

Raft通过比较两份日志中的最后一条日志的索引值和任期号来判断新旧。 如果两份日志的最后条目的任期号不同,那么任期号更大的日志更新。 如果两份日志最后条目的任期号相同,则日志较长的更新。

5.4.2 Committing entries from previous terms 提交之前任期的条目

如果5.3中描述的,Leader知道,一旦当期任期人的条目存储在大多数的节点时,该条目已被提交。 如果Leader在提交条目之前崩溃了,之后的Leader会完成日志的复制。 然而,一个Leader不能断定之前任期内的条目在存储到大多数节点时就一定committed了。Figure 8展示了一种情况,一条已经被存储到大多数节点的旧条目,依然可能被新Leader覆写

Figure 8

Figure 8:图中的时间序列展示了为什么Leader无法决定对旧任期号的日志条目进行提交。 (a)中,S1是Leader,部分Follower复制了index=2的条目。 (b)中,S1崩溃了,然后S5在任期3中通过S3,S4和自己的选票,赢得了选股,然后从客户端接受了新的条目放在index=2处。 (c)中,S5又崩溃了;S1重新启动,选举成功,开始复制日志。这时候,来自任期2的日志已被复制到大多数机器上,但是尚未提交。 (d)中,S1又崩溃了,S5可以重新被选举成功(S2,S3,S4的选票),然后覆盖它们索引2处的日志。 反之,如果在崩溃之前,S1将自己任期内产生的日志条目复制到大多数机器上,那么就会像(e)中那样,在后面的新的任期中这些条目就会被提交(因为S5此时不能选举成功)。 这样在同一时刻就保证了,之前的旧条目都会被提交。

出现这种问题的根本原因就是,(c)中处于任期4的S1提交了任期2的日志 正确的做法应该是等到任期4时大部分节点存储后,同时提交两条记录。 这样,就不会(c)时刻的情形,即任期4的S1不会复制任期2的日志到S3,而是(e)中那样,通过复制-提交任期4的日志而顺带提交任期2的日志,此时即使S1宕机,S5也不能当选成功。(即使任期4没有收到客户端请求,在任期开始时也可以尝试立即提交一条空的日志)

为了消除图 8 里描述的情况,Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。 只有Leader当前任期内的日志条目可以通过计算副本数目被提交;一旦当前任期的条目通过这种方式被提交,那么由于日志匹配特性,之前的日志条目也会被间接提交。 在某些情况下,Leader可以安全的知道一个旧日志条目是否被提交(是否被存储到所有的服务器上),但是Raft使用了一种更保守的方式。

当Leader复制之前任期的日志时,Raft会为所有的日志保留原始的任期号,这在提交规则上产生了额外的复杂性。 在其他的一致性算法中,如果一个新Ledaer需要从之前的任期拷贝日志时,他必须使用当前新的任期号。 Raft使用的方式更容易辨别出日志,因为它可以随着时间和日志的变化维护同一个任期号。 另外,新Leader需要发送的条目更少(和其他算法相比,其他算法中必须在它们commit之前发送更多的冗余日志来为其重新编号)。

5.4.3 Safety argument 安全性论证

在给定完整的Raft算法之后,我们现在可以更加精确的讨论Leader完整性特征(基于9.2节的安全性证明)。我们认为Leader完整性特征不存在,然后我们证明这是矛盾的。 假设任期T的Leader在任期内commit了一条日志,但是这条日志没有存储到未来某个任期的Leader的日志中。假设大于T的最小任期U没有这条日志。

Figure 9

Figure 9:如果S1(任期T的Leader)在它的任期内提交了一条新的日志,然后S5在之后的任期U被选举为Leader,那么至少有一个,例如S3,既拥有来自S1的日志,也拥有来自S5的日志。

  1. 在Leader U选举时一定没有那条被提交的日志(Leader不会删除或覆盖日志条目)。
  2. Leader T复制这条日志给了集群的大多数节点,同时U从集群的大多数节点获取的选票。因此至少有一个节点同时接受了来自T的条目,并给U投票了(Figure 9)。这个投票者是矛盾的关键。
  3. 这个投票者必须在给U投票前接受来自T的条目;否则它就会拒绝来自T的AppendEntriesRPC(因为此时它的任期大于T)。
  4. 投票者给U投票时仍持有这条日志,因为任何中间的Leader都包含该日志条目,Leader从不会删除/覆写条目,并且Follower只有在和Leader冲突时才会删除条目。
  5. 投票者给U投票时,U的日志必须和自己一样新。这导致了矛盾。
  6. 首先,如果投票者和U的最后一条日志的任期号相同,那么U的日志至少和投票者一样长,所以U的日志一定包含了所有投票者的日志。
  7. 除此之外,Leader U的最后一条日志的任期号必须比投票者。同时,它也比T大,因此投票者的最后一条日志的任期号至少和T一样大。创建了U的最后一条日志之前Leader一定已经包含了那条被提交的日志。所以根据日志匹配特性,U一定也包含了那条日志,因此矛盾。
  8. 这里已经产生了矛盾。因此,所有比T打的Leader一定包含了所有来自T的已被提交的日志。
  9. 日志匹配原则保证了未来的Leader也会包含被间接提交的题目,如Figure 8(d)中的索引2。

通过领导人完全特性,我们就能证明Figure 3中的状态机安全性,即如果服务器已经在某个给定的索引值应用了日志条目到自己的状态机里,那么其他服务器不会应用不同的日志条目在同一个索引值处。 在一个服务器应用一条日志到自己的状态机中时,它的日志必须和Leader的日志,在该条目和之前的条目相同,并且已经commit。 现在考虑任何一个服务器应用一个指定索引位置的最小任期;日志完全特性保证有更高任期号的领导人会存储相同的日志条目,所以之后的任期中某个索引位置的日志条目也是相同的值。因此状态机安全特性成立。

最后,Raft要求服务器按照日志中索引顺序来应用条目。 和状态机的安全特性结合来看,这意味着所有的服务器会应用相同的日志序列。

5.5 Follower and candidate crashes 跟随者和候选人崩溃

到目前为止,我们只关注了Leader崩溃的情况。Follower和Candidate的崩溃相比Leader来说,处理起来要简单的多,并且它们的处理方式是相同的。 如果Follower或者Candidate崩溃了,那么之后发送给他的RequestVote和AppendEntriesRPCs将会失败。 Raft中通过重试来处理这种失败;如果崩溃的机器重启的,那么这些RPC就会成功完成。 如果一个服务器在完成一个RPC但还未响应时崩溃了,那么它在重启之后会收到同样的RPC请求。 Raft的RPCs都是幂等的,所以这样的重试没有问题。 例如一个Follower收到一个AppendEntriesRPC,但是他已经包含了这个日志,那他就是直接忽略这个新的请求。

5.6 Timing and availability 时间和可用性

我们对Raft的要求之一就是安全性不能依赖于时间:系统不能因为某些事情发生的比预期快/慢而生成错误的结果。 然而,可用性(系统可以及时响应客户端)不可避免的要依赖于时间。 例如,如果消息交换比服务器故障间隔时间长,那么Candidate将没有足够的时间来赢得选举;没有一个稳定的Leader,Raft将不能运作。

Leader选举时Raft中对时间要求最关键的方面。Raft可以选举并维持一个稳定的Leadaer,只要系统满足一下要求:

broadcastTime<<electionTimeout<<MTBFbroadcastTime << electionTimeout << MTBF

在这个不等式中,广播时间是指的是从一个服务器并行的发送RPCs给集群中的其他服务器并接受响应的平均时间;选举超时时间即5.2节介绍的选举的超时时间限制;平故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。 广播时间必须比选举超时时间小一个量级。这样Leader才能够发送稳定的心跳消息来组织Candidate进入选举状态;通过随机化选举超时时间的方法,这个不等式也使得选票瓜分的情况变得不可能。 选举超时时间应该比平均故障间隔小几个数量级,这样整个系统才能稳定运行。 当Leader崩溃之后,整个系统会大约相当于选举超时的时间内不可用;我们希望这种情况尽可能少出现。

广播时间和平均故障时间是由系统决定的,但是选举超时时间是我们自己决定的。 Raft的RPCs需要接受方将信息持久化的保存到稳定存储总,所以广播时间大约是0.5ms-20ms,区别于存储的技术。 因此,选举超时时间可能需要10ms-500ms。 大多数的服务器的平均故障时间间隔都在几个月甚至更长,很容易满足时间的需求。