共识算法Raft

介绍

共识算法

共识算法就是保证一个集群的多台机器协同工作,在遇到请求时,数据能够保持一致。即使遇到机器宕机,整个系统仍然能够对外保持服务的可用性。
对于一个分布式系统而言,可扩展性 是非常重要的,因此 共识算法 需要具备 在 集群 中 不停服务增减机器 的能力。

Raft 算法背景

在学术理论界,分布式一致性算法的代表还是 Paxos,但是 理解起来很费劲。因此 Paxos 的可理解性 & 工程落地性的门槛很高。
斯坦福学者 花了很多时间理解 Paxos,于是他们研究出来 Raft。相比Paxos, Raft最大的特性就是 易于理解。

Raft算法

Raft算法 为了 达到 易于理解的目标,主要做了 两方面的事情:

  • 问题分解
    把 共识算法 分解为 三个子问题: 领导选举日志复制安全性

  • 转态简化
    对算法做出一些限制,减少 状态数量 和 可能产生的变动。
    是一台服务器(节点实例)只在 三个状态 之间进行切换,并且服务器之间的通信 仅通过 两类 RPC来完成。
    这是的 Raft 相对于其他 算法 非常的简洁。

Raft算法 在 功能 和 性能 方面 完全可以和 其他公式算法 等同,因此Raft出现以后,越来越多的 软件 在 实现
共识算法 时 都会优选选择 Raft算法。

复制状态机

这是一个理解 共识算法 非常重要的 高度抽象的 一个 实现模型。

  • 复制状态机理论: 相同的 初始状态 + 相同的 输入 = 相同的 结束状态
    也就说在 集群中的 多个节点上,从相同的 初始化状态 开始, 执行相同的 一串命令, 产生的 最终状态 是相同的。

在Raft中, Leader将客户端的 请求 封装到一个个 log entry 中, 将这些 log entry 复制到 所有的 Follower节点,
然后 大家 按照相同的 顺序 应用 log entry, 根据 复制状态机理论, 大家的 结束状态 肯定就是 一致的。

可以说 使用 共识算法 就是为了 实现 复制状态机,从而 始终保持 分布式中 各节点 的状态一致, 进而 实现 高可用。

状态简化

状态

在任何时刻,每个节点 都处于 Leader、Follower、Candidate 这三个状态之一,

  • Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • Candidate:Leader选举过程中的临时角色。

Raft要求系统在 任意时刻 最多只有一个Leader,正常工作 期间 只有Leader和Followers。
在这里插入图片描述

相比Paxos,这一点就极大简化了 算法的实现, 因为 Raft只需考虑 状态的切换, 而不用像Paxos那样 考虑状态之间的 共存互相影响

任期(term)

Raft算法将时间分为一个个的任期(term),每个任期 Term 都有自己的编号 TermId,该编号全局唯一且单调递增
每个 任期 的开始 都是 领导选举。

  • 如果选举成功,
    则进入 维持管理 任期 Term 阶段,此时 leader 负责接收客户端请求并,负责复制日志。Leader 和所有 follower 都保持通信,如果 follower 发现通信超时,TermId 递增并发起新的选举。
  • 如果选举失败,
    则进入新的任期,TermId 递增,然后重新发起选举直到成功。

举个例子,参考下图,Term N 选举成功,Term N+1 和 Term N+2 选举失败,Term N+3 重新选举成功。
在这里插入图片描述

通信

Raft算法中的服务器 节点使用 RPC进行通信, 并且 Raft中 只有 两种 主要的 RPC:

  • RequestVote RPC(请求投票): 由candidate发起, 在 选举期间发起。
  • AppendEntries RPC(追加条目):由 Leader发起, 用来 复制日志 和 心跳机制。

通信过程中的 几个状态变化:

  • 服务器 之间通信的时候, 会 交换 当前任期号, 如果一个 服务器上的 当前任期号 小于 其他服务器的,
    那么 该服务器 就会将 自己的 任期号 更新为 较大的 那个值。
  • candidate 或者 Leader 发现自己的 任期号 过期了, 会立即回到 follower 状态。
  • 一个节点 收到一个 任期号过期的请求, 他会直接 拒收 这个 请求。

