Consul实现原理---Raft算法

 公司的注册中心要调研选型,不再使用Eureka,我负责这件事,先调研了Consul,Raft算法真的很巧妙,可惜的是consul的一些设计架构无法在我们公司生产环境使用。

Raft将一致性问题分解成了三个独立的部分:leader选举、日志复制、安全性。

 

一、Raft一致性的实现 复制日志

        想要实现共识性算法主要有两种方式:第一种方式称为对称式或无主式,在这种方式下,所有的服务器都有相同的角色,它们有同等的权力,它们任何时候的行为几乎都是一样的,客户端可以与任何一台服务器进行通信。第二种方式称为非对称式或基于领导者(leader),服务器在任何时候都不是对等的,只有其中的一台服务器是领导者(leader),领导者负责集群的所有操作,其他的服务器只是简单地服从领导者发出的指令,在这种系统下,客户端永远与领导者通信,只有领导者才与其他的服务器发送通信。

        Raft 就是使用上面第二种方式。它将共识性算法的问题分解成两类不同的问题,一种是在领导者正常运行下,进行的普通操作;另一种是在领导者崩溃时,需要对领导者进行重新选举,这种方式有其优势,它让普通的操作变得非常简单,不需要关心是否有多个领导者相互发生冲突,或同时发出指令,只要有一个领导者控制全局,就可以完全按照它的指令来运行。Raft 算法的复杂之处在于领导者发生变化时,因为当领导者崩溃时,会使系统处于不一致的状态,后续被选举的领导者需要对此这些不一致状态进行清理。总体上说,基于领导者的方式要比无领导者的方式简单,因为无须担心不同服务器间会出现冲突,只须关心领导者发生变化的情况。

 

Raft 算法共分成 6 个部分,首先我们要介绍的就是领导者的选举。

  1. 如何从所有的服务器中选择领导者?如何在当作为领导者的服务器崩溃时能检测到故障并挑选另一个领导者来替代它?

  2. 会介绍当领导者接收到客户端请求时,系统是如何处理正常操作的。这是 Raft 算法中最简单的部分。

  3. 会讨论领导者发生改变的情况,这部分是 Raft 中最复杂的,也是保证整个系统行为最重要的部分。首先,会讨论什么叫做安全,如何保证安全?其次,领导者是如何识别日志的一致性的,从而可以将系统恢复到处于一致状态下。

  4. 会讨论领导者发生改变时的另一个问题。如何让曾经崩溃死机的老领导者,重新回归到集群后集群的状态仍然能保持一致。

  5. 会谈论客户端是如何与集群交互的。关键点在于客户端是如何处理服务器崩溃,如何保证客户端发送的命令是线性的,即操作执行也仅执行一次。

  6. 最后会讨论如何处理配置变更的情况,即如何对集群增加或移除服务器。      

 

          Raft首先选出一个杰出的leader,然后给予其管理日志复制的全部职责来实现一致性。该leader从客户端接收日志条目,将他们复制到其他服务器,并告诉他们从状态机获取日志条目是安全的。

         在任何给定的时间,每个服务器都是三种状态之一:leader(领导者),follower(跟随者),candidate(候选人)

         002827_APoV_271522.png

      Raft将时间分成任意长度的term。term以一串连续的数字进行编号。每个term都以一次选举开始,其中一个或多个candidate尝试成为leader,如果一个candidate竞选成功,那么它将作为leader服务其他服务器。Raft保证任何一个给定的term内,至多只有一个leader。每个term开始一个选举。一次成功的选举后,一个leader管理整个集群直到term结束。在 Raft 系统的所有服务器都保持着一个被称为当前任期的值,这个信息必须存于服务器的可靠媒介中(如硬盘)。这样就能在服务器崩溃之后得以重启并恢复。任期这个概念十分重要,它使 Raft 可以判断过期的信息。

     

        每台服务器无论是领导者还是跟随者,都各自保存一个日志副本。日志本身被分成了多条记录(Entries),记录是由下标索引的位置和term来进行唯一标识的。

  • 需要保证所有的状态机,以相同的顺序执行相同日志记录的命令。为了达成总体的安全性要求,Raft 实现了一个安全属性,一旦领导者决定某个特定记录已提交,那么 Raft 就需要保证该条记录会出现在它所有未来领导者的日志记录中,并且也处于已提交状态。,领导者永远不会覆盖日志记录,它只会追加,并且只有leader才能committed
  • 如果不同日志中的两个条目拥有相同的索引和term值,那么他们储存相同的命令。领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变。

  • 如果不同日志中的两个条目拥有相同的索引和term值,那么日志中之前的条目都相同。第二个特性由附加日志 RPC 的一个简单的一致性检查所保证。在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面。如果跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查保护了日志匹配特性当日志扩展的时候。因此,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志一定是和自己相同的了。

