分布式算法—Raft

146 阅读8分钟

Raft 动画选举过程

Raft 动画

Raft 介绍

Raft 是一种用来管理复制日志的算法。在分布式系统中有一种常见复制状态机的抽象,就是把具有一定顺序的一系列 action 抽象成一条日志(log),每个 action 都是日志中的一个条目(Entry)。如果要使每个节点的服务状态相同,则要把日志中的所有 entry 按照记录顺序执行一遍。所以复制状态机的核心问题就变成了让每个节点都具有相同的日志的问题,也就是把日志复制到每个节点上的问题。这个问题也被成为复制日志问题。

Raft 就是用来实现复制日志的一种算法,该算法大体执行流程为:

  • 生成一条日志
  • 把这条日志复制到所有节点上
  • 把日志的 entry 应用到状态机上

Raft 算法的组成

Raft 算法的所有节点都会有一个节点作为领导者(leader),其他非 leader 的节点被称为跟随着(follower)。leader 主要负责接收客户端的请求,根据请求生成日志,把日志复制到所有节点上,并且判断是否适合把日志应用到状态机中,这个过程称作复制(replication)过程。

如果 leader 发生宕机等异常情况,其他节点需要成为新的 leader,继续履行 leader 的职责,这个过程称作选举(election)过程。

选举后,还需要处理异常带来的各种影响,也就是进行异常处理。

总的来说,raft 算法可以分解为复制、选举、异常处理三个部分。这三个部分节点都是通过 rpc 来完成节点间通信的。

复制过程

复制过程大概可以分为如下三个步骤:

  1. 当 leader 收到客户端的请求后,会将这个 entry 按照追加到末尾(append)的方式记录到日志中。日志中每个 entry 都会有一个 index(索引),没追加一个 entry,index 加 1。
  2. leader 完成 append 操作后,会并行向所有的 follower 发起 AppendEntries RPC 请求同步日志给 follower。follower 收到请求后,将请求中的 entry 追加到自己的本地日志中,并回复 leader 成功。
  3. leader 收到大多数 follower 的成功回复后,这个 entry 就被认为是已提交状态。然后将这个 entry 应用到状态机中,并回复客户端结果。对于没有回复的 follower,会不断重试,直到调用成功。
    • 此时,follower 只是将这个 entry 添加到本地日志中,并没有应用到状态机。raft 会在两种时机告诉 follower entry 的提交状态。
    • leader 处理下一个请求时,AppendEntries 请求会带上已提交状态 entry 的 index,follower 讲这个 entry 应用到状态机中。
    • 如果暂时没有客户端请求,leader 会将已提交状态 entry 的 index 信息伴随心跳发送给所有 follower

这样的复制过程有一个特性:即使少数节点变慢或者网络拥堵,也不会导致这个过程变慢。

选举过程

选举的基本条件

发生选举的条件是:在一定时间内,没有收到 leader 的日志复制请求,包括心跳请求,即发生超时(timeout)。

如果选举的条件满足了,则节点就会进入 candidate 状态,开始执行选举过程:

  • candidate 会给其他所有节点发送投票请求,要求其他节点同意自己成为新的 leader
  • 收到投票请求的 follower ,会检查 candidate 是否值得自己的投票:candidate 的 index 要比自己大。
  • 满足条件,回复同意,否则不同意,如果 candidate 得到大多数 follower 的同意的话,就会成为新的 leader

任期

上面讲的是基本的选举过程,实际的选举过程还要处理下面两个问题:

  1. 所有 follower 都发现 leader 宕机,都转变为 candidate。多个 candidate 抢夺 follower ,导致每个 candidate 都不能形成大多数。为了选出新的 leader ,需要重新选举,并区分新旧选举的请求。
  2. 发生网络分区,恢复后,集群中可能出现两个 leader 也就是脑裂问题。需要区分新旧两个 leader ,并且阻止旧的 leader 参与集群活动。

Raft 采用任期(term)来解决上面的两个问题,每个节点都用一个整型数字来保存任期,每次开始新的选举,任期都加 1。

问题一的解决

