Raft算法概述

最近公司在选型分布式框架时集中研究对比了Paxos和Raft一致性算法实现得原理,说实话Paxos在百度百科得描述真的复杂,可能需要反复看很多遍才能理解个大概,而Raft得原理图虽然也不简单但是多看几次还是能理解得

 

Raft 是一种共识算法,旨在替代 Paxos。

在提供不亚于 Paxos 的性能、可靠性、可用性的同时,Raft 通过逻辑分离的方式使其比 Paxos 更容易理解(UnderStandable),但它也被正式证明是安全的,并提供了一些额外的功能。

Raft 提供了一种在计算系统集群中分布状态机的通用方法,确保集群中的每个节点都同意一系列相同的状态转换。

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

  • 强领导者(Strong leader):和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
  • 领导选举(Leader election):Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
  • 成员关系调整(Membership changes):Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。

 

基本概念

一个典型的 Raft 集群包含多个节点。在任意时刻,每个节点都将处于以下三个状态之一:

  • leader:处理所有来自客户端的请求,封装为可重入的日志记录(log entry),并将其复制给其他 follower 节点,并在收到超过半数的确认后将日志提交。
  • follower:follower 是被动的,他们自己不产生请求,只处理 leaders 和 candidates 的请求并回应。
  • candidate:follower 被选举成为 leader 之前的状态。

Raft 集群的节点之间只存在两种 RPC 调用:

  • RequestVote RPC:发起于 candidate 节点,作用于其他节点,用于收集选票,只有当 candidate 当前日志的 term 和 index 都比当前节点
  • AppendEntries RPC:发起于 leader 节点,作用于 follower 节点,用于将 leader 的 log entries 复制给 follower。当 AppendEntries RPC 不包含 log entry 时,则其作为心跳(heartbeat)告知所有 follower 节点 leader 的存活。

 

上图说明了 Raft 集群的节点状态转换。

所有节点启动时都是 follower 状态,当一个 follower 在一段时间内没有收到来自其他节点的请求或心跳,其超时并转化为一个 candidate 并发起选举。

当收到大多数节点的同意后,candidate 转化为 leader,并开始处理其任期内的一切操作,直到其发现了集群中处于更高 term 的节点,则再次转化为 follower 接受其他请求。

上图说明了 Raft 集群的时间状态转换。

时间轴被分解为不同的 term,每个 term 均开始于一次选举。如果选举成功,一个新 leader 会“统治”这个集群直到下一个 term。如果有多个 candidate 且每个都没有收到过半选票,则本 term 结束,直接进入下一 term 重新选举。

term 作为逻辑时钟而存在。每个节点都保存一个单调递增的 term 编号,用于在 RPC 通信时进行鉴别。如果一个节点收到更高 term 的请求或心跳,则其更新自己的 term 为此更高的 term,并且,当此节点为 candidate 或 leader 节点时,表示已经有更高 term 的 candidate/leader 存在了,则其将自己转化为 follower 节点。

为了更好的理解 Raft 算法,我们将对 Raft 的四个分解子问题进行介绍:

  • 选举(leader election)
  • 日志复制(log replication)
  • 安全性(safety)
  • 成员关系调整(membership changes)

 

选举(leader election)

当一个 follower 节点在一个选举超时时间(election timeout)内都没有接收到心跳或请求,则其将其自身 term 加 1,并转换为 candidate 并发起选举。

candidate 会并行向集群中的其他节点发起 RequestVote RPC,并持续此状态直到下列三种情况的一种发生:

  1. 获得多数选票,从而赢得此次选举成为 leader
  2. 收到相同或更高 term 的 leader 发来的 AppendEntries RPC
  3. 在选举超时时间(election timeout)内没有出现 1 情况和 2 情况

第 3 种情况发生一般是因为集群中同时产生了多个 candidate,每个都没有获得多数选票。这个时候 candidate 会重新将 term+1 并发起一次新的选举。

问题:如果多个 candidate 又重新同时发起选举,情况 3 大概率会重现。

Raft 采用随机选举超时时间来解决这个问题,即每个 candidate 有一个随机的超时时间(例如 150-300ms),因此在绝大多数情况 3 中只有一个 candidate 会超时并发起新一轮选举,大大降低了情况 3 再次发生的几率。

 

日志复制(log replication)

如前文中所说,leader 的工作就是将客户端的请求封装为日志(log entries),然后并行地复制给其他 follower 节点,并在收到超过半数的确认后,将日志提交并返回结果给客户端。

Raft 中的每个日志记录都带有一个 term 号以及一个 log index 来唯一标识。日志记录具有两个特性:

  • 如果两条不同节点的某两个日志记录具有相同的 term 和 index 号,则两条记录一定是完全相同的
  • 如果两条不同节点的某两个日志记录具有相同的 term 和 index 号,则两条记录之前的所有记录也一定是完全相同的。

