Paxos工程实践--Google chubby设计与实现

Paxos算法在工程实现时,会遇到非常多的问题,工程实现中很多细节算法并不涉及,同时如何达到较好的性能和稳定性也是一个挑战。Google的分布式锁服务chubby底层就是以Paxos算法作为基础的,这给我们提供了一个很好的范例,展示了如何填补Paxos基本算法在工程实现中的空白之处。


Chubby是以5台独立的机器组成一个cell来提供一个可靠的锁服务,5台机中只要不超过两台出错都不影响服务运行。Chubby的基本架构大致分为三层:

  • 最底层是 log replication,通过Paxos算法保证5台机的log完全一致,同时具备容错性
  • log层之上就是Key-Value类型的数据存储层,通过下层的log来保证一致性和容错性
  • 存储层之上再实现Chubby提供的锁服务和小文件存储服务
示意图如下

Paxos工程实践--Google chubby设计与实现_第1张图片


先从Log层说起。


每台机器的数据存储状态可看做一个状态机,只要给定相同的输入序列,状态机就能保证一致的变化,这就是 state machine replication。所以呢,这里的Log层目的就是实现一致的log replication。


但是Paxos算法只能从多个不同proposal value中确定一个一致的value,而这里log需要确定的是无限多个value(提供不间断服务,log无限增长),因此,每个value的确定需要一个Paxos instance。多个instance之间不相干,可以并行进行。当然每个instance也需要一个唯一的instance编号,instance编号按序分配并顺序写入log。把Paxos每个两阶段提交过程Prepare->Promise->Propose->Accept称作一个round,每个Paxos instance内又可能经过多个round才达成一致。这就是Multi-Paxos算法。


上述算法存在大量的优化空间:

  1. 多个Proposor的活锁问题会严重影响效率,导致每个instance可能要多个round才能达成一致。
  2. 在每个replica上,多个instance的Prepare->Promise阶段可以合并成一个。
因此必须选举一个master作为唯一的proposor。master宕机后其它机器自动再次选举。Paxos算法能够容忍master的“不安全状态”。也就是说,在master切换之时,允许出现短暂的多个master共存,Paxos算法可以保证replica log一致性。

先考虑第二点,如何合并多个instance的Prepare->Promise阶段。原本,多个instance之间是完全独立的,每个instance自己决定每一个round序号,保证在instance内部不重复即可,但现在为了合并Prepare->Promise阶段,多个instance公用一套序号分配,具体做法如下:

  • 当某个replica通过选举获得master资格后,用新分配的编号N广播一个Prepare消息,这个Prepare消息被所有未达成一致的instance和将来还未开始的instance共用。
  • 当Acceptor接收到Prepare后,现在必须对多个instance同时做出回应,这可以封装在一个数据包中,假设最多允许K个instance同时选举,那么:
    • 当前至多有K个未达成一致的instance,将这些未决的instance各自最新accept的value(若没有用null代替)封装进一个数据包,作为Promise消息返回
    • 同时,标记这些未决instance和所有未来instance的highestPromisedNum为N,如果N比它们原先的值大的话。这样,这些未决instance和所有未来instance都不能再accept编号小于N的Proposal。
  • 然后master就可以对所有未决instance和所有未来instance分别执行Propose->Accept阶段,始终使用编号N,如果这个master保持稳定的话,就再也不需要Prepare->Promise了。但是,一旦发现acceptor返回了一个reject消息,说明另一个master启动,用更大的编号M>N发送了Prepare消息,这时自己就要分配新的编号(必须比M更大)再次进行Prepare->Promise阶段。
上述改进的算法,在master稳定的时候,只需要用同一个编号依次执行每个instance的Promise->Accept阶段,每个instance在收到多数派的Accept后,就可以将value写入本地log并广播commit消息,其它replica收到commit消息就可将value写入log。若因为宕机或者网络原因错过了commit消息,可以主动向其它replica查询。在多个master共存的时候,也能保证多个replica的一致性。同时,只要维持多数派机器正常运行,其它机器在任意时刻宕机,都能保证已经commit的value的安全性。


这里面还有一个小的可以改进的地方。如果允许并行执行多个instance,master切换之时,新的master收到的Promise消息可能包含不连续的未决instance,即出现“gap”,state machine replication执行的时候必须按顺序执行log,遇到gap就必须等待gap处的instance达成一致value才能继续执行。为了缩短卡在gap处的时间尽快执行后续log指令,在Promise阶段对gap处提交no-operation指令,最后执行log指令时碰到no-op直接跳过。


对state machine replication的读操作,如果要保证读到最新的数据,必须也为读操作建立一个Paxos instance,序列化写入log,这样对大量的读操作性能就不高。因此,我们需要一个“安全”的选举算法,保证任意时候不出现多个master,这样,就可以对非master机器禁止commit操作,然后将读操作全部集中到master上,这样就能保证读操作始终读到最新的数据。如何实现这个“安全"的选举算法,请点击这里。


实现了一致的log replication,就可以在上层实现一个一致的state machine replication,这就是前边图中的fault-tolerant DB层。DB层在内存中的数据结构这里不做讨论,这里就大致说下snapshot+replay log的实现,这是比较简单的一个问题。


在宕机重启以后,为了恢复state machine 状态,需要将已有的log重新执行一遍,但是如果log积累了很多,那么恢复的时间就非常长,因此需要定期对state machine做一个snapshot存入磁盘,然后就可以将snapshot点之前的log删去。为了避免snapshot阻塞state machine的更新操作,可以建立一个shadow state machine,平常执行log时分别在state machine和shadow上执行,在开始snapshot后,冻结shadow,但不影响原state machine执行,snapshot完成后,再让shadow追赶上最新的log。在新的snapshot完成后才能删除旧的snapshot,这样snapshot执行一半时宕机也不影响恢复。如果state machine占用空间非常大,那么这种简单的整体snapshot方式可能开销就比较大,可以使用更好的办法,这个问题放到别的文章里讨论。


replica宕机后的恢复,分为两种情况,一种是磁盘未损坏,盘上snapshot+log可以恢复到之前某个时间的状态,然后向别的replica索取宕机后缺失的部分,一种磁盘损坏用空盘代替,这时就需要从别的replica索取整个状态,但处理方法是类似的,如果缺的比较少,可能只需要传输近期的log就够了,如果宕机太久或者需要整个重建,那就要传输最近的snapshot+log。


replica宕机重启之后,为了安全起见,暂时不能立即开始参与Paxos instance,需要等待观测到K个Paxos instance成功完成之后,K是允许并发的instance数目。这样就能保证新分配的编号(比观测到的都大)不和自己以前发过的重复。前边提到的一致读操作,也要等到这个时刻到来以后才允许开始。


log是否需要实时commit进磁盘?只要任意时刻保证多数派的机器正常运行,那么宕机后未flush到磁盘的一小部分log也可以从正常的replica中获取,因此不需要实时flush log。这可以极大的提高写盘效率。


在Fault-tolerant DB层之上,就可以比较容易的构建一个分布式锁服务--Chubby,当然需要讨论的问题还有很多,如Chubby中client cache一致性,session状态恢复,keep-Alive机制等等,且听下回分解。

你可能感兴趣的:(Paxos工程实践--Google chubby设计与实现)