分布式系统原理(9)Paxos 协议

版权声明:本文为博主原创文章,转载请附上原文链接。 https://blog.csdn.net/Raito__/article/details/83305690

Paxos 协议

简介

Paxos 协议是少数在工程实践中证实的强一致性、高可用的去中心化分布式协议

Paxos 协议的流程较为复杂,但其基本思想却不难理解,类似于人类社会的投票过程。Paxos 协议中,有一组完全对等的参与节点(称为 accpetor),这组节点各自就某一事件做出决议,如果某个决议获得了超过半数节点的同意则生效。Paxos 协议中只要有超过一半的节点正常,就可以工作,能很好对抗宕机、网络分化等异常情况

介绍 Paxos 协议的资料很多,Lamport 的论文也写得简明有趣。与大多数材料不同的是,本文不首先介绍协议的推理和证明过程,而是从工程上的算法流程描述起,感性的介绍协议过程。进而用一些复杂的例子演示协议的过程。最后,本文再介绍协议是如何推导设计出来的

协议描述

节点角色

Paxos 协议中,有三类节点:

  • Proposer: 提案者。Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,在工程中可以是任何操作,例如“修改某个变量的值为某个值”、“设置当前 primary 为某个节点”等等。Paxos协议中统一将这些操作抽象为 value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某个Proposer 提议“将变量 X 设置为 1”,另一个 Proposer 提议“将变量 X 设置为 2”,但对同一轮 Paxos过程,最多只有一个 value 被批准。
  • Acceptor: 批准者。 Acceptor 有 N 个, Proposer 提出的 value 必须获得超过半数(N/2+1)的Acceptor批准后才能通过。Acceptor 之间完全对等独立。
  • Learner: 学习者。Learner 学习被批准的 value。所谓学习就是通过读取各个 Proposer 对 value的选择结果,如果某个 value 被超过半数 Proposer 通过,则 Learner 学习到了这个 value。 回忆(2.4 )不难理解,这里类似 Quorum 机制,某个 value 需要获得 W=N/2 + 1 的 Acceptor 批准,从而学习者需要至少读取 N/2+1 个 Accpetor,至多读取 N 个 Acceptor 的结果后,能学习到一个通过的 value。上述三类角色只是逻辑上的划分,实践中一个节点可以同时充当这三类角色

流程描述

Paxos 协议一轮一轮的进行,每轮都有一个编号。每轮 Paxos 协议可能会批准一个 value,也可能无法批准一个 value。如果某一轮 Paxos 协议批准了某个 value,则以后各轮 Paxos 只能批准这个value。上述各轮协议流程组成了一个 Paxos 协议实例,即一次 Paxos 协议实例只能批准一个 value,这也是 Paxos 协议强一致性的重要体现。每轮 Paxos 协议分为阶段,准备阶段和批准阶段,在这两个阶段 Proposer 和 Acceptor 有各自的
处理流程

Proposer 的流程:

(准备阶段)
1. 向所有的 Acceptor 发送消息“Prepare(b)”;这里 b 是 Paxos 的轮数,每轮递增
2. 如果收到任何一个 Acceptor 发送的消息“Reject(B)”,则对于这个 Proposer 而言本轮 Paxos 失败,将轮数 b 设置为 B+1 后重新步骤 1;

(批准阶段,根据收到的 Acceptor 的消息作出不同选择)
3. 如果接收到的 Acceptor 的“Promise(b, v_i)”消息达到 N/2+1 个(N 为 Acceptor 总数,除法取整,下同) ;v_i 表示 Acceptor 最近一次在 i 轮批准过 value v。
 3.1 如果收到的“Promise(b, v)”消息中, v 都为空, Proposer 选择一个 value v,向所有 Acceptor 广播 Accept(b, v);
 3.2 否则,在所有收到的“Promise(b, v_i)”消息中,选择 i 最大的 value v,向所有 Acceptor 广播消息 Accept(b,v);
4. 如果收到 Nack(B),将轮数 b 设置为 B+1 后重新步骤 1;

Accpetor 流程
(准备阶段)
1. 接受某个 Propeser 的消息 Prepare(b)。
参数 B 是该 Acceptor 收到的最大 Paxos 轮数编号;V 是 Acceptor 批准的 value,可以为空
 1.1 如果 b>B,回复 Promise(b, V_B),设置 B=b; 表示保证不再接受编号小于 b 的提案。
 1.2 否则,回复 Reject(B)

