BasicPaxos算法

CPA理论

分布式系统中有个经典的CAP理论,就是说任何分布式系统最多满足一致性(Consistency),可用性(Availability),分区容错性(Partition Tolerance)这三者中的两个。

既然是分布式,必然将节点部署到不同的网络中,而这则会引起一致性问题。想解决一致性,就需要保证每次操作所有节点都成功执行,而这又会降低可用性。既然分区已经是事实,所以工程上应该尽量在保证一致性的前提下提高可用性。

而一致性又可以分为:

  • 强一致性:上次写什么,下次就一定能读到什么,这需要牺牲可用性。
  • 弱一致性:并不保证更新后所有线程都能读到最新值,需要一段时间进行同步。
  • 最终一致性:弱一致性的一种特例。

二段提交和三段提交

多个节点之间保持一致性的关键在于让当前节点知道其他节点的执行状态,所以最容易想到的就是引入一个“协调者”,于是出现了二段提交协议(Two Phase Commitment Protocol)和三段提交协议(Three Phase Commitment Protocol)。

二段提交可以分成:

请求阶段

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段

  1. 如果协调者节点从所有参与者节点获得的相应消息都为”同意”时:
    1. 协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
    2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
    3. 参与者节点向协调者节点发送”完成”消息。
    4. 协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
  2. 如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
    1. 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
    2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
    3. 参与者节点向协调者节点发送”回滚完成”消息。
    4. 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

但这种方式存在几个问题:

  1. 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
  3. 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

而三段提交则在二段提交的基础上增加了一个阶段,分为CanCommitPreCommitDoCommit三个阶段,并且参与者也引入了超时机制,而二段提交中只有协调者才有超时机制,这样解决了阻塞问题

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。

  1. 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

    1. 发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
    2. 事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
    3. 响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
  2. 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

    1. 发送中断请求 协调者向所有参与者发送abort请求。
    2. 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

DoCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

  1. 执行提交

    1. 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送DoCommit请求。
    2. 事务提交 参与者接收到DoCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    3. 响应反馈 事务提交完之后,向协调者发送Ack响应。
    4. 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
  2. 中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

    1. 发送中断请求 协调者向所有参与者发送abort请求
    2. 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3. 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
    4. 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在DoCommit阶段,如果参与者无法及时接收到来自协调者的DoCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

以上关于二段提交和三段提交的描述摘自这里,核心一点就是不论上述中的哪个方法,都没解决数据一致性的问题

另外我个人有个疑问——为什么二段提交是阻塞的而三段提交引入timeout就是非阻塞的了呢?换言之,为何二段提交中参与者不引入timeout机制?

猜测如下:

如果二段提交中参与者引入超时机制,这里就假设超时后回滚。当提交阶段某些机器收到了协调者发来的提交确认,而某些参与者超时直接进行回滚,则会导致数据不一致。也就所说,阻塞问题缓解了,但一致性问题却更恶化了。

而三段提交中,即便DoCommit阶段参与者没收到协调者发来的请求,也有极大的概率认为这个事务应该被提交,进而提高了一致性。

以上纯属猜测,欢迎拍砖。

BasicPaxos算法

Paxos算法有很多变种,这里记录最基本的BasicPaxos。

Paxos算法中涉及到3种角色:

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。这里类似Quorum议会机制,某个value需要获得W=N/2 + 1的Acceptor批准,Learner需要至少读取N/2+1个Accpetor,至多读取N个Acceptor的结果后,能学习到一个通过的value。

算法中涉及的主要是Proposer和Acceptor,简单说逻辑如下:

准备阶段

  1. proposer向网络内超过半数的acceptor发送prepare消息
  2. acceptor正常情况下回复promise消息

选举阶段

  1. 在有足够多acceptor回复promise消息时,proposer发送accept消息
  2. 正常情况下acceptor回复accepted消息

我们假设最简单的情况2个Proposer和3个Acceptor,首先P1、P2分别向A1、A2、A3发送perpare请求,由于网络原因,A3接收不到P1的消息,A1接收不到P2的消息。 1

Acceptor接收到请求后回复promise信息,这里我们假定A2先收到了P1的消息后收到P2的消息,所以A2此时存储的提案号是2: 2

接下来进入选举阶段,Proposer分别向Acceptor提交accept请求并带上value,A1和A3这里不细说,分别存储了相应的value后返回accepted,重点是A2。由于A2中存储的提案号码大于P1发送的accept中的提案号1,所以返回了reject。而P2已经获得了大多数的accepted,达成了一致。 3

由于P1没接受到大多数的accpeted,便增加提案号发送prepare消息: 4

此时A1和A2发现新提案号大于自己以存在的提案号,于是更新并返回已经存在的提案号和对应的value。

接下来,P1收到了promise(1,p1)promise(2,p2),2大于1,所以选择p2为接下来发送提案的值: 5

A1和A2发现P1的提案号和存储的一致,于是返回accepted消息,此时P1也达成了一致。

一个简单的BasicPaxos算法流程就结束了,但是真是环境多个节点时,很有可能出现"活锁(live lock)“的问题,即提案号不停增长但始终无法达成一致。于是便增加了一个新角色’leader’,multi paxos算法诞生了。但是呢,谁能保证选leader时候不会活锁呢?那就加个随机计时器吧——于是,raft算法诞生了。

关于raft,那就又是另外一个故事了。


参考资料:

Lamport的两篇论文《The Part-Time Parliament》和《Paxos Made Simple》

视频《paxos和分布式系统》,这个很详细建议多看几遍。