挑选最好的领导者

拒绝投票:(0lastTerm v > lastTerm c)|| ((lastTerm v == lastTerm c) && (lastIndex v > lastTerm c))

 

       

平衡旧领导者(Neutralizing Old Leader)

所以如果发送者的任期比接收者的要老,那么就表示发送者是过时的,这时接收者会立即拒绝 RPC 请求,并将包括了接收者任期信息的响应发送回发送者,这样当发送者接收到响应时就会意识到,它的任期号是过期的,此时它就会停下并作为跟随者继续运行,同时它还会更新自己的任期号,并与其他服务器保持一致。反之,如果接收者的任期号更老,如果这时接收者不是跟随者,那么它也会停下,并作为跟随者,而且更新它自己的任期号。略微不同的是接收者不会拒绝 RPC ,它会接收 RPC 请求。

与客户端通信

        客户端是如何与系统进行交互的。这点并不复杂,客户端将命令发送给领导者,并获得响应,如果客户端不知道哪台服务器是领导者也没关系,它可以与集群的任意一台服务器进行通信,如果这台服务器不是领导者,那么它会告知客户端,并将客户端重定向到领导者,然后客户端会再次发送请求。只有在领导者记录下命令,并已经将其提交,然后发送给状态机执行之后,才会将结果返回给客户端。这里比较微妙的是,如果领导者发生崩溃或请求发生超时该怎么办?如果发生这种情况,客户端会随机挑选另一台服务器并再次发送请求,最终它会将请求发送到新的领导者,新的领导这会执行该命令。这个可以保证命令最终总能被执行。

        问题在于领导者会在执行完命令后响应客户端之前发生崩溃,所以命令本身是无法知道自己是否被记录或已被执行。这时客户端就会再次发起请求,这样命令就又被执行了一遍。这是不能被接受的,因为我们要每条命令执行且仅被执行一次。Raft 解决这个问题的办法是让客户端为每条命令生成一个唯一的 ID ,并将其与命令一起发送给领导者,当领导者记录该条命令时,也会包括这个唯一 ID ,但在领导者接受命令之前,它会进行检查,看其他记录中是否已存在相同的 ID ,如果存在相同的,那么它就会知道该条命令请求是多余的,所以它会找到该条记录,并忽略这条新命令,并将老的执行结果返回给客户端。

        所以只要客户端不崩溃,结果最多只会被执行一次。这也是我们希望系统应该具备的线性一致性。

 

配置变更

 

uploading.4e448015.gif正在上传…重新上传取消

        必须要意识到,我们无法直接从旧配置切换到新配置。我们来看个例子。假设系统集群有三台服务器正在运行,这时我们希望再增加两台服务器,所以最终集群内会有五台服务器。如果我们只是要求每台服务器从旧配置切到新配置,问题是这个切换不能无法同时完成,时间上总会有先有后。而这可能会导致冲突的大多数。因为 S1、S2 可以在某个时候形成旧集群的大多数,并决定领导者。而与此同时,另外三台服务器 S3、S4、S5 已经切至新的配置,它们也形成了该配置状态下的大多数。所以它们也可以决定领导者,确认提交状态。这样就会与 S1、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) 也可以独立做出决策,但是两者不会发生重叠。在这两段时间之间,两个配置需要相互协调,这就能保证,集群不会两个独立的达成共识的群体存在。

 

 

 

uploading.4e448015.gif正在上传…重新上传取消

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章