编程语言
首页 > 编程语言> > 共识算法:Raft协议介绍

共识算法:Raft协议介绍

作者:互联网

一、背景概述

在分布式系统中,通常需要多副本进行备份,但是副本的同步一致一直是一个比较棘手的问题。Raft算法是一个能够代替Paxos的分布式一致性算法,能够管理日志复制(replicated log),他的性能与Paxos不相上下,但是却比Paxos更容易理解。

Paxos存在的问题:

  1. 难以理解(不过《Paxos Made Simple》比较容易理解)。一是因为paxos选择了single-degree Paxos作为基础,二是因为Multi-Paxos 的合成规则又增加了许多复杂性。
  2. 不能为构建实际的实现提供良好的基础,一是因为没有针对 multi-Paxos 的共识算法,二是因为Paxos 的架构对于构建实际系统来说是一个糟糕的设计。

一致性算法通常具有的属性:

  1. 能确保在所有非拜占庭条件下(比如网络延迟,分区和数据包丢失,重复和乱序)的安全性(不会返回错误结果)
  2. 若有2f+1个副本,则能容忍f台机器故障。
  3. 不依赖时钟保证日志的一致性(分布式情况下网络延迟等问题会导致可用性)。
  4. 通常只要超过一半的服务器响应成功调用,则认为命令已经完成。

Raft新特性:

  1. 强leader:日志只从leader流向其他服务器(简化了日志管理)
  2. Leader选举:使用随机计时器进行 leader 选举。 这只需在任何一致性算法都需要的心跳(heartbeats)上增加少量机制,同时能够简单快速地解决冲突。
  3. 成员变更:允许集群在配置更改期间继续正常运行。

Raft提高可理解的方式:模块分解(leader选举、日志复制、安全性)、减少状态空间。

二、复制状态机

复制状态机用于解决分布式系统中的各种容错问题,通常通过复制日志实现,每个服务器存储一个包含一系列命令的日志,其状态机按顺序执行日志中的命令。每个状态机处理日志的顺序相同,因此也能够得到相同的输出序列。

一致性算法的工作就是保证复制日志的一致性。 每台服务器上的一致性模块接收来自客户端的命令,并将它们添加到其日志中。 它与其他服务器上的一致性模块通信,以确保每个日志最终以相同的顺序包含相同的命令,即使有一些服务器失败。 一旦命令被正确复制,每个服务器上的状态机按日志顺序处理它们,并将输出返回给客户端。 这样就形成了高可用的复制状态机。

 

三、Raft基础

3.1 任期

Raft 把时间分割成任意长度的任期(term),每个任期从选举开始,一个或者多个 candidate 尝试成为 leader 。如果一个 candidate 赢得选举,然后他就在该任期剩下的时间里充当 leader。如果没选出leader,则任期很快结束,开启下一个leader,每个任期最多只能有一个leader。

在raft中,任期充当逻辑时钟,服务器节点可以通过任期发现一些过期的信息比如过时的 leader。任期单调递增,服务器之间通信的时候会交换当前任期号;如果一个服务器的当前任期号比其他的小,该服务器会将自己的任期号更新为较大的那个值。如果一个 candidate 或者 leader 发现自己的任期号过期了,它会立即回到 follower 状态。如果一个节点接收到一个包含过期的任期号的请求,它会直接拒绝这个请求。

3.2 通信

Raft使用RPC进行通信,主要有三种类型RPC:

  1. 请求投票(RequestVote)RPC, 由 candidate 在选举期间发起。
  2. 追加条目(AppendEntries)RPC, 由 leader 发起,用来复制日志和提供一种心跳机制
  3. 重试RPC,当服务器没有及时的收到 RPC 的响应时,会进行重试。

四、Raft三大模块

4.1 状态与leader选举

4.1.1 状态

服务器状态:

  1. Leader:副本中仅一个leader,负责处理所有客户端请求
  2. Follower:不发送任何请求,仅响应来自leader与candidate的请求。
  3. Candidate:用于选举新的leader。