第一个特性是由于 leader 产生的日志的唯一性与顺序不变性,而第二个特性是因为 leader 所发送的 AppendEntries RPC 会带有前一条日志的 term 和 index,如果 follower 对比其与自身 log 的最后一条不一致,则会拒绝 leader 发来的请求。

上图展示了一个 Raft 集群中的日志复制过程。,如果一个日志记录的 term 号与 log index 与另外一个日志记录相同,那么这两个日志记录就是相同的。

当一条日志为多数节点所接收(如图中的 index 7),则 leader 则会将此日志提交,这也会同时使得之前的日志被提交(如图中的 index1-index6,相关机制会在之后的安全性中介绍)。随后 follower 知晓 leader 的提交后,再将此日志提交。

问题:如果leader还没来得及将日志复制到多数节点就crash了,则会导致部分follower节点缺少或多余一些日志。

 

上图列举了例子中可以看到。对于一个已选举出的 leader,follower a-f 都是有可能产生的情形。

a 和 b 的情况是没有完全收到来自 leader 的 AppendEntries RPC,而 c-f 则是带有不同时期的未提交的日志(有可能是他们当 leader 时产生的,但没有提交就 crash了)。

Raft 为了解决这种问题,使用下面的方式强制将自己的日志复制给 followers:

  1. 选举成功后,leader 从来不会覆盖,删除或者修改其日志。
  2. 选举成功后,leader 会初始化一个数组 nextIndex[],长度为集群中其他 follower 个数,并初始化数组中所有值为当前自身日志的 index+1。nextIndex[] 中,对应的值表示本 leader 将给对应 follower 发送的下一条日志 index。
  3. leader 根据 nextIndex[],给对应 follower 发送对应的日志。若 follower 对比其前一条 log 不一致,则会拒绝 leader 发来的请求。此时 leader 就将其在 nextIndex[] 中的对应值减一。
  4. leader 不断重试,直到 follower 比对成功。follower 接受 AppendEntries RPC,并抛弃所有有冲突的日志。
  5. leader 按照自身日志顺序将日志正常复制给 follower,并不断将 nextIndex[] 对应值 +1,直到对应值“追上”自身日志的 index 为止。

通过以上的方式,Raft 实现了对从 leader 到 followers 的日志复制,并确保少于一半的节点故障不会影响系统的正常运行和性能。

 

安全性

上述我们介绍了 Raft 的两个主要过程:日志复制和选举。

本节介绍 Raft 通过如何方式解决了由于 crash 导致的 leader 切换而带来的完备性(leader completeness)问题。

选举约束(Election restriction)

不难发现,如果仅根据前文所述的方式进行重新选举 leader 时,如果新选举出的 leader 的日志不完全包含上一个 leader 的日志,即使这些“丢失的”日志已经被复制到了大多数 follower 节点(甚至已经被提交),新的 leader 还是会把它们直接覆盖掉。

Raft 使用在选举时,follower 通过对 candidate 的日志 term 和 index 的校验来应对这个问题。也就是说,一个 follower 在收到 candidate 的 RequestVoteRPC 后,会对其包含的 candidate 的最新日志的 term 号与 index 校验,当均至少大于等于自身的 term 和 index 时,表示 candidate 的日志比自己的更“新”,因此才会给 candidate 投票。

由于一个 candidate 当选需要超过半数的节点投票,因此这些节点中必然至少有一个节点包含了最新的日志,如果这个节点同意了选举,就表示此 candidate 也包含了最新的日志。

前序任期的未提交日志

实际上,选举约束只解决了已经被提交的日志的完备性问题。对于没有被提交的前序 term 的日志,还存在一种较为复杂的情况。

 

上图的情况会导致来自前序 term 的未被提交的日志被提交后,有可能会被再次覆盖:

  1. 图 a 中 S1 为 term2 的 leader,并正将黄色日志复制给 follower,但只复制给了 S2 便 crash 了失去了 leader 权。
  2. 接下来进入图 b,S5 当选 term3 的 leader,并产生蓝色日志,但还没来得及开始此日志复制,便也 crash了。
  3. 在图 c 中,S1 重新当选了 leader,并先将黄色日志复制给其他 follower。当复制给 S3 之后,过半并将黄色日志提交,但好景不长,它又 crash了。
  4. 在图 d 中,S5 由于最新的日志为 term3-index2,比 S2,S3,S4 都新,因此获得了多数选票又当选了 leader,则不难发现,在接下来的复制过程中,S5 将把自己的蓝色日志复制给其他 follower,因此 S1,S2,S3 的已提交的黄色日志又被覆盖了。

产生这个问题的根本原因其实在于图 c。 图 c 中,处于 term4 的 S1 节点 commit 了来自于 term2 的黄色日志,但此时已经是 term4 了,因此就会产生被更高 term 日志覆盖掉的风险。

