Paxos 协议:多状态机的一致性解决方案

问题背景

正确理解二阶段提交(Two-Phase Commit) 的文章中, 笔者解析了二阶段提交协议是如何满足一个分布式原子性提交协议应该具有的性质。

二阶段提交协议(Two-Phase Commit)出现的本质原因是, 分布式系统中不同的结点有不同的功能, 不同功能背后对应的数据集不同, 不同功能又需要一定的协同性。

二阶段协议中, 一个比较严重的问题是, 如果遇到结点宕机, 必须等到所有结点恢复以后, 协议才能继续。 于是这里就引出了如何提高分布式系统可用性的问题

中心化架构带来的单点失败问题

正确理解二阶段提交(Two-Phase Commit) 中提到的跨行转账场景为例。 银行A, 银行B , 事务协调者 TC 都拥有并维护着各自的唯一的, 权威的账目数据集。 任何一个结点停止工作都会导致其他结点也无法继续工作。

所以我们希望提高分布式系统的可用性。

复制数据

很自然的方案时将核心数据复制多份,由多个结点维护, 万一有部分结点失效, 我们希望其他结点还能正常工作

数据一致性如何解决

只要存在多份数据,就一定会面临如何解决数据一致性的问题。 当客户通过请求修改了其中一份数据以后, 其他的数据该如何保持一致?

简化问题为多份状态机

任意的服务器结点本质上都是一个状态机。

  • 磁盘, 内存, CPU 缓存中的数据都属于状态
  • 通过指令, 状态机的状态发生变化
  • 用户可以通过请求触发状态机执行特定的指令,从而进一步触发状态转化

通过复制产生了多个位于不同结点的状态机

  • 每一个状态机副本必须接受到顺序相同的指令集
  • 如果每一个指令所导致的状态是确定的(非随机), 副本状态机在执行了相同的指令以后, 最终会达到相同的状态。

如何确保副本收到相同顺序的相同指令

首先可以指定一个特殊的副本结点作为主结点

将其他的结点作为这个主结点的备份

客户统一将请求都发送给当前的主结点

主结点负责:

  • 为接受到的客户指令确定唯一排序
  • 把具有唯一顺序的指令集发送给它的备份结点
  • 响应客户

如何应对主结点宕机

显而易见: 选出新的主结点

草稿方案

为每一个结点标注一个编号, 当主结点宕机时, 现存结点中编号最小的成为主结点

在主结点宕机后, 剩余结点需要相互通信, 判断哪些结点还存活

问题

  • 通信不可靠, 相互之间发送的数据可能发生丢失
    • 后果: 可能产生多个主结点
  • 通信可能存在时延
    • 后果: 可能产生多个主结点
  • 网络分区(部分结点相互可通信, 另外一部分结点相互可通信)
    • 后果: 可能产生多个主结点

灵感:半数以上结点的两个分区必有重合

要求: 至少要有超过半数以上的结点同意才能选出新的主结点

好处:多数结点所在的分区只能有一个, 如果存在两个以上的分区选出了两个主结点, 那么多个分区中, 一定存在重合的结点, 重合的结点可以发现选出了多个主结点,进而“拉响警报”
在这里插入图片描述

Paxos: 视图变化算法

理解 Paxos 的核心技巧是理解视图的概念

首先明确如下的概念

  • 整个系统由多个状态机副本结点组成
  • 多个结点构成一个结点集
  • 每个结点集都存在一个编号
  • 这个编号代表着结点集的共识, 每一个结点都知道这个值是多少。 可以将这个值理解为整个结果集的一种状态标志
    • 例子:
      • 我们可以定义一个结点集,里面包含 3 个结点(node A, node B ,node C), nodeA 是主结点
      • 进一步定义, 系统初始化后, 3 个结点的共识值为 x
      • 进一步定义, 只要某一个结点在过去的 1 分钟内, 没有完整地收到所有其余结点的心跳消息, 那么可以认为这个代表结点集共识的值被打破。 需要寻找新的共识值 y

由此引出视图的定义:

  • 一个视图(View) 由如下两个元素构成
    • 视图编号(View Number)
    • 结点集(set of nodes)
  • 提示: 请不要将视图的编号理解为结点集中主结点的 ID, 视图的编号代表的是结点集的共识, 当一个视图明确后, 确定唯一的主结点是非常简单的事情, 例如可以直接定义结点集中, IP 地址最小的结点为主结点。

按照视图的定义, 视图编号(View Number) + 结点集(set of nodes) 任意一个元素发生了变化, 这个视图也就变化了