问题分解

领导者 选举

Raft内部有一种 心跳机制, 如果存在 Leader, 那么他就会 周期性地 向所有的 Follower 发送心跳,
依次来维持自己的 领导地位。

选举流程

如果 某个 Follower 一段时间 没有收到心跳,那么他就会 任务 Leader 出问题了,然后就 进行选举。

  1. 该 Follower 先 增加 自己的 当前任期号,
  2. 该 Follower 转为 Candidate 状态。
  3. 该 Follower 投票给自己
    (也就是说 先发现 Leader 有问题的 Follower 很大概率 会成为下一个任期的Leader, 也可能有 多个Follower 同时发现,这里 以一个为代表 介绍流程)
  4. 并行地 向集群的 其他节点 发送 投票请求(RequestVote RPC)。
  5. 节点间投票,每一轮选举中,每个节点 只可以 投一票。
选举结果
  • 该 节点 获得 超过半数的选票 赢得了次轮 选举, 成为Leader后 开始 心跳机制 维持地位。
  • 该 节点 收到 其他 赢得了 选举的 节点 的 心跳机制 后, 如果 新Leader的 任期号 大于等于 该Follower 的 任期号, 那么 该节点 就从 Candidate ----> Follower 状态。
  • 一段时间后,没有任何的 获胜者, 那么 每个 Candidate 都在 自己的 随机选举超时时间(一般为 150~300ms)后,增加 自己的任期号, 开始新一轮的 投票。
    也就是说,对于 单个 Candidate 而言,只要在 在 自己的 随机选举超时时间 内 没有选出 Leader 就会进入 下一轮的 选举。
    (如果投票的 结果 太过于分散, 任何一个节点的得票 都没有 超过半数, 那么 该轮选举 就失败了)
选举信息
  • 请求投票 的 RPC Request
type RequestVoteRequest struct {
    term            int     // 自己的当前任期号
    candidate       int     // 自己的ID
    lastLoginIndex  int     // 自己最后一个日志号
    lastLogTerm     int     // 自己最后一个日志的任期
}
  • 请求投票 的 RPC Response
type RequestVoteResponse struct {
    term            int     // 自己的当前任期号
    voteGranted     bool    // 自己是否给这个candidate投票
}

对于 没有成为 candidate的Follower 节点,对于同一任期, 会按照 先来先得 的原则 投出自己一票,
即 节点A 请求 节点B 投票, 如果 节点B通过自己的判断 投给了 节点A, 那么节点B 就不能再投给其他的节点了。
这也是为了 奖励那些 率先 发现 Leader 出问题的 Follower,让他们有更大的概率 成为下一届的 Leader。这就是所谓的 新发优势

至于 为什么 请求投票 的 RPC Request 中有 日志的信息, 这个 放到 安全性的子问题 中 进行说明。

日志复制(最核心)

客户端 寻找 Leader

Leader 被选举出来后, 开始为 客户端 提供服务, 那 客户端 是怎么知道 哪个节点是 Leader?

  • 情况1: 客户端 请求 的 节点 正好就是 Leader
  • 情况2: 客户端 请求 的 节点 是 Follower,该Follower通过 心跳机制 可以得知 Leader 的ID, 然后 告知 客户端 该找谁。
  • 情况3: 客户端 请求 的 节点 宕机了, 那么 客户端 再去找另一个 节点。
日志

Leader 收到 客户端 的请求, 会将该 请求 以 指令的形式 追加一条 新的条目 到 日志中。
每个 日志 中 需要具有 3 个信息:

  • 状态机的指令
  • Leader的任期号
  • 日志号(日志索引)

生成日志之后,Leader 并行地 发送 AppendEntries RPC 给 众多 Follower,
让他们复制 该条目,当 该条目 超过半数 的Follower 复制后,
Leader就可以 在本地 执行 该指令,并把结果 返回给客户端。
我们把 本地执行指令 称作 提交