服务器状态几个点:

  1. 当服务器程序启动时,他们都是 follower
  2. 一个服务器节点只要能从 leader 或 candidate 处接收到有效的 RPC 就一直保持 follower 状态
  3. Leader 周期性地向所有 follower 发送心跳(不包含日志条目的 AppendEntries RPC)来维持自己的地位
  4. 如果一个 follower 在一段选举超时时间内没有接收到任何消息,它就假设系统中没有可用的 leader ,然后开始进行选举以选出新的 leader。

4.1.2 leader选举

选举规则:

follower 先增加自己的当前任期号并且转换到 candidate 状态;票给自己并且并行地向集群中的其他服务器节点发送 RequestVote RPC。Candidate 会一直保持当前状态直到以下三件事情之一发生:

  1. 收到过半投票,赢得选举:对于同一个任期,每个服务器节点只会投给一个 candidate ,按照先来先服务(first-come-first-served)的原则。一旦 candidate 赢得选举,就立即成为 leader 。然后它会向其他的服务器节点发送心跳消息来确定自己的地位并阻止新的选举。
  2. 其他的服务器节点成为 leader:candidate等待投票时可能会收到其他candidate的AppendEntries RPC,如果这个 leader 的任期号(包含在RPC中)不小于 candidate 当前的任期号,那么 candidate 会承认该 leader 的合法地位并回到 follower 状态。 如果 RPC 中的任期号比自己的小,那么 candidate 就会拒绝这次的 RPC 并且继续保持 candidate 状态。
  3. 一段时间之后没有任何获胜者:如果有多个 follower 同时成为 candidate ,那么选票可能会被瓜分以至于没有 candidate 赢得过半的投票。当这种情况发生时,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。

为了防止第三种情况无限重复,使得没有leader选举出来,raft做了以下工作:

  1. 使用随机选举超时时间:选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择,避免同时出现多个candidate。
  2. Candidate等待超时的时间随机:candidate 在开始一次选举的时候会重置一个随机的选举超时时间,然后一直等待直到选举超时;这样减小了在新的选举中再次发生选票瓜分情况的可能性。

4.2 日志复制

4.2.1 复制规则

日志定义:客户端的每一个请求都包含一条将被复制状态机执行的指令。Leader 把该指令作为一个新的条目追加到日志中去,然后并行的发起 AppendEntries RPC ,同步到其他副本。

安全复制:当有超过一半的副本复制好了该条目,便认为已经安全的复制,leader接着会将条目应用到他的状态机中,并将结果返回给客户端。若遇到网络故障或丢失包,leader 会不断地重试 AppendEntries RPC(即使已经回复了客户端)直到所有的 follower 最终都存储了所有的日志条目。

日志索引:日志如图所示,每个日志条目存储一条状态机指令和 leader 收到该指令时的任期号,同时每个日志条目都有一个整数索引值来表明它在日志中的位置。

日志提交:一旦创建该日志条目的 leader 将它复制到过半的服务器上,该日志条目就会被提交,raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。不仅如此,leader 日志中该日志条目之前的所有日志条目也都是已经提交的条目。Leader会保存当前已提交最大的日志索引,未来的所有 AppendEntries RPC 都会包含该索引,这样其他的服务器才能最终知道哪些日志条目需要被提交。Follower 一旦知道某个日志条目已经被提交就会将该日志条目按顺序应用到自己的本地状态机中。

日志条目的两点:

  1. 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
  2. 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也都相同一致性检查在发送 AppendEntries RPC 的时候,leader 会将前一个日志条目的索引位置和任期号包含在里面。如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。

4.2.2 复制故障说明

