Paxos 协议

可用性与一致性

为了向用户提供更好的服务体验,现代软件架构越来越注重系统的可用性availability

正是在这种趋势的驱动下,微服务与容器化技术才能在今天大行其道。

而高可用架构的前提是冗余

一个高可用服务必然由多个进程组成,这些进程互为备份,部分进程失效不会导致整个服务不可用。

如果服务是有状态的,那么每个进程都需要维护自己的一个状态副本replica。为了保证有状态进程的可替代性,如何维护这些副本的一致性consistency成为了至关重要的问题。

以提供锁服务的分布式协调服务为例,这类服务必须满足一下两个特性:

  • 高可用:服务失效会导致下游服务不可用
  • 强一致:下游服务观察到不一致的状态会导致锁失效

根据分布式数据库的CAP理论,同时实现这个特性是困难的:

  • Consistency 一致性
  • Availability 可用性
  • Partition 网络分区

在网络通信正常的时候,分布式数据库可以同时保证 C 与 A

在发生网络分区(进程间无法正常通信)的时候,数据库必须在 C 和 A 中做出权衡:

  • AP 系统:保证可用性,牺牲一致性,每个节点可以对外服务,但整个数据库会处于不一致的状态
  • CP 系统:保证一致性,牺牲可用性,每个节点拒绝对外服务,但整个数据库会始终保持一致的状态

在 CAP 的框架下,我们似乎陷入了一个两难的境地:实现一个 CA 的系统是不可能的吗?
在回答这个问题前,首先要指出 CAP 理论存在的一个问题:

CAP 理论在权衡时仅考虑了 3 个因素,而忽略了其他的指标,比如:性能、实现复杂度、机器数量...

举个例子:存在一种流行的观点 —— ZooKeeper 是一个 CP 系统。然而,在 ZooKeeper 集群中,只要有半数以上的节点之间能够正常通信,集群仍然能够正常对外提供服务。这意味着:ZooKeepr 在网络发生分区的情况下,依然是可用且一致。

在发生网络分区时,如果要保证系统的可用且一致,需要付出一些额外的代价。以 ZK 为例子,其付出的的代价就是更多的机器资源(必须部署 3 个以上的节点)与更高的系统复杂度(使用一致性算法)。

共识问题

所谓共识Consensus,就是在某件事情上达成一致意见。分布式系统的共识问题可以表述为:

系统中有一组可以提出提案Proposal的进程集合,其中一个或多个进程提出一个提案,然后通过共识算法Consensus Algorithm从中选定Chosen一个提案作为最终结果。

以上图中的场景为例:图中是一个多主的分布式数据库,同时允许多个节点接收写操作请求,并且两者会彼此交换修改指令以保证数据的一致性。

刚好在某个时刻,两个客户端同时发起对同个记录的变更操作,因此可能同时存在两种不同的更新顺序(提案)。为了保证数据副本的一致性,两个数据库必须在更新顺序上达成一致(共识)。

容错

由于运行环境的不确定性,系统故障总是是不可避免的,这些故障大致可以分为两类:

  • 非拜占庭故障Byzantine fault
    进程间通过不可靠的网络来传输消息
    • 进程在运行过程中可能出现卡顿、宕机、重启,但是不会发送错误的消息
    • 消息在传输过程中可能丢失、重复、失序,但是不会损坏或被篡改
  • 拜占庭故障Non-Byzantine fault
    系统中可能出现恶意的进程,故意向其他进程发出错误的讯息
    引发其他进程出现异常的行为,导致整个系统失效

因此共识算法又可以分为拜占庭容错非拜占庭容错两类。
前者主要是应用于区块链领域,通过高昂的计算开销来杜绝进程作恶的可能性,功能强大但不适合用于提供高性能的一致性保障。
后者主要是为数据管理服务,通过额外的安全性假设,减少容错开销,从而能够提供高性能的一致性保障。

接下来要解析的分布式一致性协议都属于非拜占庭容错的共识算法。这类算法能够在不确定的环境中,为系统提供以下保证:

  • 合法:提案必须由某个进程提出,不能凭空出现
  • 完整:每个进程只能表决一次,不能改变结果
  • 一致:所有进程的最终决策必须一致

容错共识算法的核心可以概括为以下两点:

  • 安全性Safety:所有进程收敛到一致的合法状态
  • 容错性Fault-tolerance:少量进程崩溃,系统可持续运行

Paxos 算法

最经典的一致性协议莫过于 Paxos 算法,下面对其进行简单的分析。