(批准阶段)
2. 接收 Accept(b, v)
 2.1 如果 b < B, 回复 Nack(B),暗示 proposer 有一个更大编号的提案被这个 Acceptor 接收了
 2.2 否则设置 V=v。表示这个 Acceptor 批准的 Value 是 v。广播 Accepted 消息

实例

基本例子

基本例子里有 5 个 Acceptor,1 个 Proposer,不存在任何网络、宕机异常。我们着重考察各个 Accpetor 上变量 B 和变量 V 的变化,及 Proposer 上变量 b 的变化
1.初始状态

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 0 0 0 0 0
V NULL NULL NULL NULL NULL
Proposer 1
b 1

2.Proposer 向所有 Accpetor 发送“Prepare(1)”,所有 Acceptor 正确处理,并回复 Promise(1, NULL)

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 1 1 1 1 1
V NULL NULL NULL NULL NULL
Proposer 1
b 1

3.Proposer 收到 5 个 Promise(1, NULL),满足多余半数的 Promise 的 value 为空,此时发送Accept(1, v1),其中 v1 是 Proposer 选择的 Value

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 1 1 1 1 1
V v1 v1 v1 v1 v1

4.此时, v1 被超过半数的 Acceptor 批准, v1 即是本次 Paxos 协议实例批准的 Value。 如果 Learner学习 value,学到的只能是 v1

批准的 Value 无法改变

在同一个 Paxos 实例中,批准的 Value 是无法改变的,即使后续 Proposer 以更高的序号发起 Paxos协议也无法改变 value
1.例如,某次 Paxos 协议运行后,Acceptor 的状态是:

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 3 3 3 2 2
V v1 v1 v1 NULL NULL

5 个 Acceptor 中,有 3 个已经在第三轮 Paxos 协议批准了 v1 作为 value。其他两个 Acceptor 的 V为空,这可能是因为 Proposer 与这两个 Acceptor 的网络中断或者这两个 Acceptor 宕机造成的。

2.此时,即使有新的 Proposer 发起协议,也无法改变结果。假设 Proposer 发送“prepare(4)消息”,由于 4 大于所有的 Accpetor 的 B 值,所有收到 prepare 消息的 Acceptor 回复 promise 消息。但前三个 Acceptor 只能回复 promise(4, v1_3),后两个 Acceptor 回复 promise(4, NULL)

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 4 4 4 4 4
V v1 v1 v1 NULL NULL

3.此时,Proposer 可能收到若干个 Acceptor 发送的 promise 消息,没有收到的 promise 消息可能是网络异常造成的。无论如何,Proposer 要收到至少 3 个 Acceptor 的 promise 消息后才满足协议中大于半数的约束,才能发送 accpet 消息。这 3 个 promise 消息中,至少有 1 个消息是 promise(4, v1_3),至多 3 个消息都是 promise(4,v1_3)。另一方面,Proposer 始终不可能收到 3 个 promise(4, NULL)消息,最多收到 2 个。综上,按协议流程,Proposer 发送的 accept 消息只能是“accept(4, v1)”而不能自由选择 value

无论这个 accept 消息是否被各个 Acceptor 接收到,都无法改变 v1 是被批准的 value 这一事实。即从全局看,有且只有 v1 是满足超过多数 Acceptor 批准的 value。例如,假设 accept(4, v1)消息被Acceptor 1、Acceptor2、Acceptor4 收到,那么状态变为:

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 4 4 4 4 4
V v1 v1 v1 v1 NULL

从这个例子我们可以看到一旦一个 value 被批准,此后永远只能批准这个 value

一种不可能出现的状态

Paxos 协议的核心就在与“批准的 value 无法改变”,这也是整个协议正确性的基础,为了更好的理解后续对 Paxos 协议的证明。这里再看一种看似可能,实际违反协议的状态,这种状态也是后续反证法证明协议时的一种错误状态

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 1 1 1 2 2
V v1 v1 v1 v2 v2

上述状态中,3 个轮次为 1 的 Acceptor 的 value 为 v1,2 个轮次更高的 Acceptor 的 value 为 v1。此时被批准的 value 是 v1