因此,Raft 规定,leader 只提交属于当前 term 的日志,对于之前 term 的未提交日志,将在提交当前 term 的日志时一并提交。

如图 e 的 S1 节点就遵守了这一约定,在图 c 的情境中,先不提交黄色日志,而在复制 term4 的红色日志时才将黄色日志提交。由此可见 S5 由于日志不够新,不能再像图 d 中那样赢得选举,问题被完美解决了。

另外,Raft 算法原文还有一段对 Raft 算法安全性非常精彩的论述证明。感兴趣的同学可以阅读一下。

其他节点的 crash

另外,对于 follower 和 candidate 节点的 crash,影响远没有 leader 的 crash 复杂。

当其他节点 crash 时,发给 follower 的 AppendEntries RPC 和 candidate 发送的 RequestVote RPC 均会超时/失败。此时 Raft 算法不会调整集群,只会不断重试这些请求直到成功。

因此小于半数的节点 crash 都不会影响 Raft 集群的正常运行,而且只需要等到 crash 的节点服务恢复, nextIndex[] 机制又会将缺失的日志重新补齐。

成员关系调整(Membership changes)

在运行情况中,如果需要更改集群配置,如增加一些节点,或减少一些节点,或兼有之。Raft 算法保证可以在服务可用且保证一致性的情况下变更配置。

 

如图所示,Server1-5 在不同的时间进行配置的切换,在一个时刻出现了两种不同的集群配置。事实上,从理论层面所有的节点不可能在同一时刻将集群配置从旧切换到新,因此一次性切换是不安全的。

Raft 算法使用一种称为”联合一致”(joint consensus)的方式保证配置切换时的一致性。

如上图的时间线,旧的集群配置为 Cold,新的集群配置为 Cnew:

  1. 旧集群 leader 收到切换至 Cnew 的请求后,其先向 Cold 与 Cnew 的并集 Cold,new 复制新的配置 Cold,new entry。
  2. 收到新配置 Cold,new entry 的节点将其作为当前集群配置更换,无论此新配置是否已经被提交
  3. 旧集群 leader 根据 Cold,new 的集群来确认此 Cold,new entry 是否可以提交。
  4. 若此时旧集群 leader crash,则 candidate 可以根据 Cold 或者 Cold,new 的集群配置来竞选自己,取决于此 candidate 是否已经接到过配置 Cold,new entry。
  5. 一旦联合集群配置 Cold,new entry 被提交,Cold 与 Cnew 就均不能独自作决定了,接下来便开始向新集群 Cnew 切换(并且,之前的选举限制将决定此时只有包含 Cold,new entry 的节点能够被竞选为 leader)。
  6. 接下来的过程相似,新的配置 Cnew entry 将以相同的方式被复制到 Cnew 的节点上,直到新配置 Cnew entry 被提交。

 

问题 1:对于 Cnew 中新添加的节点,其不包含之前所有的日志,因此在其被添加入集群时,会有很长一段时间的日志复制过程。这个过程有可能会阻碍新的 log entry 被 commit,从而导致服务不可用。

Raft 引入了一个额外的阶段来解决这个问题。在新节点的日志复制过程中,新节点被剥夺投票权(即 candidate 计算投票时不将其计算在内),直到其 log index 追上 leader(在 ETCD 实现的 Raft 中,这种节点被称为 learner)。

问题 2:旧 leader 不包含在 Cnew 中。

对于旧 leader 不包含在 Cnew 中的情况。当旧 leader 开始将 Cnew entry 复制给 Cnew 中的节点但还未提交的这段时间内,leader 在统计过半数量时,不将自己统计在内。一旦 Cnew entry 被提交,则此 leader 转化为 follower,而新集群 Cnew 中选举出新的 leader。

问题 3:被移除的节点有可能会干扰新的集群 Cnew。

一旦新集群 Cnew 建立,Cold 中被移除的节点就将不再接收到新 leader 的心跳。当达到选举超时时间时,这些被移除的节点就会成为 candidate。他们向 Cold 与 Cnew 交集的部分发送的 RequestVote RPC 带有更高的 term 编号,这有可能会干扰新集群的 leader,使得其转换为 follower。

Raft 设置了一个最小选举超时时间(minimum election timeout),随机的选举超时时间均应大于这个最小选举超时时间。

follower 节点在收到 RequestVote RPC 时,如果其与上一次收到的 AppendEntries RPC 的时间间隔小于一个 minimum election timeout,这时可以确认 leader 是仍然存在的,不应该有正常的 follower 超时,因此忽略这个 RequestVote RPC。

从而解决了这个问题。

ETCD 中对 Raft 算法的实现

在 ETCD 中,Raft 包内为的 Raft 算法的具体实现。ETCD 通过将 Raft 算法抽象为 node 接口,而本身 ETCDServer 模块则作为一个应用来使用它。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菠萝-琪琪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值