当一个稳定的视图, 因为 视图编号(View Number) 或 结点集(set of nodes) 的变化而被破坏时 , 我们就需要一个视图变化算法, 帮我们确定, 一个视图“打破”后, 下一个视图应该是什么。

Paxos 算法即是这个视图变化算法的实现方式之一

Paxos 概览

首先粗略展示 Paxos 的大致流程:

  • 一个(或多个结点) 决定要成为 Leader
  • 想要成为 Leader 的结点, 首先提出一个候选值(Proposed Value), 以期待大家对这个候选值可以达成共识
  • Leader 联系所有的参与结点, 尝试收集到半数以上结点的同意回复
    • 参与结点的含义:
      • 可以是一个预先配置好的结点集
      • 也可以是前一个视图中的所有结点
  • 如果半数以上的人回复了同意, 成功的达成了共识 !

Paxos 协议原来是这么简单的一个算法吗 ?! 很遗憾, 并不是, 上面只是一个非常粗略的大致流程

  • 需要解决的问题:
    • 如果有两个或更多结点同时决定要成为 Leader 怎么办?
    • 如果出现了网络分区隔离, 导致产生了两个或更多 Leader 怎么办?
    • 如果决定要成为 Leader 的结点在“说服” 了一部分结点后,自己宕机了怎么办?
    • 如果决定要成为 Leader 的结点已经收集到了超过半数的同意意见
      • 还没来及公布结果, 然后自己宕机了
      • 向一部分结点送达了结果, 然后自己宕机了
    • 怎么办?