假设此时发生新的一轮 b=3 的 Paxos 过程,Proposer 有可能收到 Acceptor 3、4、5 发出的 3 个promise 消息分别为“promise(1, v1_1)”,“promise(2, v2_2)” “promise(2, v2_2)”。按协议,proposer选择 value 编号最大的 promise 消息,即 v2_2 的 promise 消息,发送消息“Accept(3, v2)”,从而使得最终的批准的 value 成为 v2。就使得批准的 value 从 v1 变成了 v2

上述假设看似正确,其实不可能发生。这是因为本节中给出的初始状态就是不可能出现的。这是因为,要到达成上述状态,发起 prepare(2)消息的 proposer 一定成功的向 Acceptor 4、Acceptor 5发送了 accept(2, v2)。但发送 accept(2, v2)的前提只能是 proposer 收到了 3 个“promise(2, NULL)”消息。 然而,从状态我们知道,在 b=1 的那轮 Paxos 协议里,已经有 3 个 Acceptor 批准了 v1,这 3 个Acceptor 在 b=2 时发出的消息只能是 promise(2, v1_1),从而造成 proposer 不可能收到 3 个“promise(2, NULL)”,至多只能收到 2 个“promise(2, NULL)”。另外,只要 proposer 收到一个“promise(2, v1_1)”,其发送的 accept 消息只能是 accept(2, v1)

从这个例子我们看到 Prepare 流程中的第 3 步是协议中最为关键的一步,它的存在严格约束了“批准的 value 无法改变”这一事实。在后续协议推导中我们将看到这一步是如何被设计出来的

节点异常

这里给一个较为复杂的异常状态下 Paxos 运行实例。本例子中有 5 个 Acceptor 和 2 个 Proposer
1.Proposer 1 发起第一轮 Paxos 协议,然而由于异常,只有 2 个 Acceptor 收到了 prepare(1)消息

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 1 1 0 0 0
V NULL NULL NULL NULL NULL

2.Proposer 1 只收到 2 个 promise 消息,无法发起 accept 消息;此时, Proposer 2 发起第二轮 Paxos协议,由于异常,只有 Acceptor 1、3、4 处理了 prepare 消息,并发送 promise(2, NULL)消息

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 2 1 2 2 0
V NULL NULL NULL NULL NULL

3.Proposer 2 收到了 Acceptor 1、3、4 的 promise(2, NULL) 消息,满足协议超过半数的要求,选择了 value 为 v1,广播了 accept(2, v1)的消息。由于异常,只有 Accptor 3、4 处理了这个消息

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 2 1 2 2 0
V NULL NULL v1 v1 NULL

4.Proposer 1 以 b=3 发起新一轮的 Paxos 协议, 由于异常,只有 Acceptor 1、 2、 3、 5 处理了 prepare(3)消息

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 3 3 3 2 3
V NULL NULL v1 v1 NULL

5.由于异常,Proposer 1 只收到 Acceptor1、2、5 的 promise(3, NULL)的消息,符合协议要求,Proposer 1 选择 value 为 v2,广播 accept(3, v2)消息。由于异常,这个消息只被 Acceptor 1、2 处理

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 3 3 3 2 3
V v2 v2 v1 v1 NULL

当目前为止,没有任何 value 被超过半数的 Acceptor 批准,所以 Paxos 协议尚没有批准任何 value。然而由于没有 3 个 NULL 的 Acceptor,此时能被批准的 value 只能是 v1 或者 v2 其中之一

6.此时 Proposer 1 以 b=4 发起新的一轮 Paxos 协议,所有的 Acceptor 都处理了 prepare(4)消息

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 4 4 4 4 4
V v2 v2 v1 v1 NULL

7.由于异常,Proposer 1 只收到了 Acceptor3 的 promise(4, v1_3)消息、Acceptor4 的 promise(4, v1_2)、 Acceptor5 的 promise(4, NULL)消息,按协议要求,只能广播 accept(4, v1)消息。假设 Acceptor2、3、4 收到了 accept(4, v1)消息。由于批准 v1 的 Acceptor 超过半数,最终批准的 value 为 v1

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 4 4 4 4 4
V v2 v1 v1 v1 NULL

竞争及活锁

从前面的例子不难看出,Paxos 协议的过程类似于“占坑”,哪个 value 把超过半数的“坑”(Acceptor)占住了,哪个 value 就得到批准了。这个过程也类似于单机系统并行系统的加锁过程。假如有这么单机系统:系统内有 5 个锁,有多个线程执行,每个线程需要获得 5 个锁中的任意 3 个才能执行后续操作,操作完成后释放占用的锁。我们知道,上述单机系统中一定会发生“死锁”。例如, 3 个线程并发,第一个线程获得 2 个锁,第二个线程获得 2 个锁,第三个线程获得 1 个锁。此时任何一个线程都无法获得 3 个锁,也不会主动释放自己占用的锁,从而造成系统死锁。

