分布式协议:Paxos

提出问题

在分布式系统中如何实现跨进程通信,这是一个经典问题。主要的解决方案包含了两种:

  • 共享内存
  • 消息传递

本篇不记录关于共享内存的方式,将从消息传递方式引出paxos算法。在基于消息传递通信模型的分布式系统中,不可避免的会发生进程变慢、被杀死或者重启,消息可能会发生延迟、重复、丢失等问题。在一个典型的场景中,一个分布式数据库系统,如果各个节点初始状态一致,每个节点都进行相同的操作,那么他们最后是一个一致的状态。为了保证每一个节点都保证一致的状态,那么就需要在每一条指令上都执行一个共识算法以保证每一个节点看到的指令(插入、更新、删除)都是一致的。一个通用的一致性共识算法可以应用在许多场景中,是分布式计算中的重要问题。因此从20世纪80年代起对于一致性共识算法的研究就没有停止过。

1982年一位叫Lamport的大神与另外两位作者发表了一篇论文The Byzantine Generals Problem,文中已较为晦涩的方式阐述了Paxos算法,由此打开了计算机在分布式一致性方面的大门,这也是目前公认的解决分布式一致性问题的有效办法,该论文较为难懂,如若想更好的理解还请参看Paxos Made Simple

若想了解一致性算法,那么先要明确几个实际需求。假设有一组可以提出提案的进程,那么对于一个一致性算法来说需要保证以下几点:

  • 如果没有提案被提出,那么就不会有被选定的提案。
  • 在一次被提出的多个提案中,只有一个提案能够被选中。
  • 当一个提案被选定时,其他进程能够获取到该提案。

带着这几个需求再来深入了解Paxos算法。

算法内容

在Paxos中共有三种角色参与,用 Proposer、Acceptor、Learner 表示(当然,允许身兼多职),Proposer负责提出提案,提案包括了提案编号和提案内容(比如:提案001-将税率提升10%),Acceptor可以接收提案,若提案被多数的Acceptor接受,那么将会被提交批准(chosen),Learner只能够从Acceptor那里学习被批准的提案。

上边的说法可能不好理解,可以将其理解为代表与群众的关系,代表可以是Proposer同时也是Acceptor,他们在会议上提出提案,由其他的代表一同决定是否统一该提案,如果多数同意,那么该提案也就意味着通过,群众再从各个代表那里学习提案精神。明确了角色关系,再回头看之前的问题:

  • 约束1:决议(value)只有在被Proposer提出后才能够被批准(未经批准前的决议被称为提案)。
  • 约束2:在一次paxos算法(可能存在多个提案)执行的过程中,只有一个提案会被批准。
  • 约束3:只有一个提案被批准成为决议时,Learner才能够获取到他。

推导过程

批准value的过程中,首先Proposer将value发送给Acceptor,之后Acceptor对value进行接受。为了满足只批准一个value的约束,要求经多数派接受的提案value成为决议。这是因为无论是按照人数还是按照权重划分,两组多数派至少有一个公共的Acceptor,如果每个Acceptor只能接受一个value,约束2就能保证。

于是产生了一个显而易见的新约束:

1
P1:一个Acceptor必须接受(accept)第一次收到的提案。

注意 P1 是不完备的。如果恰好一半Acceptor接受的提案具有valueA,另一半接受的提案具有valueB,那么就无法形成多数派,无法批准任何一个value。

约束2并不要求只批准一个提案,暗示可能存在多个提案。只要提案的value是一样的,批准多个提案不违背约束2。于是可以产生约束 P2:

1
P2:一旦一个具有valuev 的提案被批准(chosen),那么之后批准(chosen)的提案必须具有valuev。

注:通过某种方法可以为每个提案分配一个编号,在提案之间建立一个全序关系,所谓“之后”都是指所有编号更大的提案。

如果 P1 和 P2 都能够保证,那么约束2就能够保证。

批准一个value意味着多个Acceptor接受(accept)了该value。因此,可以对 P2 进行加强:

1
P2a:一旦一个具有valuev 的提案被批准(chosen),那么之后任何Acceptor再次接受(accept)的提案必须具有valuev。

由于通信是异步的,P2a 和 P1 会发生冲突。如果一个value被批准后,一个Proposer和一个Acceptor从休眠中苏醒,前者提出一个具有新的value的提案。根据 P1,后者应当接受,根据 P2a,则不应当接受,这种场景下 P2a 和 P1 有矛盾。于是需要换个思路,转而对Proposer的行为进行约束:

1
P2b:一旦一个具有valuev 的提案被批准(chosen),那么以后任何Proposer提出的提案必须具有valuev。

由于Acceptor能接受的提案都必须由Proposer提出,所以 P2b 蕴涵了 P2a,是一个更强的约束。

但是根据 P2b 难以提出实现手段。因此需要进一步加强 P2b。

假设一个编号为 m 的valuev 已经获得批准(chosen),来看看在什么情况下对任何编号为 n(n>m)的提案都含有valuev。因为 m 已经获得批准(chosen),显然存在一个Acceptor的多数派 C,他们都接受(accept)了v。考虑到任何多数派都和 C 具有至少一个公共成员,可以找到一个蕴涵 P2b 的约束 P2c:

1
P2c:如果一个编号为n的提案具有valuev,该提案被批准(chosen),那么存在一个多数派,要么他们中所有人都没有接受(accept)编号小于n的任何提案,要么他们已经接受(accept)的所有编号小于n的提案中编号最大的那个提案具有valuev

要满足P2c的约束,Proposer提出一个提案前,首先要和足以形成多数派的Acceptor进行通信,获得他们进行的最近一次接受(accept)的提案(prepare过程),之后根据回收的信息决定这次提案的value,形成提案开始投票。当获得多数Acceptor接受(accept)后,提案获得批准(chosen),由acceptor将这个消息告知learner。这个简略的过程经过进一步细化后就形成了Paxos算法。

在一个paxos实例中,每个提案需要有不同的编号,且编号间要存在全序关系。可以用多种方法实现这一点,例如将序数和Proposer的名字拼接起来。如何做到这一点不在Paxos算法讨论的范围之内。

如果一个没有chosen过任何Proposer提案的acceptor在prepare过程中回答了一个Proposer针对提案n的问题,但是在开始对n进行投票前,又接受(accept)了编号小于n的另一个提案(例如n-1),如果n-1和n具有不同的value,这个投票就会违背P2c。因此在prepare过程中,acceptor进行的回答同时也应包含承诺:不会再接受(accept)编号小于n的提案。这是对P1的加强:

1
P1a:当且仅当acceptor没有回应过编号大于n的prepare请求时,acceptor接受(accept)编号为n的提案。

完整算法

通过一个决议分为两个阶段:

  • prepare阶段:

    • Proposer选择一个提案编号M并将prepare请求发送给Acceptor中的一个多数派(超过半数的子集);

    • acceptor收到prepare消息后,如果提案的编号M大于它已经回复的所有prepare消息(回复消息表示接受accept),则acceptor将自己上次接受的提案回复给Proposer(ACK),并承诺不再回复小于M的提案;

      举例来说,当前Acceptor已经回复了编号为1、2、3、4、5提案,那么该Acceptor在接收到编号为6的提案时,将会回复编号5,并且承诺不会再接收编号小于6的提案。

  • 批准阶段:

    • 当一个Proposer收到了半数以上Acceptor对prepare的回复后,它将要向回复prepare请求的Acceptor发送accept请求,包括编号n和根据P2c决定的value[M, value],如果根据P2c没有已经接受的value,那么value将可以是任意值。
    • 在不违背自己向其他Proposer的承诺的前提下,acceptor收到accept请求后即批准这个请求。

在实际运行中,每一个Proposer都有可能产生多个提案,但只要系统按照当前算法运行,那么就能够保证正确性。如果一个Proposer已经生成了更大的提案编号,那么删除编号较小的是一个更好的选择,所以在Acceptor接收到一个编号为n的请求,但其发现已经接受了比n更大的编号,那么它会通知发送编号n提案的Proposer,让其将这个提案删除掉。

决议的发布

  • 一个显而易见的方法是当Acceptor批准一个value成为决议时,将这个消息发送给所有learners。也就是说当提案通过时,将会有至少Acceptors个数*Learner个数的通信次数,这对一个分布式系统来说并不是十分友好。
  • 当前我们不考虑拜占庭将军问题,learners可以通过别的learners进行通信,获取已经通过的决议。因此Acceptor只需将批准的消息发送给指定的某一个learner(也就可以理解为主learner),其他learners向它询问已经通过的决议。这个方法降低了消息量,但是主learner存在单点问题,其下线将引起系统失效。因此Acceptor需要将accept消息发送给learners的一个子集,然后由这些learners去通知所有learners。
  • 但是由于消息传递的不确定性,可能会没有任何learner获得了决议批准的消息。当learners需要了解决议通过情况时,可以让一个Proposer重新进行一次提案。注意一个learner可能兼任Proposer。

可持续性的保证

根据上述过程当一个Proposer发现存在编号更大的提案时将终止提案。这意味着提出一个编号更大的提案会终止之前的提案过程。如果两个Proposer在这种情况下都转而提出一个编号更大的提案,就可能陷入活锁,也就是两者不停的拿出更大的提案,这违背了算法可持续性的要求。一般活锁可以通过随机睡眠-重试的方法解决。

不过在这种情况下的更好的解决方案是选举出一个leader Proposer,仅允许leader提出提案。这样一来只要主Proposer和过半数的Acceptor能够进行通信,那么提案就能够被批准。

这种情况下的解决方案是选举出一个leader,仅允许leader提出提案。但是由于消息传递的不确定性,可能有多个proposer自认为自己已经成为leader。这也就需要提供leader Proposer高可用。

总结

本文通过对分布式系统的消息传递模型分析,进而引申出Paxos算法,Paxos通过引入过半原则,和基于2PC的机制,进一步优化了2PC和3PC中包含的问题,解决的同步阻塞、脑裂、无限期等待等问题。可以说Paxos算法是分布式一致性共识协议中较为理想的实现。