学习笔记-分布式系统共识算法-paxos

70 阅读9分钟

本文是在学习文章《paxos made simple》时按自己的理解整理的笔记,仅作为记录供日后回顾。

1. 概述

Paxos 算法是基于消息传递1的、具有高度容错特性共识算法2

  • 拜占庭将军问题

    若干拜占庭将军各自率领一支军队围困一座城市,将各军队行动简化为进攻和撤退两种,由于各军队行动不一致会造成灾难性的后果,因此各个将军需要通过投票达成一致性策略。

    由于各将军所在的位置不同,他们之间只能通过一个信使将进攻还是撤退的信息传递给其他将军,这样每位将军就可以通过自己和其他将军的投票结果决定策略。

    现假设将军中出现了叛徒,他们倾向于向较为糟糕的策略投票,且会选择性的发送投票信息。如:现有 9 各将军,有 1 名叛徒,4 名将军选择进攻,另外 4 名选择撤退。叛徒故意给选择进攻的将军发送进攻消息,给选择撤退的将军发送撤退信息。这时 4 名进攻将军会根据投票情况选择进攻,4 名撤退将军会选择撤退,导致军队间的一致性被破坏。

基于消息传递通信模型的分布式系统会有:进程较慢、被杀死或重启,消息延迟、重复、或丢失。在最普通的 Paxos 场景(不考虑消息篡改即拜占庭错误问题)中,通过一个共识机制,可以保证上述问题不发生,使集群对某个值达成一致,保证系统的一致性。

2. 算法推导

2.1 问题提出与定义

  • 问题的提出

对于一个包含若干节点且可能发生节点宕机网络异常的分布式系统,快速对某个值达成一致且保证整个系统的一致性。

  • 共识算法

假定一个可以提议某个值 v 的提案 p,共识算法有以下安全性要求:

  1. 如果v没有被任意一个p提出,则v不能被选择
  2. 只有一个 v 被选择
  3. v被选择后,其他节点都可以学习被选择的v
  • 角色定义:Proposer、Acceptor、Learner

  • 通信

不同角色间通过消息传递相互通信。

每个角色的运行相互独立,单个角色可能因故障宕机或重新启动。一个 v 被选中后角色能记录相关信息,即使 v 被选中后全部节点故障重启,也能确定被选中的 v

消息的传递可以容忍高时延、丢失、重复,但不能出现数据篡改(拜占庭错误)

2.2 选择一个值

  • P1 的提出

对于一个存在若干节点的分布式系统,为了保证只有一个 v 被选择,我们需要规定:只有被一半以上的 Acceptor 接受的值才能被选择。

同时,为了保证 Proposer 提出的值能被有效选择,我们约定:

P1:Acceptor 必须接受它收到的第一个提案。

  • P2 的提出

对于约定 P1,可能会存在如下问题:

Pasted image 20231003002654.png

Propo1Propo2 几乎同时发布提案,提议的值分别为 v1v2。并且,Accep1Accep2 接受了 v1Accep3 接受了 v2

假设此时 Accep2 宕机,没有一个 v 被半数以上的 Acceptor 接受,当 Accep2 重启后,也不知道学习哪一个值。

对于这种情况,需要重新提出 p,这就意味着,每个 Proposer 会发布不止一个提案,一个 Acceptor 会接受不止一提案。因此我们需要给 p 分配一个唯一且有序的编号 n 进行标识,因此,一个提案可以表示为 p[n,v]

我们允许选择多个 p ,那我们就需要保证选择的每一个 p 都有相同的 v 来保证一致性。为了实现这点,我们约定:

P2:如果选择了值为 v0 的提案 p[i,v0],那么选择的每个编号大于 i 的提案 p 的值也是 v0

  • P2aP2^a 的提出

考虑到提案的编号是有序的,在 P2 约束下我们可以保证只有一个值被选中。我们知道被大多数 Acceptor 接受的 p 才会被选择,因此对于约束 P2 可以理解为:

P2aP2^a:如果选择了值为 v0 的提案 p[i,v0],那么 Acceptor接受的每个编号大于 i 的提案 p 的值也是 v0

  • P2bP2^b 的提出

但对于约束 P2aP2^a,可能会存在如下问题:

Pasted image 20231004180320.png

如果 Accep3 刚从宕机状态恢复且之前没有接受过任何提案,此时,Propo2 发布了一个编号跟高但是值不同的提议,由于约束 P1Accep3 必须接受该值,而这违反了 P2aP2^a

因此我们约定:

P2bP2^b:如果选择了值为 v0 的提案 p[i,v0],那么 Proposer 发布的每个编号大于 i 的提案 p 的值也是 v0