故障情况:正常操作期间,leader 和 follower 的日志保持一致,所以 AppendEntries RPC 的一致性检查从来不会失败。但也有可能出现下图的故障:当一个 leader 成功当选时(最上面那条日志),follower 可能是(a-f)中的任何情况。每一个盒子表示一个日志条目;里面的数字表示任期号。Follower 可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。例如,场景 f 可能这样发生,f 对应的服务器在任期 2 的时候是 leader ,追加了一些日志条目到自己的日志中,一条都还没提交(commit)就崩溃了;该服务器很快重启,在任期 3 重新被选为 leader,又追加了一些日志条目到自己的日志中;在这些任期 2 和任期 3 中的日志都还没被提交之前,该服务器又宕机了,并且在接下来的几个任期里一直处于宕机状态。

leader 通过强制 follower 复制它的日志来解决不一致的问题。这意味着 follower 中跟 leader 冲突的日志条目会被 leader 的日志条目覆盖。

  1. 情形一:要使得 follower 的日志跟自己一致,leader 必须找到两者达成一致的最大的日志条目(索引最大),删除 follower 日志中从那个点之后的所有日志条目,并且将自己从那个点之后的所有日志条目发送给 follower 。
  2. 选举时:Leader 针对每一个 follower 都维护了一个 nextIndex ,表示 leader 要发送给 follower 的下一个日志条目的索引。当选出一个新 leader 时,该 leader 将所有 nextIndex 的值都初始化为自己最后一个日志条目的 index 加1(图 7 中的 11)。如果 follower 的日志和 leader 的不一致,那么下一次 AppendEntries RPC 中的一致性检查就会失败。在被 follower 拒绝之后,leaer 就会减小 nextIndex 值并重试 AppendEntries RPC 。最终 nextIndex 会在某个位置使得 leader 和 follower 的日志达成一致。此时,AppendEntries RPC 就会成功,将 follower 中跟 leader 冲突的日志条目全部删除然后追加 leader 中的日志条目(如果有需要追加的日志条目的话)。一旦 AppendEntries RPC 成功,follower 的日志就和 leader 一致,并且在该任期接下来的时间里保持一致。

优化:如果想要的话,该协议可以被优化来减少被拒绝的 AppendEntries RPC 的个数。例如,当拒绝一个 AppendEntries RPC 的请求的时候,follower 可以包含冲突条目的任期号和自己存储的那个任期的第一个 index 。借助这些信息,leader 可以跳过那个任期内所有冲突的日志条目来减小 nextIndex;这样就变成每个有冲突日志条目的任期需要一个 AppendEntries RPC 而不是每个条目一次。在实践中,我们认为这种优化是没有必要的,因为失败不经常发生并且也不可能有很多不一致的日志条目。

4.3 安全性

目的:防止缺少很多日志条目的follower被选举为leader

实现方式:

  1. 增加选举限制(限制保证了对于给定的任意任期号, leader 都包含了之前各个任期所有被提交的日志条目)
  2. 提交之前任期内的日志条目

4.3.1 选举限制

Raft 使用投票的方式来阻止 candidate 赢得选举除非该 candidate 包含了所有已经提交的日志条目。候选人为了赢得选举必须与集群中的过半节点通信,这意味着至少其中一个服务器节点包含了所有已提交的日志条目。如果 candidate 的日志至少和过半的服务器节点一样新(接下来会精确地定义“新”),那么他一定包含了所有已经提交的日志条目。

投票规则:RPC 中包含了 candidate 的日志信息,如果投票者自己的日志比 candidate 的还新,它会拒绝掉该投票请求。(如果两份日志最后条目的任期号不同,那么任期号大的日志更新。如果两份日志最后条目的任期号相同,那么日志较长的那个更新。)

4.3.2 提交之前任期内的日志条目

存在的问题:如图的时间序列展示了为什么 leader 无法判断老的任期号内的日志是否已经被提交。在 (a) 中,S1 是 leader ,部分地复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 中通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,继续复制日志。此时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。但是,在崩溃之前,如果 S1 在自己的任期里复制了日志条目到大多数机器上,如 (e) 中,然后这个条目就会被提交(S5 就不可能选举成功)。 在这种情况下,之前的所有日志也被提交了。