注意:日志复制 超过半数 就 百分百提交哪?
当然不是,因为 Follower复制完成,到 通知Leader,再到 Leader完成提交, 是需要时间的,
这个时间内 Leader 如果宕机了,就 无法进行 提交了。

异常情况

在日志复制过程中, Leader 和 Follower 随时都有 宕机 或 缓慢 的可能,
Raft 必须要在 有宕机的情况下 继续支持 日志复制,并且保证每个副本的 日志顺序一致。

  • case1:如果有Follower 因为 某些原因 没有给Leader响应, 那么Leader会不断的 重发 追加条目的请求(AppendEntries RPC),即使 Leader已经回复了 客户端
  • case2:如果有Follower崩溃后恢复,这是 Raft追加条目的 一致性检查 生效,保证 Follower 能按顺序 恢复 崩溃后的 缺失的 日志。

一致性检查:
Leader在每一个发往 Follower 的追加条目的RPC中,会放入前一个日志条目的索引位置任期号,如果 Follower 在他的日志中 找不到前一个 日志,那么他就会 拒绝 此日志, Leader收到 Follower 的 拒绝后, 会发送前一个日志条目,从而 逐渐向前 定位到 Follower 第一个缺失的 日志。

  • case3:如果Leader崩溃,那么奔溃的Leader可能已经复制了 日志到部分 Follower 但是 还没有提交,
    而被选出的 新Leader又可能不具备 这些日志,这样就有 部分 Follower 中的日志 和 新Leader 的日志不相同,
    在这种情况下,Leader 通过 强制 Follower 复制他的日志 来解决 不一致的 问题。
    这意味着 Follower 中和 新Leader 冲突的日志 会被覆盖,因为没有提交,所以不违背 外部一致性。
日志复制信息
  • 追加日志 的 RPC Request
type AppendEntriesRequest struct {
    term            int     // 自己的当前任期号
    leaderId        bool    // 用户告诉 Follower 我是 leader
    entries         []byte  // 当前 日志体

    // 以下两个属性 用来 一致性检查的
    prevLogIndex    int     // 前一个日志的 日志号
    prevLogTerm     int     // 前一个日志的 任期号

    // Follower根据该字段,就可以把 自己 复制未提交的 日志 设为 已提交 状态
    // 对于那些 追赶Leader日志进度 的 Follower来说,leaderCommit 大于自己最后一个日志,这时它的所有日志都是可以提交的。
    leaderCommit    int     // leader的已提交 日志号
}
  • 追加日志 的 RPC Response