Paxos 详解

  • 算法总共分3个阶段
  • 如果算法执行到一半, 有结点宕机或者在发生响应等待超时, 算法可能需要重头执行
  • 每一个运行 Paxos 算法的视图中的任一结点维护一个状态(该状态由 4 个值构成):
    • n_a( n_accepted) 当前结点所认同的最大值 n ( 初始值为 -1 )
    • v_a(value_accepted ) 与 n_a 一同接收到的值, 代表该结点当前所认同的视图, 具体格式为一对值: { 视图编号, 结点集}
    • n_h (n_highest ) 所有已经收到的 Q1 类型的 message 中 n 的最大值 ( 初始值为 -1 )
    • done, 该值为 true 标识收到了某个 leader 发来的消息说多数结点共识已经达成, 现在可以使用新的视图编号值(初始值为 false

Paxos 第一阶段(Phase 1)

  • 一个结点(可能多于一个结点)决定要成为 Leader, 然后它就
    • 提出一个候选值 n
      • 这个 n 必须是唯一, 且大于所有该结点已经见过的候选值
      • 所以可以直接把已经见过候选值中最大值 n_h 作 +1 操作后, 再追加上该节点的 ID 标识 node_id 作为新提出的候选值
    • 向所有结点(包括该结点自己)发送 Q1(n) message
      • Q1(n) 标识消息类型为 Q1 , 消息中包含该节点新提出的候选值 n = n_known_max + 1 || node_id
  • 任意结点收到了 Q1(n) 的消息 ,且 n > n_h
    • n_h = n
    • 发送响应 R1(n_a,v_a)

Paxos 第二阶段(Phase 2)

  • 如果 Leader 收到了半数以上结点(包含它自己)回复的 R1(n,v) 响应消息

    • 如果一个或多个 R1(n,v)v 的值非空,
      • 找出其中 n 值最大的消息 R1(n_max, v)
      • 执行 v_a = R1(n_max, v).getV()
      • 执行 n_h = R1(n_max, v).getN()
    • 否则 Leader 就得自己产生一个 v_a
      • v_a = {旧的视图编号+1, 目前能正常通信的结点集}
    • 发送 Q2(v_a, n_h)消息给所有回复了 R1(n,v) 的结点
  • 如果任意结点收到了 Q2(n,v) 类型的消息 且消息中的 n >= n_h

    • 执行 n_h = n_a = n
    • 执行 v_a = v
    • 发送 R2() 类型的消息作为 Q2 消息的响应
      • R2() 消息中没有特别的内容, 单纯就是 Q2 消息的响应

Paxos 第三阶段 ( Phase 3)

  • 如果 leader 收到了半数以上结点回复的 R2() 消息
    • 发送 Q3() 消息给所有的参与结点
  • 如果任意结点收到了 Q3() 消息
    • 执行 done = true
      • 标识此时共识达成, 达成的共识视图是 v_a
      • 主结点可以规定为 v_a 中编号最小的那个结点

Paxos 通信超时应对方案

  • 所有的结点发送消息时, 都会设置一个等待超时时间。
  • 一旦发生了超时, 这个结点就宣布自己要成为 leader, 发起 Paxos 算法的第一阶段 Phase 1

Paxos 图解(一图胜千言)

先看 Paxos 在最理想的情形下是如何正确执行的

首先假设系统初始状态下有 5 个可以相互通信的,未达成共识的结点

在这里插入图片描述
然后 结点1 突然决定要站出来成为 Leader, 先自行起草一个共识编号 n = 01
在这里插入图片描述初始化一个 Q1(n) 类型的消息, 将自己起草的这个编号 01 填进去, 发给Q1(01)所有参与结点, 包括自己
在这里插入图片描述所有收到 Q1(n) 消息的结点, 由于发现 n = 01 , 大于自己记录的 n_h= -1 , 于是将 n_h 更新为收到的 01
在这里插入图片描述

然后向 Leader 结点回复 R1(n_a, v_a)

在这里插入图片描述由于 Leader 结点收到超过半数(2.5)以上的 4 个结点的 R1 响应, 它进入了 Paxos 第二阶段。

首先查看响应中是否有已经存在的共识内容 v , 结果发现 v 值都是空的

那 Leader 需要自行起草一个共识 v = { 视图编号 + 结点集 } = { 1, {0,1,2,3,4} }
在这里插入图片描述
Leader 现在将自己提出的共识内容 v = {1,{0,1,2,3,4}} 填到 Q2 消息中, 发送给所有参与结点
在这里插入图片描述

任意结点收到了 Q2(n,v) 类型的消息 且消息中的 n >= n_h

  • 执行 n_h = n_a = n
  • 执行 v_a = v
    在这里插入图片描述
    然后回复 R2 消息作为 Q2 响应
    在这里插入图片描述

Leader 收到了半数以上结点回复的 R2 消息, 进入 Paxos 第三阶段, 发送 Q3() 消息给所有的参与结点
在这里插入图片描述

  • 如果任意结点收到了 Q3() 消息
    • 执行 done = true
      • 标识此时共识达成, 达成的共识视图是 v_a
      • 主结点可以规定为 v_a 中编号最小的那个结点
        在这里插入图片描述
        至此 Paxos 协议执行完成, 所有结点都达成了共识内容{1,{0,1,2,3,4} } , 主结点就可以规定为编号最小的 结点0

Paxos 几个待思考的问题

上文的流程图只是展示了, 只有一个 Leader, 且通信过程中无超时, 无结点宕机的情形, Paxos 成功使所有结点达成了共识。

但是, 根据 Paxos 协议的超时应对方案, 每当有结点通信超时时, 它就会自行尝试成为 Leader, 所以 Paxos 协议还得确保有多个 Leader 执行协议的正确性。

多个 Leader 出现的情形

加入 Paxos 协议发生通信超时等问题后, 就有可能产生不止一个 Leader 在发起 Paxos 协议

根据 Paxos 第一阶段的协议, 每个 Leader 首先需要生成各自的 n, 生成方式为:

  • n_known_max 作 +1 操作后, 再追加上该节点的 ID 标识 node_id

由于 n 生成方式中, 追加了各自结点的 ID, 两个 Leader 生成的 n 肯定是不一样的,假设:

  • Leader 1 生成了 n = 01
  • Leader 2 生成了 n = 02

由于 Paxos 协议的第一阶段, Leader 只是将自己选出的 n 封装在 Q1 消息中,向全部已知结点发送。

而收到 R1 消息的结点会简单地根据 Q1(n) 中的 n 值与自己目前所见过的最大 n 的大小关系, 决定是否返回 R1

那我们可以假设, Leader 1 和 Leader 2 同时发送了 Q1, Leader 1 的消息 Q1 先到达了半数以上的结点, Leader 2 的消息 Q2 后到达了半数以上结点。

  • Tip: Leader 2 发送的 Q1 如果先到达了大部分结点, 这些结点就不会再向 Leader 1 回复 R1 了, 我们也就不用担心多 Leader 情形了

由于 Leader 2 提出的 n=02 比 Leader 1 提出的 n=01 要大, 那么收到 Q1 消息的结点, 会先向 Leader 1 回复 R1, 然后再向 Leader 2 回复 R1 , 这样两个 Leader 都能收集到半数以上的 R1 消息。 成功通过 Phase 1 。

紧接着, Leader 1 和 Leader 2 会根据收集到的 R1 信息, 决定是否要提出一个共识, 或者使用 R2 中已经存在的共识。 这里我们假设 Leader 1 和 Leader 2 收到的 R1 中 v 都为 null 。 那么他们会分别发送 Q2 给所有回复了 R1 的结点。

这之后可能出现以下的情形:

  • 情形1: Leader 1 没有收到半数以上结点的 R2 响应

    • 原因1 : 半数以上的结点都收到过了 Leader 2 的 Q1, 由于 Leader 2 中 Q1 中的 n 更大, 所以半数以上的结点都将自己维护的 n_h 更新为了 02 , 这样再收到来自 Leader 1 的 Q2 时, 不会再予以回复, 至此, Leader 1 也就失去了继续执行协议的能力, Paxos 协议运行到最终会获得统一的共识
    • 原因2: Leader 1 目前已经处于一个被隔离开的网络分区, 该分区中, 只有不到半数的结点可以相互通信。 至此, Leader 1 同样失去继续执行协议的能力。 Paxos 协议运行到最终会获得唯一的视图
  • 情形2: Leader 1 收到了半数以上结点的 R2 响应

    • Leader 1 就可能已经发送了 Q3 ! 注意, 收到了 Q3 的结点会直接将 Q3 中的共识 v 作为新的共识, 并标识 Paxos 执行已经完成。 如果 Leader 1 和 Leader 2 在一个协议执行期间先后发送了共识内容不同的 Q3, 显然会导致结点共识的混乱!
    • 但是, 仔细分析会发现, 如果 Leader 1 收到了半数以上的 R2, 那这些向 Leader 1 回复 R2 的结点, 在收到 Leader 1 的 Q2 前, 肯定没有收到过 Leader 2 的 Q1, 否则不会有人向 Leader 1 回复 Q1
    • 因此, Leader 2 如果收到了来自了半数以上的 R1, 这半数以上的结点中, 必然有一个结点曾经收到过 Leader 1 的 Q1 , 这个结点向 Leader 2 回复的 R1 就会是包含 Leader 1 挑选的共识 v
    • 这样 Leader 2 就能感知到 Leader 1 提出的共识值 v
    • 进而 Leader 2 就会直接使用 Leader 1 的共识值 v 来发送 Q2 ,而不是新创建一个共识值
    • 至此, 就能确保, 协议最终达成的共识是 Leader 1 提出的共识!

协议执行过程中发生结点宕机

考虑 Leader 在发送 Q2 的过程中宕机后的情形

  • 一些结点会在超时收不到任何消息后, 主动跳出了, 作为 Leader 发起 Paxos 协议的执行
  • 我们将宕机的 Leader 称为 Leader_old , 由于 Leader_old 还没有发送 Q3 , 所以不需要担心Leader_old 的突然宕机会导致 Paxos 协议达成的共识混乱
  • 我们将新出现的 Leader 称为 Leader_new, 如果 Leader_new 站出来以后, 提出的 n 值, 比 Leader_old 的 n值大, 那很好, 协议执行时, 结点都会响应 Leader_new
  • 如果 Leader_new 提出的 n 值, 比 Leader_old 的 n 值小, 那也不要紧, 收到过 Leader_Old 消息的结点都不会响应 Leader_new, 这样必然会发生消息超时, 会有新的结点站出来, 最终最会有一个结点提出的 n 值比 Leader_old 的 n 值大

考虑 Leader 在成功发送 Q2 之后宕机的情形

  • 如果 Leader 是向少于半数的结点发送完 Q2 后宕机的
    • 结果和两个 Leader 都成功进入了 Phase 2 的情形一样
  • 如果 Leader 是向多于半数的结点发送完 Q2 后宕机的
    • 结果还是和有两个 Leader 成功进入 Phase 2 的情形一样

考虑参与结点中, 有结点在收到了 Q2 ,并发送 R2 之后发生宕机

  • 则需考虑该结点是否会重新启动
  • 如果该结点会被重启
    • 它必须从硬盘中恢复出来它曾经维护的 v_an_a
    • 因为 Leader 可能在只向少数结点发送了 Q3 消息后就宕机了
    • 这个结点恰巧就是两个含半数以上结点的网络分区重合的唯一结点。
    • 新的 Leader 是隶属于另外一个网络分区中的结点, 它成功的收集了自己分区中的所有结点(假设恰好占到 1/2 的结点) + 这个重启结点的 R1 消息, 如果这个重启结点没有保存重启前的 v_an_a , 就会导致新的 Leader 所在分区达成一个和老 Leader 所在分区不一样的共识状态, 违背 Paxos 协议。

总结

最初的目标:

  • 在一个存在多副本状态机的系统中,即使有少数结点失效,系统也能继续运行
  • 在每一次发生结点失效以后, 执行视图变化算法 Paxos
  • 这样可以使得系统中的多数状态机达成新的共识状态
  • 这样, 系统中的每个结点都能有共同认可的主结点

注意 Paxos 并没有讨论, 如何在没有结点宕机时, 确保多个副本状态机的数据一致性。

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