基本概念

  • 提案:每个提案[n,v]由两部分组成

    • n : 提案被提出的顺序
    • v : 提案的值
  • 角色:每个进程都能够同时担任以下角色中的一个或多个

    • 提议者Proposer:主动发起提案
    • 决策者Acceptor:对提案进行表决
    • 提议者Learner:被动接受表决结果
  • 提案状态:每个提案可能处于以下 3 个状态之一

    • 提出prepare:被某个 Proposer 提出
    • 通过accept:被某个 Acceptor 通过
    • 选定chosen:被半数以上 Acceptor 通过
  • 通信模型:进程之间的通信是通过消息传递进行的,并且消息是单向传递的,消息接收进程可以选择拒绝响应。网络是不可靠的,进程需要自行处理消息丢失,乱序的情况。

表决

Paxos 的基本思想很简单:通过对多个提案进行表决,从中选定一个提案作为最终的共识,并且一旦这个提案被选定之后,结果不会再发生改变。参与表决的角色有 Proposer(主动发起请求)与 Acceptor(被动响应请求),而 Learner 只关注表决结果,本身并不参与表决过程。

Paxos 的每轮表决分为两阶段:生成提案Prepare ➜ 表决提案Accept
为了支持表决过程,Acceptor 进程需要在本地维护以下两个状态:

  • n max_prepare :已响应Prepare请求的最大的提案编号
  • [nmax_accept, v'] :已通过Accept请求的最大编号的提案

为了更好地理解表决过程,下面结合一个具体的场景进行描述:

集群启动时需要选举出一个 Leader 进程,两个名为 Alice 与 Bob 的进程分别以 Proposer AProposer B 的身份提出了选举自己为 Leader 的提案,并分别向集群中的 3 个 Acceptor X / Y / Z 发起表决流程。

Prepare 阶段

Proposer在提出提案[n,v]前,要保证编号n是未被其他提案占用的。

因此Proposer会向多个Acceptor发送编号为n的 prepare 请求,锁定这个编号。

首先两个 Proposer 先通过某些方式(比如:维护一个全局计数器、询问所有Acceptor……)获取到可用的提案编号,A、B 分别得知的可用的编号为 2、3。然后使用这个编号向 Acceptor 集群发出 prepare 请求。

Acceptor在接收到编号n的 prepare 请求后:

  • 如果 n > nmax_prepare,返回已知编号的最大的提案[nmax_accept, v']
  • 否则,拒绝响应这个请求

X、Y 先后接收到 A、B 的请求,并且 B 的编号要比 A 大,因此 A、B 都接收到了响应。

而 Z 则是先接收到了 B 的请求,后接收到 A 的请求,因此只响应了 B 的请求,忽略了 A 的请求。

只有当Proposer的 prepare 接收到半数以上的Acceptor响应时,才能发起 accept 请求。

在 prepare 阶段中,A、B 都接收到了半数以上的响应,因此两者都能够进入 accept 阶段。

Accept 阶段

在锁定编号 n 后,Proposer 需要在发起 accept 请求前生成一个新提案 [n, vnew]。其中,提案值 vnew 的生成需要遵循以下规则:

Acceptor返回的所有已知提案形成一个集合 Saccept

  • 如果 Saccept = ∅(不存在任何已知提案),vnew 可以指定为任意值
  • 如果 Saccept ≠ ∅ 则找到编号最大的已知提案 [nmax, v'] ∈ Saccept 并令 vnew = v'

在 prepare 阶段的响应中,A、B 没有接收到任何已知的提案,因此都将自己的进程 ID 作为 vnew 并生成新的提案,并发起 accept 请求。

Acceptor 接受到提案为 [nnew,vnew] 的 accept 请求之后:

  • 如果 nnew ≥ nmax_prepare,通过提案并更新 [nmax_accept,v']=[nnew,vnew]
  • 否则,拒绝通过该提议

由于 X、Y 已经响应了 B 的 prepare 请求,于是认为 A 的提案已经过期,拒绝通过其 accept 请求。

而 X、Y、Z 已知编号的最大的 prepare 请求均由 B 所发出,因此 B 的 accept 请求顺利通过。

最终,进程 Alice 落选,进程 Bob 当选为新的 Leader。

Learner 如何得知表决结果?
  • 每次通过一个提案后,Acceptor 主动通知 Learner
    • 优点:实时性高,在 Learner 数量少时效率高
    • 缺点:网络复杂度高,并且 Acceptor 的通知消息也可能丢失,需要重试机制
  • Learner 主动向 Acceptor 轮询通过的提案信息
    • 优点:可靠性高,能够处理 Acceptor 崩溃或者消息丢失的情况
    • 缺点:实时性较差,需要控制好轮询频率在

安全性

一旦半数以上的 Acceptor 通过了某个提案后,意味着该提案被选定了,并且这个被选定的提案值 vchosen 今后不会再发生改变。

由于此前 A 的 accept 请求并未通过,因此发起了新的一轮表决。

尽管此次 A 的 accept 请求被通过了,但是被选定的提案值是此前已经被选定的 Bob。

容错性

只要半数以上的 Acceptor 存活,系统的表决机制依然能够正常运作,此时系统仍然是可用的。

假设在此前的 accpet 阶段,其中一个 Acceptor 崩溃了,系统中剩下两个 Acceptor 仍然能够继续进行后续的表决流程,系统依然是可用的。

边界情况

在 accept 阶段,X、Y 通过并选定了提案值 Bob,由于消息丢失,Z 并未通过该提案。
后来 X、Y 进程崩溃了,系统中只剩下 Z 正常运作,此时 A 发起新的一轮表决。
由于过半数的限制,A 无法生成值为 Alice 的提案,系统的状态不会被改变。

在 accept 阶段,X 通过了提案值 Bob,由于消息丢失,Y、Z 并未通过该提案。
后来 X 进程崩溃了,系统中剩下 Y、Z 正常运作,此时 A 发起新的一轮表决。
由于过半数的限制,此前的提案值 Bob 未被选定,系统选定了 A 提出的值 Alice。

安全性证明

Paxos 算法的安全性保证:

  • 合法:参与决策的提案必定来源于某个 Proposer
  • 完整:编号为n的提案只能被某个 Proposer 提出一次,也只能被同一个 Acceptor 表决一次
  • 一致:一旦提议[n, vchosen]被选定,之后所有的提案都会包含vchosen

Paxos 的一致性是通过约束提案的生成来实现的,下面进行简单的证明:

假设:

第一个被选定的提案是 [m0,v0],那么必然存在一个多数 Acceptor 集合 S0 通过了编号 m0 的 accept 请求
下一个被提出的提案是 [m1,v1],那么必然存在一个多数 Acceptor 集合 S1 响应了编号 m1 的 prepare 请求

证明:v1 = v0

根据过半数原则,S0S1 的交集必然非空,S1 中必然至少存在一个通过了提案 [n0,v0]Acceptor, 因此 S0S1 的交集中的所有 Acceptor 最大的 n max_accept 只能是 m0

根据 prepare 成功的条件,S1 中所有 Acceptor 通过的提案中,最大的 n max_accept 必然小于 m1

假设 S1 中所有 Acceptor 通过的所有提案中编号最大为 x,显然有 m0 ≤ x < m1,由于序号 x 必须是由 m1 前某个提案提出的,此时 x 必定是 m0

因此 prepare 的响应中,必定至少有一个通过了 m0 提案的 Acceptor 返回提案 [n0,v0]
因此 v0 = v1 成立。

根据数学归纳法,vn+1 = vn 亦可得证。

应用构想

在实际工程应用中,并不会直接使用原始的 Paxos 算法,但是这并不妨碍我们用这个算法实现一个强一致的分布式数据库。

在数据库领域,预写日志 WAL是一种常见的提高数据库性能的手段:

  • 任何修改数据库状态的命令在执行之前,都会先写入一个顺序日志并落盘,然后通过异步线程按照命令的写入顺序将其应用到数据库中
  • 读取数据时,会将数据库中的状态与 WAL 中的状态进行合并,保证返回给客户端的数据是最新的

这一机制能在提供较高写入性能的同时,保证了数据的安全性:即便机器断电,数据也不会丢失。

WAL 中的条目是有序的,每个条目包含一个或一组原子性的变更命令。根据上一篇文章的结论:只要所有数据库按照相同的顺序执行相同的命令,则这两个数据库的状态就能保持一致。换而言之,通过使用 Paxos 算法,保证多个数据库本地的 WAL 副本中的内容完全一致,那么数据库最终肯定能收敛到一致的状态

假设存在一个类似 Redis 的 KV 数据库,用户可以向数据库发送指令存取键值对。该数据库支持多主架构,并通过使用 Paxos + WAL 保证数据的一致性:

当某个数据库 X 接收到请求时:
  1. 作为 Proposer 提出提案(向 WAL 中的特定位置中写入一条记录),并且交由 X、Y、Z 组成的 Acceptor 集群进行表决
  2. 每个 Acceptor 通过提案后,会将结果告知由 X、Y、Z 组成的 Learner 集群
  3. 当 Learner 感知到某个提案被选定了(该 WAL 位置尚未被占用,允许写入),就会往本地的 WAL 追加命令(图中省略了 Y、Z 节点的 WAL)并返回响应
  4. 如果提案没有被选定(该 WAL 位置已经被占用,拒绝写入),则发起下一轮表决,直到提案被通过或客户端请求超时

从 WAL 的角度来观察,上述过程如下图所示:

posted @ 2020-05-12 00:50  buttercup  阅读(667)  评论(0编辑  收藏  举报