由于提案必须有 Proposer 发布后才能被 Acceptor 接受,因此 P2bP2^b 包含 P2aP2^a

  • P2cP2^c 的提出(满足 P2bP2^b

为了保证 P2bP2^b 能被满足,我们约定:

P2cP2^c:对于任意的 nv,如果提案 p[n,v] 被提出,那么存在一个半数以上的 Acceptor 集合 S,至少满足以下两个条件中的一个: (a)S 中没有一个 Acceptor 接受过编号比 n 更小的提案。 (b)S 中的 Accetper 接受过编号最大的提案的值也为 v

通过保证 P2cP2^c 的不变性保证 P2bP2^b 的不变性。

而为了保证 P2cP2^c 的不变性,对于每个想要发布编号为 n 的 p 的 Proposer,必须满足:

    1. Proposer 先学习编号小于 n 中编号最大的那个提案(if any)。3
    1. Acceptor 不再接受任何编号小于 n 的提案,保证上述学习提案的有效性。

如 :若想发布编号为 5 的 p,学习的提案编号为 3,如果期间有另一个 Proposer 发布了编号为 4 的 p,可能导致约定 P2cP2^c 被打破,因此需要有对 Acceptor 的承诺。

3. 算法流程

对于上述推导流程推导出的约定 P1P2cP2^c,我们可以获得下述提案发布的算法:

3.1 Proposer 提出提案

  • 学习阶段:prepare request

Proposer 选择一个编号为 n 的新提案 p[n,?],向半数以上的一个 Acceptor 集合 S 发送请求,要求其响应:

    1. 承诺不再接受编号小于 n 的提案
    1. 已接受的编号小于 n 中编号最大的提案(if any)3

对于这样的请求,称为准备请求(prepare request)

  • 接受阶段:accept request#

如果 Proposer 收到了大多数(半数以上)Acceptor 的响应,那么:

    1. 如果 Proposer 收到了 Acceptor 响应的提案,那么 Proposer 发布 p[n,v],其中 v 是 Proposer 收到的响应提案中编号最大的提案的值。
    1. 如果 Proposer 未收到 Acceptor 响应的提案,那么 Proposer 发布 p[n,v],其中 v 由 Proposer 自己生成

对于这样的请求,称为接受请求(accept request)

tips: 学习阶段和接受阶段的 Acceptor 集合 S 不要求是同一个

3.2 Acceptor 接受提案

对于 Accetper,它可以接收来自 Proposer 的两种请求:prepare requestaccept request。为了保证安全,Acceptor 可以忽略任何请求。

因此对于 Acceptor,我们只需规定何时允许响应请求:

    1. Acceptor 始终被允许响应 准备请求(prepare request)
    1. Acceptor 可以响应接收请求, 接受提案,前提是它没有承诺过不这么做。

对于第二点,转化为约定:

P1aP1^a:Acceptor 可以接受编号为 n 的提案,当且仅当它未响应编号大于 n 的准备请求(accept request)

可以观察到 P1aP1^a 包含 P1

通过上述规定,我们可以知道,对于 Acceptor,只需记录两个信息4

    1. 接受过的编号最高的提案5
    1. 响应过的编号最高的准备请求的编号6

对于这两个信息,即使 Acceptor 中断重启也必须保证正确记录。

3.3 两阶段提交

Pasted image 20231004183455.png

Proposer 可以提出多个提案,也可随时放弃执行中的提案。例如如果由 Proposer 尝试发布编号更高的提案,那么此时最好放弃当前的提案。同时,如果 Acceptor 收到了编号更高的准备请求而准备忽略当前请求时,可以通知 Proposer 放弃当前提案以获得一定的性能优化。

3.4 Learner 学习提案

当 Acceptor 接受提案之后,Acceptor 无法立刻得知接受的值是否被选择,因此当 Acceptor 接受提案后,会变为 Learner 发现被选择的值。

目前学习被选中的值的方式有以下三种:

方案一方案二方案三
内容每个 Acceptor 接受一个提案后就给所有 Learner 发送提案,发送数量为接收者数量与学习者数量的乘积Acceptor 将接受的提案发给一个主 Learner,由主 Learner 将提案发给其他 LearnerAcceptor 将接受的提案发给一个主 Learner 集合,由主 Learner 集合将提案发给其他 Learner
优点Learner 能快速获取 value通信次数减少 (M+N)可靠性高
缺点通信次数太多(M*N)单点故障(主 Learner 故障)通信网络复杂

3.5 Paxos 死循环

对于以下场景:

    1. 假设两个提议者 P1P2
    1. P1 发布提案 p[n1,v],Acceptors 响应 Prepare_Req (n1)
    1. P2 发布提案 p[n2,v],Acceptors 响应 Prepare_Req (n2),忽略 Accept_Req(n1,v)
    1. P1 发布提案 p[n3,v],Acceptors 响应 Prepare_Req (n3),忽略 Accept_Req(n2,v)
    1. ...

可以看到由于 P1P2 不断发布提案导致陷入死循环。为了解决这个问题,需要选择一个主 Proposer,只有主 Proposer 才能提出提案。

Footnotes

  1. 消息传递是在并发计算、并行计算、面向对象编程、进程间通信中使用的一种通信方式,这种通信范式由一个传信者,将消息发送给一个或多个收信者,常见的消息通信系统有 RPC、CORBA、SOAP。

  2. 根据 wiki,共识算法与一致性算法是不一样的概念,paxos 数据与执行算法

  3. {m(mSiS:i<niS:im)}\{m|(m\in S \wedge \forall i \in S:i<n \wedge \forall i \in S: i \leq m)\} 2

  4. 对应 [[#2.1 问题提出与定义]]通信小节中角色需要记录的相关信息

  5. 依据为 [[#3.1 Proposer 提出提案]] 准备请求第二个响应要求

  6. 依据为约定 P1aP1^a