但在 Paxos 协议过程中,虽然也存在着并发竞争,不会出现上述死锁。这是因为,Paxos 协议引入了轮数的概念,高轮数的 paxos 提案可以抢占低轮数的 paxos 提案。从而避免了死锁的发生。然而这种设计却引入了“活锁”的可能,即 Proposer 相互不断以更高的轮数提出议案,使得每轮 Paxos过程都无法最终完成,从而无法批准任何一个 value

1.Proposer 1 以 b=1 提起议案,发送 prepare(1)消息,各 Acceptor 都正确处理,回应 promise(1, NULL)

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 1 1 1 1 1
V NULL NULL NULL NULL NULL

2.Proposer 2 以 b=2 提起议案,发送 prepare(2)消息,各 Acceptor 都正确处理,回应 promise(2, NULL)

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 2 2 2 2 2
V NULL NULL NULL NULL NULL

3.Proposer 1 收到 5 个 promise(1, NULL)消息,选择 value 为 v1 发送 accept(1, v1)消息,然而这个消息被所有的 Acceptor 拒绝,收到 5 个 Nack(2)消息

4.Proposer 1 以 b=3 提起议案,发送 prepare(3)消息,各 Acceptor 都正确处理,回应 promise(3, NULL)

Acceptor 1 Acceptor 2 Acceptor 3 Acceptor 4 Acceptor 5
B 3 3 3 3 3
V NULL NULL NULL NULL NULL

5.Proposer 2 收到 5 个 promise(2, NULL)消息,选择 value 为 v2 发送 accept(2, v2)消息,然而这个消息被所有的 Acceptor 拒绝,收到 5 个 Nack(3)消息

上述过程交替进行,则永远无法批准一个 value,从而形成 Paxos 协议活锁。Paxos 协议活锁问题也是这个协议的主要问题

工程投影

Chubby 中的 Paxos

Chubby 是最早基于 Paxos 的分布式系统之一。Chubby 的设计人员没有直接提供一种 Paxos 的开发库,而是利用 Paxos 实现一个高可用的分布式系统,再利用这个分布式系统对外提供高可用存储、分布式锁等服务,从而间接的提供了 Paxos 功能。

Chubby 中的节点完全是对等的,通过 Paxos 协议,这些节点选举出一个 Master 节点(Primary),公开的资料中没有解释 Chubby 使用 Paxos 的细节,例如如何选择 Paxos 的轮次号,如何避免 Paxos活锁等。当选举出 Primary 节点后,所有读写操作都由 Primary 节点控制,Chubby 系统从一个完全对等的去中心化状态变为一个 Primary-Secondary 的中心化状态。当 Primary 异常时, Chubby 节点将重新利用 paxos 协议发起新一轮的选举以确定新的 primary 节点。新 primary 节点与原 primary 节点具有完全一样的持久化信息,新 primary 将代替原 primary 节点对外提供读写服务。

基于 Chubby 的服务,其他的分布式系统可以很容易的实现选择 primary、保存最核心元数据等功能。利用 Chubby 可以大大简化分布式系统的设计:可以认为整个 Chubby 集群逻辑上是一个 magic的高可用(几乎不会停服务)的中心节点,其他分布式系统可以基于这个大中心节点可以实现中心化的副本控制协议。由于 Chubby 集群本身是由多个节点组成的分布式系统, 基于 Chubby 的分布式系统无需直接实现 Paxos 协议,就可以利用 Paxos 协议实现全局完全无单点

Zookeeper 中的 Paxos

Zookeeper 使用了一种修改后的 Paxos 协议。

首先, Zookeeper 的协议运行依赖 TCP 协议实现 FIFO, Zookeeper 通过 TCP 协议获得两点保障:1、数据总是严格按照 FIFO (first in first out)规则从一个节点传递到另一个节点的; 2、当某个 TCP链接关闭后,这个链接上不再有数据传递。由于 TCP 协议为传输的每一个字节设置了序列号(sequence number)及确认(acknowledgment),上述两点在 TCP 协议上是完全可以保证的。需要注意的是 Zookeeper 并不要求 TCP 协议可以可靠的将数据传输到对端节点,基于 TCP 协议实现真正意义上的可靠传输也是做不到的。Zookeeper 基于 TCP 的上述两点保障,可以较大的简化问题模型,忽略诸如网络消息乱序、网络消息重复等的异常,从而较大的简化协议设计。