解决:Raft 不通过计算副本数目的方式来提交之前任期内的日志条目。只有 leader 当前任期内的日志条目才通过计算副本数目的方式来提交;

五、其他问题

5.1 Follower 和 candidate 崩溃

如果 follower 或者 candidate 崩溃了,那么后续发送给他们的 RequestVote 和 AppendEntries RPCs 都会失败。Raft 通过无限的重试来处理这种失败;如果崩溃的机器重启了,那么这些 RPC 就会成功地完成。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在它重启之后就会再次收到同样的请求。Raft 的 RPCs 都是幂等的,所以这样的重试不会造成任何伤害。例如,一个 follower 如果收到 AppendEntries 请求但是它的日志中已经包含了这些日志条目,它就会直接忽略这个新的请求中的这些日志条目。

5.2 随机时间规则

只要整个系统满足下面的时间要求,Raft 就可以选举出并维持一个稳定的 leader:广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)

5.3 集群成员变更

为了使配置变更机制能够安全,在转换的过程中不能够存在任何时间点使得同一个任期里可能选出两个 leader 。

存在的问题:直接从一种配置转到另一种配置是不安全的,因为各个机器会在不同的时候进行转换。在这个例子中,集群从 3 台机器变成了 5 台。不幸的是,存在这样的一个时间点,同一个任期里两个不同的 leader 会被选出。一个获得旧配置里过半机器的投票,一个获得新配置里过半机器的投票。

方法:联合一致(joint consensus):

  1. 日志条目被复制给集群中新、老配置的所有服务器。
  2. 新、旧配置的服务器都可以成为 leader 。
  3. 达成一致(针对选举和提交)需要分别在两种配置上获得过半的支持。

配置变更步骤:

  1. Leader接收到变更配置请求,将从C-old变更到C-new,因此需要为联合一致的配置增加一个日志条目,并复制到其他follower。
  2. 收到该日志的副本将使用联合一致的方式配置和选举,未收到的副本按照原来的规则选举。在此期间C-new无法单独做出决策,新 leader 可能是在 C-old 配置也可能是在 C-old,new 配置下选出来的,这取决于赢得选举的 candidate 是否已经接收到了 C-old,new 配置。
  3. 当联合一致的日志条目提交后,C-old 和 C-new 都不能在没有得到对方认可的情况下做出决定,只有拥有 C-old,new 日志条目的服务器才能被选举为 leader。
  4. leader 创建一个描述 C-new 配置的日志条目并复制到集群其他节点(此时已经安全),新的配置被服务器收到后就会立即生效,旧的配置就变得无关紧要。

配置变更时存在的问题:

  1. 新的服务器开始时可能没有存储任何的日志条目:Raft 在配置变更前引入了一个额外的阶段,在该阶段,新的服务器以没有投票权身份加入到集群中来(leader 也复制日志给它们,但是考虑过半的时候不用考虑它们)。一旦该新的服务器追赶上了集群中的其他机器,配置变更就可以按上面描述的方式进行。
  2. 集群的 leader 可能不是新配置中的一员:在这种情况下,leader 一旦提交了 C-new 日志条目就会退位(回到 follower 状态),这意味着有这样的一段时间(leader 提交 C-new 期间),leader 管理着一个不包括自己的集群;它复制着日志但不把自己算在过半里面。Leader 转换发生在 C-new 被提交的时候,因为这是新配置可以独立运转的最早时刻(将总是能够在 C-new 配置下选出新的领导人)。在此之前,可能只能从 C-old 中选出领导人。
  3. 那些被移除的服务器(不在 C-new 中)可能会扰乱集群:这些服务器将不会再接收到心跳,所以当选举超时,它们就会进行新的选举过程,发送带有新任期号的 RequestVote RPCs,当服务器认为当前 leader 存在时,服务器会忽略RequestVote RPCs。

欢迎关注公众号:BugGo

 

标签:任期,follower,条目,算法,服务器,共识,Raft,日志,leader
来源: https://blog.csdn.net/qq_34924156/article/details/111589352