本篇文章以 John Ousterhout(斯坦福大学教授) 和 Diego Ongaro(斯坦福大学得到博士学位,Raft算法发明人) 在 Youtube 上的讲解视频及 ppt 为蓝本,深刻分析 Raft 的内部机制,并以日志复制同步(Replicated Logs)为背景,详细介绍使用 Raft 协议实现日志复制的共识性问题。php
Raft 的目标是将日志完整地复制到集群内的全部服务器,这些复制的日志会被状态机所使用。假设咱们但愿程序或应用能可靠地执行,可以实现的一种方式是保证集群中全部服务器内的状态机都能按照相同的方式执行命令,这就是状态机复制同步的目的,这里的状态机一般指的是一个输入输出程序或应用。日志能够保证状态机执行相同的命令。下面介绍它的运做机制。web
若是系统的客户端将要执行的命令传递给集群中的一台服务器,假设命令是 X ,那么它会被该台服务器记录,而后命令会被发送到其余服务器,并被其余服务器上的日志所记录。一旦命令被安全的复制到日志中,那么它们就能被发送到状态机供执行。当其中的一台状态机完成了命令的执行,结果会被返回给客户端。能够注意到只要各个服务器上的日志是相同的,各个服务器上的状态机就能以相同的顺序执行相同的命令,这样它们执行的结果也都是同样的。因此共识性模块的任务就是管理这些日志,并保证它们正确的在集群内复制而且决定什么时候将命令传送给状态机才是安全的。算法
咱们将这一过程称为共识性方法的缘由是咱们不须要全部的服务器在任什么时候候都处于运行状态,实际上,系统只要在大多数服务器存活的状态下能继续正常运行和相互通讯就能够。因此例如可能有 3 台服务器,那么咱们就能够接受其中 1 台服务器宕机,只要有两台服务器是存活的便可;当服务器有 5 台时,咱们就能够接受其中的 2 台服务器宕机,只要其中三台是正常运行的。安全
如今咱们来简短地介绍但愿系统可以处理的失败的状况。咱们容许服务器崩溃,不过咱们但愿它们是 “失败-中止(fail-stop)” 的方式。也就是说,它们只是中止工做,或者在中止后又恢复,不过要求只要它们是处于运行状态的,它们的行为就必须正确。这个协议要求服务器不能有 拜占庭行为 作一些错误的操做。咱们还容许网络的通讯能够被打断,消息能够出现延迟或丢失的状态,甚至出现消息到达处于无序的状态。网络也有可能出现隔离的状况,而后又恢复正常。服务器
想要实现共识性算法主要有两种方式:第一种方式称为对称式或无主式,在这种方式下,全部的服务器都有相同的角色,它们有同等的权力,它们任什么时候候的行为几乎都是同样的,客户端能够与任何一台服务器进行通讯。第二种方式称为非对称式或基于领导者(leader),服务器在任什么时候候都不是对等的,只有其中的一台服务器是领导者(leader),领导者负责集群的全部操做,其余的服务器只是简单地服从领导者发出的指令,在这种系统下,客户端永远与领导者通讯,只有领导者才与其余的服务器发送通讯。网络
Raft 就是使用上面第二种方式。它将共识性算法的问题分解成两类不一样的问题,一种是在领导者正常运行下,进行的普通操做;另外一种是在领导者崩溃时,须要对领导者进行从新选举,这种方式有其优点,它让普通的操做变得很是简单,不须要关心是否有多个领导者相互发生冲突,或同时发出指令,只要有一个领导者控制全局,就能够彻底按照它的指令来运行。Raft 算法的复杂之处在于领导者发生变化时,由于当领导者崩溃时,会使系统处于不一致的状态,后续被选举的领导者须要对此这些不一致状态进行清理。整体上说,基于领导者的方式要比无领导者的方式简单,由于无须担忧不一样服务器间会出现冲突,只须关心领导者发生变化的状况。并发
Raft 算法共分红 6 个部分,首先咱们要介绍的就是领导者的选举。app
如何从全部的服务器中选择领导者?如何在看成为领导者的服务器崩溃时能检测到故障并挑选另外一个领导者来替代它?分布式
会介绍当领导者接收到客户端请求时,系统是如何处理正常操做的。这是 Raft 算法中最简单的部分。性能
会讨论领导者发生改变的状况,这部分是 Raft 中最复杂的,也是保证整个系统行为最重要的部分。首先,会讨论什么叫作安全,如何保证安全?其次,领导者是如何识别日志的一致性的,从而能够将系统恢复处处于一致状态下。
会讨论领导者发生改变时的另外一个问题。如何让曾经崩溃死机的老领导者,从新回归到集群后集群的状态仍然能保持一致。
会谈论客户端是如何与集群交互的。关键点在于客户端是如何处理服务器崩溃,如何保证客户端发送的命令是线性的,即操做执行也仅执行一次。
最后会讨论如何处理配置变动的状况,即如何对集群增长或移除服务器。
在对这六步进行详细地介绍前,先来介绍一些整体信息。
任什么时候候,服务器都处于如下三种状态中的一种:
在上图最下面展示了一个状态图,它展现了三种状态,以及三种状态在不一样条件下发生转变的状况。如今不会对此进行详细解释,可是在随后对算法做详细介绍时,就能发现它们之间的联系。
时序被分割为领导者任期,每段领导者任期都有一个序号,这些序号随着任期数的增长会自动增加,不会被重复使用。每段任期都分为两个部分,首先,任期是由选举开始的,这个过程会挑选任期内的领导者,若是选举成功,被选择的领导者会服务至本任期结束。在同一任期内,只有一台服务器能够被选择为领导者。不过也会存在某些任期没有任何领导者,若是出现分票就会出现这种状况,不存在得到大多数投票的领导者,当发生这种情况时,系统会即刻进入到下一个新的任期并尝试从新选举。在 Raft 系统的全部服务器都保持着一个被称为当前任期的值,这个信息必须存于服务器的可靠媒介中(如硬盘)。这样就能在服务器崩溃以后得以重启并恢复。任期这个概念十分重要,它使 Raft 能够判断过时的信息。例如,若是一台服务器认为当前的任期号是 2 与另外一台认为当前任期号为 3 的服务器进行通讯,那么咱们就能知道来自于服务器 2 的信息是过时的,咱们只会使用来自于最新任期的信息。因此咱们将会看到在某些状况下,会使用到任期来检查并消除过时的信息。
上图是 Raft 协议的完整归纳,目前还不会对它们进行详细的介绍,可是会简单介绍一些它的特性。
首先分别描述 Raft 协议里的三种角色:跟随者(Followers)、候选者(Candidates)和领导者(Leaders)。
其次描述须要在服务器磁盘上进行持久化存储的信息。
第三描述服务器是如何进行通讯的,Raft 的全部通讯都是基于远程过程调用的(RPCs),这里只有两种类型的调用:一种被称为远程过程调用投票(RequestVote RPC),它在选举的过程当中被用来挑选领导者;另一种远程过程调用是领导者用来执行正常操做,复制日志记录的。这是 Raft 系统使用的惟一两种远程过程调用的方式。这两种调用均可以很好的处理日志复制同步以及消息丢失等问题。
如今让我来一一讲解 Raft 协议的六个组件。Raft 协议的第一个组件是选举。Raft 必须保证在任什么时候候只能有一台服务器做为集群的领导者。服务是以跟随者角色启动的,处于这种状态时,它不会与其余的服务器进行通讯,跟随者彻底是被动的,它只是简单地对来自于其余服务器的远程调用作出响应。不过,为了让跟随者一直处于跟随者的状态,必须使它们相信集群有一个活跃的领导者存在。惟一能实现的方式就是,若是它接收到来自于其余服务器的通讯,不管是领导者或是候选者,因此若是领导者想要保持它的领导地位,它就必须按期与集群的其余服务器进行通讯,若是它没有与其余服务器进行主动通讯的须要,那么它也必须发送心跳检测的消息,在 Raft 协议中,这些心跳检查消息也只是一些不含任何数据信息的 AppendEntries 远程调用。若是在一段时间内,跟随者没有接收到任何的远程调用,那么它会假定集群内没有可达或可用的领导者,因此它就会开始进行选举,看它是否有必要成为新的领导者。这段时间周期被称为选举超时(electionTimeout),一般集群将这个时间定为 100ms 到 500ms 。因此当集群启动时,全部的服务器都是做为跟随者的,没有领导者,因此它们都会等待这段超时,而后它们都会开始进行选举。
如今让咱们看看,选举是如何工做的。
当服务器开始进行选举的时候,它所作的第一件事情就是增长当前的任期号,建立一个比以前使用过的任何值都要大的新任期号。随后,服务器将它们本身从跟随者状态转换到候选者状态,在这种状态下,它的目标就是要让本身当选为领导者,为了这么作,它须要接收来自于大多数服务器的投票。候选者要作的第一件事情就是给本身投票,而后它会给其余全部服务器发送投票请求的远程调用(RequestVote),一般这些请求是并行发出的。若是它没有得到响应,它就会持续发送重试的请求,直到得到响应为止。
最终会出现三种状况中的其中一种:
第一,在大多数状况下,也是咱们但愿出现的状况就是候选者获得了多数票,而后它会将本身的状态转换为领导者并当即向集群其余服务器发送心跳检测,这能够创建它的领导者地位,有效的标记领导者所管理的范围。
第二,可能出现有其余的候选者也同时在运行,或许它们也有可能得到多数票成为领导者,在这个点上,若是候选者收到来自于有效领导者的 RPC 调用,那么它会当即放弃成为领导者的可能,随即回到跟随者的状态。
第三,有可能没有任何服务器得以获胜,若是存在有多个服务器都同时成为候选者,它们会致使分票,没有服务器会得到多数选票。为了检测到出现这种情况的可能性,随着时间的推移,当没有出现以上第1、第二种状况时,它既没有成为领导者,也没能得到来自于其余领导者的响应,那么它就会假定出现分票的状况。在这种状况下,只要简单地增长任期号,从新选举便可。
选举有两个重要的属性:安全(Safety)和可用(Liveness)
安全(Safety) 指的是必须最多只有一个候选者能够在某一任期内赢得领导者地位。Raft 能够保证这件事。每台服务器只给一个候选者投票,一旦它投出选票,它就会拒绝来自其余候选者的任何请求。服务器并不关心它的票到底投给了哪台服务器。为了实现这种机制,服务器须要保证将本身的投票信息存储到磁盘,这样就能在服务器崩溃以后也能恢复到以前的状态。不然就会出现服务器已经做出投票,并在崩溃重启后,在同一任期内将票又投给了另一个不一样服务器的状况。由于每台服务器只能进行一次投票,并且每一个候选者都必须得到多数票,也就能够发现,不可能出现两个候选者同时获胜的状况。
比方说有三台服务器在某一任期内进行选举,另外两台服务器显然没法得到多数票。不事后面会介绍不一样任期间会出现不一样候选者获胜的状况,但在某一肯定的任期内,只有一个候选者能够被选举为领导者。
可用(Liveness) 须要保证必定有获胜者,这样系统不会永远处于没有领导者的状态。问题在于理论上,会反复出现分票的状况,多个候选者在同一任期内同时开始进行选举,这样就会致使分票,在超时以后,又进行新一轮的选举又再次出现分票,因此从理论上说这样的状态能够无限循环下去。Raft 须要分散出现超时的间隔,每台服务器都会随机的计算下次超时的间隔时间,这个时间间隔在 [T, 2T] 之间。T 表明着选举超时的时间,即服务器可能出现超时的最短期。经过将超时时间分散,能够下降两台服务器同时开始选举的机率,先启动的那台有足够的时间向其余全部服务器发起请求,并在其余服务器参与竞争以前就完成选举这个过程。当这个超时间隔时间远大于广播投票请求的时间时,这个策略会变得更为有效。这里的广播时间指的是,一台服务器与其余全部服务器通讯所需的时间。
如今进入 Raft 协议的第二部分,即领导者用普通操做来处理日志复制同步时使用的机制。
首先,让咱们说说日志自己。每台服务器不管是领导者仍是跟随者,都各自保存一个日志副本。日志自己被分红了多条记录(Entries),记录是由下标索引的位置来进行惟一标识的,在记录内部有两个主要信息:首先,每条记录都包括供状态机执行的一条命令,命令的格式能够是客户端与状态所达成一致的某种格式。其次,每条记录都包括一个任期号,这个任期号是该条记录建立时,领导者所处的任期,随着日志记录的增多,这个任期号也会单调上升。每台服务器都必须保证日志能在崩溃后还能够恢复,因此日志自己一般是存于磁盘或其余一些稳定的存储介质中。不管服务器做何更新,它都须要在收到来自于其余服务器的响应以前,将内容写入到磁盘。若是某条记录已存储于大多数服务器,例如上图中的记录 7 (Entry-7),那么咱们就称该条记录已提交(committed)。这是 Raft 协议里很是重要的一个属性。若是一条记录是已提交的,那么它就能安全被传送给状态机进行执行,Raft 能够保证该条记录的耐久性。在上图中记录 7 是已提交的,全部先于记录 7 的记录也是已提交的状态,可是记录 8 还处于未提交状态,由于它只存储于两台服务器上。
如今须要注意的是,在稍后讨论如何管理跨服务器日志间的一致性的时候,我会对提交(commitment)这个概念的定义做些许修改。
普通操做比较简单,客户端将命令发送给领导者,领导者首先将命令写入它本身的日志中,而后向全部其余的跟随者发送 AppendEntries 的远程调用。一般这些调用的消息会被同时发送全部服务器,以并行的方式执行,并等待这些消息的响应。一旦领导者收到足够多的响应,能够它认为该条命令已经在多数服务器上处于已提交状态时,那么该条命令就能够被执行。领导者这时会将命令发送给状态机,当执行结束后,它会将结果返回给客户端。不只如此,一旦服务器知道某个记录已经处于提交状态,它就会经过后续的 AppendEntries 远程调用告知其余的服务器。因此最终,每一个跟随者都会知道该记录已提交,而且将该命令发送至本身本地的状态机执行。若是跟随者崩溃了或处于慢响应状态,领导者会反复重试这个调用,直到跟随者恢复后,领导者就能重试成功。可是领导者并不须要等待每一个跟随者的响应,它只须要等到足够数量的响应,保证记录已被大多数服务器存储便可。因此这样就能在通常状况下得到很好的性能提高。也就是说,在一般状况下,只须要得到大多数最快的服务器的应答,领导者就能够当即执行命令,并将结果返回至客户端。例如,若是某个服务器很慢,这并不能影响客户端得到响应的速度,由于领导者并不须要一直等待该台服务器。
Raft 指望能将集群日志维持高水准的一致性。理想状态下,这些日志在任什么时候候都是相同的,甚至是服务器崩溃时也如此。Raft 会尽量的保证在不一样服务器上的日志是同样的。上图的内容会列出一些重要的属性,它们在任什么时候候都是有效的。
第一,日志记录的索引以及任期号的组合能够惟一标识一条日志记录。也就是说若是有两条记录的索引是同样的,任期号也是同样的,那么就能够保证它们所存储的命令也是相同的。除此以外,还能保证在这条记录以前的全部记录都能相互匹配。因此任期号和索引的组合能够惟一标识整个日志的起始至该点的位置。若是某条记录是已提交的,那么其全部前序的记录都应该处于已提交状态。这也与以前介绍的规则一致,若是发现服务器存储记录(如上图的记录 5),由于有了以上规则,它们存储的前序记录也必须相同。因此这些前序记录也存在于集群的大多数服务器上。
这个属性强制在 AppendEntries 远程调用时进行检查,当领导者向跟随者发起 AppendEntries 调用时,除了新建立的新日志记录,它还包括两个值。他包括当前新记录前序记录的下标位置索引以及任期号,跟随者只会接受与它日志匹配的远程调用,若是跟随者的日志没有相应的记录,那么它会拒绝这个远程调用。
让咱们来看一个例子,假设领导者从客户端接收到一个新命令 jmp ,它将这个命令以 AppendEntries 远程调用的方式发送给跟随者,包括它前序记录的下标位置索引以及任期号,这里下标位置索引是 Index-4 ,任期号是 Term-2 。这样跟随者会将此信息与它本身当前日志的记录匹配,而后接受建立新的记录。如上图下半部分,跟随者的当前最新记录与领导者的前序记录的信息不匹配,这样跟随者会拒绝接受远程调用的请求。
这个一致性检查的过程很是重要。能够将这个过程看做一个概括的步骤,从而保证前面一致性里所讲的内容。它要求前序每条记录都能知足此条件,因此这意味着若是一个跟随者接受了来自领导者的新记录,它的日志记录也与领导者的日志记录是彻底匹配的。
以上就对普通操做的介绍告一段落。接下来介绍领导者变动的状况。
当领导者发生变动时,新领导者面对的状态不必定是干净的,由于前一领导者可能在它完成复制同步以前就已经崩溃了,当 Raft 处理这个问题时,它在新的领导者被选出以前,不会有任何特别的操做,不会存在一个独立清理过程,清理过程是在普通操做过程当中发生的。缘由是当新领导者被选出后,某些服务器可能还处于宕机的状态,不可能马上对它们的日志进行清理,必须能有操做恢复它们,并且在这些机器从新加入集群以前可能会要等待很长一段时间,因此就必须对系统进行设计,要求普通操做最终能让全部的日志达成一致状态。为了达成这个目标,Raft 始终会认为领导者的日志老是正确的,因此对于全部领导者,它们必须时刻的让跟随者的日志与本身保持一致,但同时仍是有可能出如今领导者未完成任务就崩溃的状况,因此就会出现一个又一个的新领导者。因此,在极端扭曲的状态下,日志记录会无限堆积并出现混乱的状态,就如上图所示的那样。
为了简单起见,上图中只显示了下标索引位置以及任期号,没有显示具体的命令信息。
当服务器 S四、S5 在任期 二、三、4 时是领导这,可是因为某些缘由,它们没法完成对其余服务器(S一、S二、S3)上日志的复制同步,而后它们崩溃了,系统在一段时间内处于分隔状态,服务器 S一、S二、S3 在任期 五、六、7 内成为领导者,但同时也没法与服务器 S四、S5 进行通讯,要求它们进行相应的清理操做。这就会出现上图中所示的状态,日志彻底是混乱的。这里的关键在于 S一、S二、S3 的索引 1-3 以及 S四、S5 的索引 1-2 区域。这些都是已提交状态的记录,因此咱们必须保留它们,但其余的日志记录都是未提交的,因此究竟是保留仍是丢弃它们并不重要。咱们尚未将它们传入状态机,也没有客户端获得了这些命令的执行结果。因此它们都是能够丢弃的。
例如,假设服务器 S4 是任期 7 的领导者,并且它能够与其余全部服务器通讯,那么它最终会让集群里其余服务器上的日志与它本身的保持一致,并删除那些与之冲突的记录。在介绍领导者是如何让其余服务器上日志与之保持一致前,首先须要介绍两个概念:正确性(Correctness)和安全性(Safety)。咱们是如何知道系统的行为是正确的?如何知道它们没有丢失一些重要信息?由于这里能够看到,为了让集群回到一致的状态,有些日志记录会被丢弃。咱们是如何安全地作到这点的?
几乎全部的日志复制同步系统都会对安全性有所要求,一旦某个状态机接收了一条日志记录并执行,咱们必须保证不存在其余的状态机执行不一样的命令。须要保证全部的状态机,以相同的顺序执行相同日志记录的命令。为了达成整体的安全性要求,Raft 实现了一个安全属性,一旦领导者决定某个特定记录已提交,那么 Raft 就须要保证该条记录会出如今它全部将来领导者的日志记录中,而且也处于已提交状态。若是咱们可让 Raft 听从这个属性,那么它就天然能够保证以上的安全性要求。首先,领导者永远不会覆盖日志记录,它只会追加,正如咱们所知,做为领导者时,这些日志记录永远不会被改变,其次,为了到达已提交的状态,记录必须在领导者日志中,这样就不会有其余值会被提交,第三,若是咱们知道日志记录必须在发送给状态去执行以前被提交,因此将以上三点放在一块儿,咱们就能使该属性能够知足安全性的要求。
目前为止,咱们对 Raft 的描述还不能保证这个属性。下面我会来看看 Raft 是如何解决这个问题的。不过再次以前咱们须要再看看,若是某条记录是已提交的,那么它在将来的领导者日志记录中也必须是已提交的。为了知足这个要求,咱们会从两个方面对 Raft 算法做出修改。首先,咱们会修改选举过程,将日志记录不正确的那些机器排除在选举以外,其次,会对已提交的定义作略微的调整。有时在知道安全以前,咱们会延迟一条记录的提交。
下面会先介绍选举相关的问题
如何保证选择的领导者有全部已提交的日志记录?首先,这有点微妙,事实上咱们没法辨别哪些记录是已提交的,假设有如上图的三台服务器,咱们须要选择一个新的领导者,但其中的一台服务器不可用,那么只要在这个过程当中,查看可用的服务器,咱们此时是没法分辨记录 5 是否已提交,它依赖于不可用服务器上存储的内容。在这个例子中,记录 5 是已提交的,但在其余状况下,可能不是。能够确定的是咱们没法知道哪些记录已被提交了。因此咱们能作的就是找到一个候选者,这个候选者颇有可能包括全部已提交的记录,我先从直观上尝试解释如何作到的,而后在用精确的方式加以证实,咱们是可以挑选到候选者存有全部已提交的记录的。
咱们经过比较日志的方式来实现。当一个候选者发起投票请求,它会包括自身的日志记录信息,位置索引 index 以及该记录的任期号 term 。当响应投票的服务器接收到请求,它会将候选者的日志信息与本身的日志信息进行比较,若是投票者的日志更完整,那么它会拒绝投票(lastTerm v > lastTerm c)|| (lastTerm v == lastTerm c) && (lastIndex v > lastTerm c)。结果是赢得选举的服务器能够保证比大多数投票者有更完整的日志记录。
让咱们看看实际究竟是如何工做的。
最有趣的状况刚好是在领导者决定刚决定日志记录是已提交的时候,会有两种场景:
这里任期 2 以及领导者(S1)刚成功调用 AppendEntries 至 S3 ,此时它发现记录已在大多数服务器上存储,随即标记该记录是已提交的,并将其传送给状态机。此时这条记录是安全的,下一任期的领导者必须认定该记录的已提交状态。正如以前介绍的规则,S5 是没法成为下一任期的领导者,S4 也没法成为领导者,因此只有 S一、S二、S3 可能被选举成领导者,实际上,若是 S1 在它们中间,S1 必定能够保证赢得选举,但 S二、S3 也能够经过得到其余服务器(S四、S5)的投票,获胜成为领导者。但在任意一种状况下,下一任期的领导者都必须包含该日志记录。
第二种:提交的记录是在前序任期
在这种状态下,领导者在任期 2 只复制了两台服务上的日志记录,随后任期 3 的领导出(S5)于某些缘由没有关注到这些记录,在它本地建立了一些记录,而后崩溃了。而后在任期 4 上,领导者(S1)做为试图将其余服务器上的日志内容与它本身的达成一致。因此它让服务器 S3 复制了它本身 Term-2 记录,在这个点上,该记录已被领导者知道存于大多数服务器上,但该记录并无安全的被提交。由于此时 S1 可能出现崩溃,S5 成为领导者,由于它的前序任期值 3 较大,因此它能够得到来自于 S二、S三、S4 的投票,若是它当选,那么它会试图将本身的日志推到其余的服务器,这也就意味着从 S1 - S4 下标位置索引 3 开始的全部记录都会被删除。因此此时咱们还没法认定记录 3 是否已经提交。
在这种状况下,新的选举规则并不足以保证安全性(Safety),咱们还须要修改提交的规则。到目前为止只要领导者发现记录已存于大多数服务器,那么它就认为该记录已被提交。可是为了保证安全性,咱们须要增长另外一条规则。除了上述规则,领导者必须能看见至少有一条来自于它本任期内的记录也存于大多数服务器。回到以前的例子,若是领导者完成了记录 3-2 的复制,它此时还没法提交该记录并将其发送给状态机,取而代之的是,它必须等待直到它当前任期内的第一条记录(4-4)提交并存于大多数的服务器。至此,两条记录才能都发送给状态机。这么作的缘由在于,在这种状态下,服务器 S5 是不可能被选举为下届领导者的,由于有更多的服务器处于更近的任期(任期 4),服务器 S5 只能从服务器 S4 处获得选票。此时,记录 3 和 4 都是安全的。因此将新选举规则来比较日志与新提交规则相结合,咱们就能保证 Raft 的安全属性老是有效的。即一旦领导者决定记录已提交,它就会对将来的全部领导者可见。这里咱们展现的例子只说明,已提交的记录对下一任期的领导者可见,但也能够很容易就证实,每一个将来的领导者也会有相同的日志记录。
如今咱们能够保证安全性,也明白了日志是正确的。那么咱们如何让全部跟随者的日志都与领导者保持一致呢?首先,让咱们来看看日志不一致能够出现怎样的状况。
须要作的是剔除全部不一样的日志记录,并将全部丢失的记录根据领导者的日志填充完整。
要想恢复到一致状态,领导者会为每一个跟随者维护一个状态变量,这个变量称为 nextIndex ,这个变量存储日志的下一条记录的下标位置索引,服务器会把这个位置发送给跟随者(如上图所示,nextIndex = 11)。当一台服务器成为领导者后,它会将 nextIndex 值设置成当前日志记录的下一位置。因此在上面的例子中,任期 7 的领导者的最后一条记录的索引位置是 10 ,那么它会将 nextIndex 设置成 11 。领导者会根据 AppendEntries 调用发现一致性问题,由于当跟随者接收到 AppendEntries 调用时,都会进行检查。这个检查就能够发现全部的问题。因此当下一次领导者想要与跟随者进行通讯时,它都会包括下标位置索引(10)以及任期号(6)做为请求的参数。当选为领导者后,下一次请求也有多是以心跳检测的方式发送的,心跳检测与 AppendEntries 调用的方式同样,只是没有新值建立,但仍是包括一致性检查的。因此当消息到达跟随者(a)后,它会将接收到的下标位置索引与任期与本身的日志信息进行比较,并无匹配的记录,因此它会拒绝 AppendEntries 请求,当领导者收到拒绝的响应以后,它的响应很简单,它要作的只是将 nextIndex 减 1 ,因此这个值就变成了 10 。如此逐一减小,直到最终 nextIndex 为 5 的时候,领导者再次发送请求的信息会包括下标位置索引(4)以及任期号(4),这时它与跟随者(a)当前的日志记录信息是相匹配的,因此这时跟随者会接受 AppendEntries 请求,并追加记录 5-4 。直到领导者将跟随者的日志记录填充完整。类似的过程也会在跟随者(b)上出现。当 nextIndex 减小到 4 时,领导者会包括下标位置索引(3)以及任期号(1)做为请求的参数,并修正跟随者(b)上的日志记录。
这个过程还须要注意一点,当跟随者接收来自于领导者的替换请求时,它会将后续的日志记录截断并删除后续的全部日志记录,在上述的例子中,若是领导者发送请求(4-4),nextIndex = 4 ,这时跟随者的记录为 4-2 ,是不一致的,这时它不只会将 4-2 覆盖,同时还会删除剩余的全部记录,由于在不一致的记录后也都是不一致的记录。
如今对领导者发生变动的状况做个小结。整体上须要解决两个问题:一个是须要保证系统的安全性,第二个是一旦新的领导者开始行使权利,它要作的事情就是使全部跟随者上的日志记录与自身保持一致,AppendEntries 的一致性检查会为咱们提供全部的信息。
Raft 协议的第四步也是与领导者更替相关的。旧领导者有可能并非真的死了。例如出现了网络的隔离,将领导者与集群内其余服务器分隔,那么剩下的服务器会等待选举超时,并选举一个新领导者,那么问题来了,若是旧领导者又从新恢复链接怎么办?这个旧领导者并不知道已经从新进行了选举,也不知道新领导者的存在。因此这时它还会试图以领导者的身份继续运行,它还会与跟随者进行通讯,并试图让其余跟随者与本身的日志记录保持一致,咱们必须阻止这个事情的发生。
可使用任期来防止这种状况的出现。由于每一个 RPC 请求都包括发送者的任期号,当 RPC 接收时,接受者会将其与本身的任期号相比较,若是不匹配,则会更新那些过时的记录。因此若是发送者的任期比接收者的要老,那么就表示发送者是过期的,这时接收者会当即拒绝 RPC 请求,并将包括了接收者任期信息的响应发送回发送者,这样当发送者接收到响应时就会意识到,它的任期号是过时的,此时它就会停下并做为跟随者继续运行,同时它还会更新本身的任期号,并与其余服务器保持一致。反之,若是接收者的任期号更老,若是这时接收者不是跟随者,那么它也会停下,并做为跟随者,并且更新它本身的任期号。略微不一样的是接收者不会拒绝 RPC ,它会接收 RPC 请求。
这里比较有趣的是选举过程会致使任期号的更新,即当候选者请求投票并与大多数服务器发生通讯后,它会将本身的任期号随着 RPC 请求发送出去,这样全部的接收者都会更新本身的任期号,并与候选者保持一致,因此当新领导者被选出后,集群里的多数服务器都会更新到这个任期号。这也就意味着,一旦选举完成,被罢免的领导者是没法提交新记录的,由于它须要与至少一台服务器进行通讯,这样它就能发现本身的任期号更老,这时它就会中止领导者的行为并做为跟随者继续运行。
还有一些比较典型的场景,这里不做更多的讨论,但能够用任期号来处理全部相似的问题。
如今让咱们看看 Raft 协议的第五部分,即客户端是如何与系统进行交互的。这点并不复杂,客户端将命令发送给领导者,并得到响应,若是客户端不知道哪台服务器是领导者也不要紧,它能够与集群的任意一台服务器进行通讯,若是这台服务器不是领导者,那么它会告知客户端,并将客户端重定向到领导者,而后客户端会再次发送请求。只有在领导者记录下命令,并已经将其提交,而后发送给状态机执行以后,才会将结果返回给客户端。这里比较微妙的是,若是领导者发生崩溃或请求发生超时该怎么办?若是发生这种状况,客户端会随机挑选另外一台服务器并再次发送请求,最终它会将请求发送到新的领导者,新的领导这会执行该命令。这个能够保证命令最终总能被执行。
但这留有一个风险,即命令有可能被执行两次。
问题在于领导者会在执行完命令后响应客户端以前发生崩溃,因此命令自己是没法知道本身是否被记录或已被执行。这时客户端就会再次发起请求,这样命令就又被执行了一遍。这是不能被接受的,由于咱们要每条命令执行且仅被执行一次。Raft 解决这个问题的办法是让客户端为每条命令生成一个惟一的 ID ,并将其与命令一块儿发送给领导者,当领导者记录该条命令时,也会包括这个惟一 ID ,但在领导者接受命令以前,它会进行检查,看其余记录中是否已存在相同的 ID ,若是存在相同的,那么它就会知道该条命令请求是多余的,因此它会找到该条记录,并忽略这条新命令,并将老的执行结果返回给客户端。
因此只要客户端不崩溃,结果最多只会被执行一次。这也是咱们但愿系统应该具有的线性一致性。
接下来要介绍 Raft 协议的第六部分,也是最后一部分。
咱们已经有了应对配置发生变动的处理机制。当咱们提到配置,指的是集群服务器的信息,包括每台服务器的 ID 、网络地址等。这些信息都很是重要,由于咱们须要用它们来决定多数票的具体数量,从而进行领导者选举或用来提交日志记录。咱们要支持这些变动的缘由在于,好比当服务器出现失败的状况,它们能够被新的机器替换,或者集权管理员但愿能更改副本数量,咱们但愿全部的这些事情都能在安全自动的条件下完成,不要由于配置的变动致使系统出现故障或停机的状况。
必需要意识到,咱们没法直接从旧配置切换到新配置。咱们来看个例子。假设系统集群有三台服务器正在运行,这时咱们但愿再增长两台服务器,因此最终集群内会有五台服务器。若是咱们只是要求每台服务器从旧配置切到新配置,问题是这个切换不能没法同时完成,时间上总会有先有后。而这可能会致使冲突的大多数。由于 S一、S2 能够在某个时候造成旧集群的大多数,并决定领导者。而与此同时,另外三台服务器 S三、S四、S5 已经切至新的配置,它们也造成了该配置状态下的大多数。因此它们也能够决定领导者,确认提交状态。这样就会与 S一、S2 发生冲突。这样,咱们就须要使用两段协议(two-phase protocol),没法在一段内达到目的。
这固然也是全部分布式决策的所必须使用的方式。
解决方案是使用两段协议的方式来更改配置信息。
Raft 将第一阶段到中间阶段称为多边共识(joint consensus),在这个阶段中,集群包括全部的服务器上新旧两种配置,可是如选举和提交的决策,须要在新旧两个独立的配置状态下达成一致。
集群配置以 C(old) 开始,而后客户端向领导者发送请求,当接收者收到请求以后,会向日志里新增一条记录,要求记录新配置 C(old+new) ,配置与其余普通的命令记录同样,领导者会用 AppendEntries RPC 请求将其发送给集群的其余服务器,配置变动惟一的不一样在于它们会当即生效,一旦服务器将新配置记录到日志中,那么它就马上生效,并不须要等待该日志记录变为已提交状态。因此此时在领导者上已经认为 C(new) 已生效,那么若是配置C(old+new) 要生效,就要求该配置分别在新旧配置服务器下同时都成为大多数。又过了一会,当记录状态变成已提交后,也仍是可能存在决策在 C(old) 与 C(old+new) 决定。例如,若是领导者在记录新配置记录后就发生崩溃,有可能某些其余旧配置的机器仍然处于工做状态,被选举成领导者管理集群。但在某个时间点,C(old+new) 会变为已提交的状态,在此种状态下,任何机器就没法只根据 C(old) 来作出决策。为了让领导者被成功选举,它必须保证全部的记录都已提交,因此一旦 C(old+new) 记录已提交,它就能保证任意选举的领导者都有该记录,也就是说领导者已使用该配置。因此在这个时候,集群是处于联合共识下运行的,一旦联合共识被提交确认,领导者就能够将配置变动 C(new) 写入日志记录,并发送给集群其余服务器。因此在这个时候,集群下服务器配置可能在 C(new) 或 C(old+new) 的状态,由于这时服务器也可能再次出现崩溃,另外一服务器会替代成为领导者,并使用联合共识下的 C(old+new) 配置。但最终新配置记录 C(new) 会处于提交状态,一旦出现这种状况,集群全部将来的决策都将基于 C(new) 。因此关键在于,不存在 C(old) 或 C(new) 在不进行相互协调的前提下就能作出决策的状况。C(old) 能够独立作出决策,C(new) 也能够独立作出决策,可是二者不会发生重叠。在这两段时间之间,两个配置须要相互协调,这就能保证,集群不会两个独立的达成共识的群体存在。
在这里,两段协议是一个基础协议。任何共识性算法都须要使用两段协议来对配置进行变动,实际上任何分布式一致都须要两段协议。
这个协议还有些须要注意的地方。
在过分期间,有可能服务器来自于任何一种配置都能被选举为集群领导者,这里比较微妙的是若是当前的领导者不在新配置里,那么它最终会停下,并转换为跟随者。在 Raft 里,旧领导者在 C(new) 处于已提交状态后当即中止并转换成跟随者。这时其余的跟随者会超时,并选举新的领导者,这时被选举的领导者所使用的配置必定是 C(new) 。尽管如此,旧的领导者也仍是会领导一小段时间。
参考来源:
2013 Raft lecture, Diego Ongaro
Wiki: Byzantine fault tolerance