再者,在 Zookeeper 中,始终分为两种场景:一、Leader activation,在这个场景里,系统中缺乏 Leader(primary) ,通过一个类似 paxos 协议的过程完成 Leader 选举。二、Active messaging,在这个场景里,Leader 接收客户端发送的更新操作,以一种类似两阶段提交的过程在各个 follower(secondary)节点上进行更新操作。在 Leader activation 场景中完成 leader 选举及数据同步后,系统转入 Active messaging 场景,在 active messaging 中 leader 异常后,系统转入 Leader activation 场景。

无论在那种场景,Zookeeper 依赖于一个全局版本号:zxid。zxid 由(epoch, count)两部分组成,高位的 epoch 部分是选举编号,每次提议进行新的 leader 选举时 epoch 都会增加,低位的 count 部分是 leader 为每个更新操作决定的序号。可以认为,一个 leader 对应一个唯一的 epoch,每个 leader任期内产生的更新操作对应一个唯一的有序的 count,从而从全局的视野,一个 zxid 代表了一个更新操作的全局序号(版本号)。每个 zookeeper 节点都有各自最后 commit 的 zxid,表示这个 zookeeper 节点上最近成功执行的更新操作,也代表了这个节点的数据版本。在 Leader activation 阶段,每个 zookeeper 节点都以自己的 zxid 作为 Paxos 中的 b 参数发起 paxos 实例,设置自己作为 leader(此为 value)。

每个 zookeeper节点既是 proposer 又是 acceptor,所以,每个 zookeeper 节点只会 accpet 提案编号 b 大于自身 zxid的提案。不难理解,通过 paxos 协议过程,某个超过 quorum 半数的节点中持有最大的 zxid 的节点会成为新的 leader。值得注意的是,假如参与选举的每个 zookeeper 节点的 zxid 都一样,即所有的节点都以相同的 b=zxid 发提案,那么就有可能发送无法选举出 leader 的情况。 zookeeper解决这个问题的办法很简单,zookeeper 要求为每个节点配置一个不同的的节点编号,记为 nodeid,paxos 过程中以 b=(zxid, nodeid)发起提议,从而当 zxid 相同时会优先选择节点编号较大的节点成为leader。成为新 leader 的节点首先与 follower 完成数据同步后,再次说明,数据同步过程可能会涉及删除 follower 上的最后一条脏数据。当与至少半数节点完成数据同步后, leader更新 epoch,在各个 follower 上以(epoch + 1, 0) 为 zxid 写一条没有数据的更新操作。这个更新操作称为 NEW_LEADER 消息,是为了在各个节点上更新 leader 信息,当收到超过半数的 follower 对NEW_LEADER 的确认后, leader 发起对 NEW_LEADER 的 COMMIT 操作,并进入 active messaging状态提供服务。

进入 active messaging 状态的 leader 会接收从客户端发来的更新操作,为每个更新操作生成递增的 count,组成递增的 zxid。 Leader 将更新操作以 zxid 的顺序发送给各个 follower (包括 leader 本身,一个 leader 同时也是 follower),当收到超过半数的 follower 的确认后,Leader 发送针对该更新操作的 COMMIT 消息给各个 follower。这个更新操作的过程很类似两阶段提交,只是 leader 永远不会对更新操作做 abort 操作。

如果 leader 不能更新超过半数的 follower,也说明 leader 失去了 quorum,此时可以发起新的 leader选举,最后一条更新操作处于“中间状态”,其是否生效取决于选举出的新 leader 是否有该条更新操作。从另一个角度,当 leader 失去 quorum 的 follower,也说明可能有一个超过半数的节点集合正在选举新的 leader。

Zookeeper 通过 zxid 将两个场景阶段较好的结合起来,且能保证全局的强一致性。由于同一时刻只有一个 zookeeper 节点能获得超过半数的 follower,所以同一时刻最多只存在唯一的 leader;每个 leader 利用 FIFO 以 zxid 顺序更新各个 follower,只有成功完成前一个更新操作的才会进行下一个更新操作,在同一个 leader 任期内,数据在全局满足 quorum 约束的强一致,即读超过半数的节点一定可以读到最新已提交的数据;每个成功的更新操作都至少被超过半数的节点确认,使得新选举的 leader 一定可以包括最新的已成功提交的数据