type AppendEntriesResponse struct {
    term           int     // 自己的当前任期号
    success        bool    // 是否同意追加该条目, 如果 包含前一个日志 则为 true

安全性

领导选举 和 日志复制 这两个子问题,实际上 已经涵盖了 共识算法 的全过程,
但是,这两点 还不能保证 每一个状态机 会按照相同的 顺序 执行相同的 命令。

这里重点 强调一下 “顺序”, 因为 日志中的 命令应用到 状态机的 顺序 是一定不能 颠倒的。
但是 很多公式算法 为了 提高效率,会允许 日志 乱序复制到 Follower 节点上,如下图,
造成 非常多的 边界情况 需要处理。
在这里插入图片描述

Raft是一个非常追求 易理解的 共识算法,所以 Raft为了 简化设计,避免对这些 边界情况的 复杂处理,
日志复制阶段,就保证了 日志是 有序 且 无空洞的。

日志复制阶段 对于 日志顺序 的保证 是基于 Leader 正常工作,如果 Leader 出现宕机,
他的后几个日志的 状态 就有可能 出现不正常,这时, 新Leader 是否 具备这些 不正常的 日志,
以及 怎么处理这些 日志,就显得 尤为重要 了。
这也是 Raft 为数不多的几个 需要进行 特殊处理的 边界情况。
所以,安全性 这个 子问题 主要目的就是: 定义几个 规则来 完善Raft算法,使得在各种 边界情况 都不出错。即 安全性 是 领导选举 和 日志复制 的 附加规则,即补丁 规则

我们讨论 分 4种 情况:

case1 ==》 Leader宕机处理:选举限制

如果一个 Follower 落后 Leader 几条日志, 但没有 漏一整个 任期,
那么他在下次选举中 是有可能 当选 新Leader 的, 因此 他在当选新Leader后 就永远也无法 补上之前缺失的那部分日志了,
从而导致 状态机 之间的 不一致。

所以 需要对 领导的选举 增加一个限制: 被选举出来的Leader一定包含之前 各个任期 的所有 被提交 的日志条目。

实现 该限制的 机制原理 是 通过 请求投票 的 RPC Request

type RequestVoteRequest struct {
    term            int     // 自己的当前任期号
    candidate       int     // 自己的ID
    lastLoginIndex  int     // 自己最后一个日志号
    lastLogTerm     int     // 自己最后一个日志的任期
}

“新” 在这里的定义:

  1. 两份日志,如果 任期号 不同,那么 任期号 大的 日志 更 “新”;
  2. 两份日志,如果 任期号 相同,那么 日志号 大的 日志 更 “新”;

如果 请求投票的 候选节点A, 向 候选节点B 发出 请求投票 的日志,
候选节点B 通过比对 他和 候选节点A 谁更 新, 如果发现 候选节点A 还没有自己新,
候选节点B 会果断拒绝 候选节点A 的拉票,拒绝给 候选节点A 投票。
上述行为 进而保证了 上面 增加的 限制。

case2(核心、优秀) ==》 Leader宕机处理:新Leader是否 提交之前 任期的 日志条目

一旦 当前任期的 某个日志条目 已经存储到 过半 的服务器节点上 时, Leader 就知道 该日志条目 可以被 提交 了。

  • 单点提交
    目前讨论的 提交 都是一个 单点状态(只leader节点),而非 集群状态
    Leader 收到 超过半数节点 的复制成功反馈后 就可以 提交。但这时候 对 Follower 而言 节点虽然 复制到了 日志,
    但还没有进行 提交, 因此 提交 这个状态并没有 构成 大多数的。

  • Follower 是怎么知道 自己何时可以 提交 ?
    日志复制 中说过 追加日志 AppendEntries RPC 有 leaderCommit 这么一个参数,
    通过这个参数 Follower 就可以知道Leader提交了哪个日志,
    进而 Follower 自己也可以 提交 这个日志。

那么按照上面的说法, 当前日志的 最快提交 也得 等到 下个日志 的发送时才行,
其实不然,心跳机制 也是 追加日志 AppendEntries RPC,只不过 没有 日志体,
但 仍然可以 传递 leaderCommit 这个参数,从而 告知 Follower 当前日志 是否可以 提交。

  • 单点提交 的 隐患
    Leader 提交 和 Follower 提交 必然会 相隔一段时间,那么 Leader 提交 后直接返回 客户端,
    在 通知 Follower 提交之前,也就是 一个 心跳的时间之内,Leader 宕机了,就有可能出现
    返回 client(客户端)成功, 但是 事物提交 却没有在 集群中 保留下来。
    这就是 单点提交 的 容易出现的问题, 如果非要 避免这个问题, 可以设置一个 集群提交 的 概念,

实际上 对于多少系统需求而言 Leader单点提交 后 就返回 客户端,已经是安全的了,
并没有 等待 集群提交 的必要。

  • 新leader 对 老leader任期内的 日志 的 处理
    如果 老Leader在 某个日志条目 提交之前 宕机了, 新leader会试图 完成该日志条目 的 复制
    注意:新leader是 试图 复制 而非 提交, 新leader 提交 是危险的,但是 复制 是安全的。
    Raft 永远不会 通过 计算 副本的数量(已复制日志的节点数) 的方式来 提交 **前任期** 的 日志。
    这是为了防止 集群中 已提交的日志 被 覆盖掉。
    Raft 只有自己任期内的 日志 才会通过 计算 副本的数量(已复制日志的节点数) 的方式 来提交。

那么 新leader 复制的 老日志 何时 提交 哪? 等新leader在他的任期内 产生一个新日志,
在这个 新日志 提交时, 老日志 也就可以 提交了,这样 老日志 就不会被 覆盖 了。
注意:这里是 新日志 的 提交 而非 复制。

case3 ==》 Follower和Candidate宕机处理

Follower 和 Candidate 宕机后的 处理方式比其 Leader 较为 简单,
并且 两者的处理方式 是相同的, 如果宕机了,那么 后序发给他们的 RequestVote 和 AppendEntries RPC 都会失败,
Raft 通过 无限的重试 来处理 这种失败, 如果 宕机的 节点 重启了,那么 RPC 就会成功的 完成。

如果 一个节点 收到一个 RPC, 且完成相应操作,但是 没来及 响应给 Leader 完成信息 就宕机了,
那么 Leader 还会 再发送一次 和上次同样的 RPC。

case4 ==》 时间 和 可用性 限制

raft算法 整体 不依赖 客观时间, 也就是说, 哪怕因为 网络 or 其他因素,
造成 后发的RPC先到, 也不会 影响 raft的正确性。

只要整个 系统 满足 下面的 时间要求, Raft就可以选举出 并 维持一个稳定的 leader:

广播时间(一次RPC网络来回时间) << 选举超时时间 << 平均故障时间

如果 广播时间 大于 选举超时时间,那么 将永远选不出 Leader。

广播时间 和 平均故障时间 是由 系统的硬件 决定的, 而 选举超时时间 则可以 手动设定,
Raft的RPC需要将 接受的信息落盘,用时取决于存储技术,所以 一般 广播时间为 0.5ms ~ 20ms,
因此 选举超时时间 可能需要在 10ms ~ 500ms 之间。
服务器的平均故障间隔时间 都在 几个月 甚至更长 的时间。

集群成员变更

在实际生产过程中,很多情况下 都需要对 集群的配置进行调整,
比如:集群扩容、对故障机器进行下线、调整机器的高可用。
这时需要对 Raft的 配置文件进行改变,改变的过程 可能会影响Raft的正常运行,
当然 我们可以把 集群停掉 在执行变更,但这 就要 停止对外的服务,并且有 手动操作的 风险。
所以Raft设计了 集群成员变更 的功能,来 自动、无需停止服务的 集群成员变更。

脑裂问题

对于 分布式系统而言,是不可能 在所有机器上 同一时间 完成变更,这点是分布式 天然的限制
所以变更 必然会持续一段时间,这就 造成 集群成员变更 最大的难点——脑裂问题
即 同一期任内 出现 两个 Leader 节点。

  • 脑裂问题:
    3节点 扩展到 5节点 集群的 举例说明说明,一开始 有 S1、2、3 三个节点,
    运行一段时间后,加入 S4、5 节点,由于 配置的改变需要一段时间,
    假设 S3 率先使用 新配置,此时S1、2是老配置,S3、4、5 是新配置。
    此时leader宕机,开始进行选举:
    S1 自己一票,加上S2投自己一票,由于老配置,所以 2/3 就满足大多数票,
    所以S1当选为 Leader。
    与此同时,S3自已一票,S4、5 投自己两票,由于新配置,所以3/5就满足大多数票,
    所以S3也可以当选为Leader。
    此时,同一个任期,有两个 Leader, 出现了 脑裂问题。

两阶段

为了解决 脑裂问题, Raft的新配置的使用 采用一种 两阶段 的方法。

  • 第一阶段
    leader发起C(new,old) ,使整个集群进入 新旧配置共存的状态,称之为 联合一致状态,
    此时leader的选举 要在 新、旧 两个配置中 都达到 大多数 才可以。

借用脑裂问题的 例子说明:
如果S3要当选为 leader,那么 在老配置节点中,
要 至少有 两个节点 投票自己,即S1、2 都投票给自己,满足 旧配置 的 大多数。
在 新配置中,要 至少有 三个节点 投票给自己,即S3自己一票,S4、5也投自己,
满足 新配置 的 大多数。

  • 第二阶段
    leader发起C(new) ,使整个集群 进入 新配置 状态,此时 只要在 新配置下 达到大多数 就可以当选为 leader。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值