任期如何解决问题一的呢?多个 candidate 抢夺 follower ,导致每个 candidate 都不能形成大多数。为了选出新的 leader ,需要重新选举。这时候节点 A 的任期加 1 ,重新开始一次选举,各节点都会无条件优先接受更大的任期的请求,所以节点 A 这次会得到大多数节点的同意,成为新的 leader。

但是存在一种特殊的情况,两个节点同时选举,都没有达到大多数同意,任期加 1 后,还是同时开始,又同时失败。这样就会一直反复进行下去,没有休止。raft 用了一种很简单的方法解决了这个问题,就是在选举失败后,开始新的选举前,随机等待一段时间(这种方法成为随机回退)。这样再次同时选举的可能性就大大降低了。

问题二的解决

leader 的任期信息会包含在所有的请求(复制请求和心跳请求)中,其他节点收到请求,如果请求中的任期比自己的大,则用请求中的任期更新自己的任期,在选举结束后,所有节点的任期最终都会统一为 leader 的任期。

当发生网络分区时,发生脑裂现象,旧的 leader 仍然在运行,但是它给其他节点发送请求不会形成大多数,因为大多数节点都具有一个更大的任期。一旦新的 leader 发送请求给旧的 leader ,旧的 leader 就会发现有更大的任期存在,它会主动转表为 follower ,并且更新自己的任期为新的 leader 的任期。

完整的选举过程

  1. 节点启动时处于 follower 状态。

  2. 该节点在一段时间内没有收到任何请求,则发生超时,转变为 candidate。

  3. candidate 增加自己的任期,开始新的选举,向所有节点发出投票请求。candidate 发出投票请求后,会有三种情况:

    a. 没有得到大多数节点的同意,本次选举超时,开始新的选举。

    b. 得到大多数节点的同意,成为新的 leader。

    c. 收到其他节点请求,若任期相同,则说明本次选举已经产生 leader,则回退到 follower 状态。若包含更大的任期,也回退到 follower 状态

  4. 在 leader 收到的请求中包含更大的任期,leader 转变为 follower 状态。

异常处理

在上面的选举过程中,虽然集群从 leader 宕机和网络分区中恢复过来。但是新选出的 leader 与之前 leader 可能会存在数据不一致的问题,本小节主要来看看这种不一致的问题以及处理。

不一致的产生

  • 在 leader 成功写入 entry 还未同步到其他节点之时又发生分区,这时选举出新的 leader ,新 leader 不存在旧 leader 未提交的数据。
  • 在经历过几次分区以后,可能导致新的 leader 的数据比非 leader 的数据少,在最后一轮选举时,可能数据较多的节点都处于分区状态,可选举的节点都存在数据未完全同步问题。

一致性检查

Raft 算法会强制要求所有的 follower 保持与 leader 一致,不一致的部分要丢弃,替换为 leader 响应的部分。新的 leader 上所有未提交的 entry 保留,其他节点上未提交的 entry 丢弃。

当 leader 发送 AppedEntries RPC 发送一个 entry 时,会包含新的 entry 的前一个 entry 及任期,如果 follower 发现自己的日志中没有对应的 entry 及任期,则会拒绝这个请求。leader 收到请求失败后,在发送往前一个 entry ,直到 RPC 请求成功为止。这个时候说明 leader 和 follower 的日志达到一致的状态,再从这个 entry 开始往后同步到 follower

不提交旧的 leader 的 entry

leader 会保留节点未提交的 entry ,但是不会主动提交。有新的 entry 继续追加,在新的 entry 达到提交状态时,会自动提交前面未提交的 entry。

与 Paxos 算法差异点

PaxosRaft
值确认特性大多数同意大多数同意
数据一致性允许空洞情况,允许其他节点的值比 leader 节点新,会通过重试填补不允许空洞情况。以 leader 数据为主,未提交 entry 会被丢弃
脑裂处理共识算法(提议ID),保证脑裂情况下正常可用,承受数据空洞通过任期保证脑裂情况下可用。分区恢复后,已 leader 数据为准,可能会有数据丢失
理解难度简化 Paxos ,更易理解
leader 宕机新 leader 选出前拒绝请求新 leader 选出前拒绝请求