Megastore 中的 Paxos

Megastore 中的副本数据更新基于一个改良的 Paxos 协议进行。与 Chubby 和 Zookeeper 仅仅利用 Paxos 选出 primary 不同的是,Megastore 的每次数据更新都是基于一个 Paxos 协议的实例。从而使得 Megastore 具有一个去中心化的副本控制机制。另一方面,为了获得较大的数据更新性能,Megastore 又引入了类似 Primary 的 leader 角色以在绝大部分的正常流程时优化原有 paxos 协议

基本的 Paxos 协议两个特点使得其性能不会太高:

  1. 每个 Paxos 运行实例,至少需要经历三轮网络交互:Proposer 发送 prepare 消息、Acceptor 发送 promise 消息、Proposer 再发送 accept 消息;
  2. 读取 Paxos 上的数据时,需要读取超过半数的 Acceptor 上的结果才能获得数据。

对于一个高吞吐、高并发的在线存储系统,上述特性会制约系统的性能。为此,Megastore 使用了一些方式对 Paxos协议进行了改良。 Megastore 中的副本一般都是跨机房、跨地域部署,在通常状态下,某个用户只会
。为此,Megastore 在每个机房为每个副本部署一个特殊的称为协调器(coordinator)的服务。Coordinator 服务相对 Megastore 的底层 Big table 系统而言显得非常简单,其主要功能就是维护副本直接一致性的信息,外部节点(主要是 Megastore 的 client)可以通过访问Coordinator 获知当前本地副本是否与其他副本一致,即当前副本是否具有最新的已提交的数据。利用 Coordinator,如果判断出当前本地副本已经是最新的数据,则只需读取本地副本,而不需要读取超过半数的节点就可以读取到最新的数据

下面介绍 Megastore 中的数据读取流程, Megastore 中的数据读取流程除了读取数据外还有两个重要功能:

  1. 尝试更新本地副本的 coordinator。
  2. 解决中间态数据问题。这里需要说明的是,Megastore 的更新日志与数据是分离的,每个 Megastore 副本收到更新操作后,都会立刻更新自己的日志,但不会立刻把更新操作应用到对应的 Big Table 中。这是因为,在类似 Paxos 的 Prepare 阶段就发送到各个副本了, 此时更新操作会写入日志, 但只有收到 Accept 消息后,副本才能确定这是一个已经成功提交的 Paxos 的数据,才可以将更新操作真正写入 Big Table

Megastore 数据读取流程:

1. 查询本地副本对应 Coodinator 以获知本地副本是否已经是最新的已提交的数据
2. 选择一个要读取的副本,使得该副本肯定包含最新的已提交的更新操作。
 2.1 如果从 1 中发现本地副本已经包含最新的已提交的数据,则选择本地副本。
 2.2 如果 1 中检查失败,则读取半数以上的副本,从中挑选版本号最大的副本。
3. 追赶数据。对于选择的副本,如果不能确定副本上某次更新操作是已经提交的,则通过查询其他副本确定。如果读取所有副本都无法确定,则以 paxos 协议发起一次空的更新操作。则要么空操作成为本次 paxos 的 value,要么之前不能确定的更新操作成为 paxos 的 value。
4. 修正 Coordinator。如果在 2 中选择了本地副本,且在 1 中 Coordinator 认为本地副本不包含最新已提交的数据,则向 Coordinator 发送一个 Validation 消息,告诉 Coorinator 本地副本以及与其他副本一致。
5. 向 2 中选择的副本查询数据,如果查询失败,重新发起本流程并选择其他副本读数据

这里解释追赶数据这一过程。假设有 3 个副本,正如在 2.4.4 中详细分析的,如果仅仅读取两个副本,虽然已经满足 Quorum,但在这两个副本中选择版本号最大的一个副本,却不能知道该副本上最后一个版本的数据是不是最新的已提交的数据。例如,如果 3 副本的版本是(3, 2, 3),那么读取前两个副本,3 已经是最新的已提交的版本,但如果 3 副本的版本是(3, 2, 2),那么读取前两个副本,3 不是一个最新的已提交的版本。在 Megastore 中,系统利用 Paxos 协议更新,如果某个副本收到对于某个版本的 Accept 消息,则说明该版本数据已经提交提交,对于没有收到 Accept 消息的数据,副本本身无法判断该数据是否已经提交。为此 Megastore 在读取数据增加了追赶数据的过程,就是为了在各个副本上确定每个 paxos 实例最终产生的 value

