提出问题
在分布式系统中如何实现跨进程通信,这是一个经典问题。主要的解决方案包含了两种:
- 共享内存
- 消息传递
本篇不记录关于共享内存的方式,将从消息传递方式引出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就能保证。
于是产生了一个显而易见的新约束:
P1:一个Acceptor必须接受(accept)第一次收到的提案。
注意 P1 是不完备的。如果恰好一半Acceptor接受的提案具有valueA,另一半接受的提案具有valueB,那么就无法形成多数派,无法批准任何一个value。
约束2并不要求只批准一个提案,暗示可能存在多个提案。只要提案的value是一样的,批准多个提案不违背约束2。于是可以产生约束 P2:
P2:一旦一个具有valuev 的提案被批准(chosen),那么之后批准(chosen)的提案必须具有valuev。
注:通过某种方法可以为每个提案分配一个编号,在提案之间建立一个全序关系,所谓“之后”都是指所有编号更大的提案。
如果 P1 和 P2 都能够保证,那么约束2就能够保证。
批准一个value意味着多个Acceptor接受(accept)了该value。因此,可以对 P2 进行加强:
P2a:一旦一个具有valuev 的提案被批准(chosen),那么之后任何Acceptor再次接受(accept)的提案必须具有valuev。
由于通信是异步的,P2a 和 P1 会发生冲突。如果一个value被批准后,一个Proposer和一个Acceptor从休眠中苏醒,前者提出一个具有新的value的提案。根据 P1,后者应当接受,根据 P2a,则不应当接受,这种场景下 P2a 和 P1 有矛盾。于是需要换个思路,转而对Proposer的行为进行约束:
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:
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的加强:
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算法是分布式一致性共识协议中较为理想的实现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。