另一方面,如果某次更新对应的 paxos 实例不完整,那么也无法确定该次更新产生的 value。例如,accept 消息只在某一个副本上产生效果并生成对于的更新日志,此时读取所有的副本可以发现该日志并非一个已经成功提交的更新,且对应的那个 paxos 实例也还产生 value,有可能那次更新操作已经失败,也有可能那次更新操作正在进行。为此 Megastore 发起一次 Paxos 空操作,要么空操作成为最后 paxos 的 value,要么正在进行的更新操作成为 paxos 的 value

从上述流程不难发现,在没有异常,各副本一致的情况下,查询只会发生在本地副本,而无需读取多个副本

再继续讨论 Megastore 的更新流程。 Megastore 每次成功的更新操作都会附带指定下一次更新操作的 Leader 副本。通常,客户端指定本地机房的副本作为 leader 副本。所谓 Leader 副本非常类似Primary-Secondary 中的 Primary,但 leader 副本不是必须的,只是一种性能优化,利用 leader 副本尝试跳过 Paxos 的准备阶段, 简化了 Paxos 流程。但当 leader 副本失败(类似于代码优化中的 fast path失败),系统退化到普通的 paxos 过程

Megastore 中的数据更新流程:

1. 尝试使用 Leader 直接提交数据。访问 Leader 节点,请求 Leader 节点以 paxos 编号 0 直接向
各副本发送 Accept 消息。如果成功,转 3.
2. 准备阶段:通过更新操作在日志中的位置,获得当前 paxos 实例的编号。在本次 paxos 实例
中,选择一个最大的轮次号 b 发起正常的 Paxos 准备流程,如果收到超过半数的 promise 消
息,则转 3.
3. 批准阶段:向所有的副本发送 Accept 消息,如果失败,转 2.
4. 修正 Coordinator。如果没有收到某个副本的 Accepted 消息,向该副本对应的 Coordinator
发送一个 Invalid 消息,告知该 Coordinator 对应副本已经不与其他副本同步。
5. 各个节点根据本次操作日志更新对应的 Big Table 中的数据

上述流程中,步骤 1 尝试使用 Leader 副本进行快速更新。如果该 leader 副本收到的是本次 paxos实例(对应于全局更新操作的次序)第一个更新请求,则该流程可以生效。当 leader 节点失效,或者有并发的多个更新请求时,该优化失败,转为正常的 Paxos 过程

流程中的步骤 4 是不能失败的,如果某个副本处于不一致的状态,而又不能通知对应的Coordinator,则用户就有可能在读取流程中读到该副本上的数据,从而打破系统的强一致性。Megastore 对此的办法是:1. 相比于底层的 Big table 系统,Coordinator 是一个非常简单的无状态的轻量级服务,其稳定性本身较高。2. 每个 Coordinator 的状态都会计入 Chubby,一旦 Coordinator失去 Chubby 中锁,即失去 Chubby lease, Coordinator 会将对应副本的状态标记为不一致。其他节点可以通过监控 Coordinator 再 Chubby 中对应的锁而获知 Coordinator 的状态。从而,一旦一个Coordinator 异常失效,更新流程可能会阻塞在第 4 步直到这个 Coordinator 失去在 Chubby 中的锁。在极端网络分化等异常下,可能有这样的情况:更新流程的执行节点无法给 Coordinator 发送 Invalid消息,而 Coordinator 却能始终占有 Chubby 中对应的锁。 Megastore 将这种极端情况通过 OP 手动杀Coordinator 解决

Megastore 通过改良的 Paxos 协议给出了一种跨机房、跨地域实现高可用系统的方案。与 PNUTS的跨机房方案相比, Megastore 的方案具有强一致性,且可以随时在多个副本上读取最新的已提交的数据。而 PNUTS 的虽然也具有读最新已提交的数据的功能,但由于副本之间采用异步同步的方式,通常只能在 Primary 副本上才能读到最新的已提交的数据

猜你喜欢

转载自blog.csdn.net/Raito__/article/details/83305690