Zab: A simple totally ordered broadcast protocol(译)

摘要

这是一个关于ZooKeeper正在使用的全序广播协议(Zab)的简短概述。它在概念上很容易理解,也很容易实现,而且提供很高的性能。在这篇文章里,咱们会呈现ZooKeeper在Zab上的需求,也会展现这个协议该如何使用,而后咱们整体概述一下这个协议是如何工做的。html

 

1. 简介node

在雅虎(Yahoo!),咱们开发了一款叫作ZooKeeper[9]的高性能高可用的协做服务,它容许大规模的应用群执行协做任务,好比Leader选举、状态传播和会合(rendezvous)。该服务实现了一个层级的数据结点空间——znodes,客户端能够用它来实现本身的协做任务。咱们已经发现这个服务有很强的性能扩展性,所以它很容易就知足雅虎的网络规模,关键任务的产品需求。ZooKeeper放弃了锁,经过实现了无等待(wait-free)的共享数据对象,并保证在这些对象上的操做是有序的,以此来代替锁。客户端充分利用这些保障来实现本身的协做任务。通常而言,一个引出ZooKeeper的主要缘由就是,对于应用来讲,保持更新操做的有序性比其余特定的协做技术更加剧要,好比阻塞。web

集成到ZooKeeper里的是一个全序的广播协议:Zab。有序的广播是实现咱们的客户端保障的关键,同时它也须要在每一台ZooKeeper服务器上维护ZooKeeper的状态副本。这些副本经过使用咱们的全序广播协议来保持一致性,好比使用复制状态机[13]。这篇文章主要关注ZooKeeper在该广播协议上的需求以及对它实现的一个概述。算法

一个ZooKeeper服务一般由3到7台机器组成。咱们的实现支持更多机器,可是3到7台机器能够提供足够的性能与弹性。一个客户端链接提供服务的任意一台机器,并始终能得到ZooKeeper状态的一致性视图。这个服务最多能够容忍在2f+1台服务器中有f台出现故障。数据库

使用ZooKeeper的应用数量很是普遍,而且同时有成千上万个客户端并发地访问它,因此咱们须要有高的吞吐量。咱们为了知足读写操做比例大于2:1的场景而设计ZooKeeper,可是咱们发现ZooKeeper的高写吞吐量使得它也能胜任一些写主导的工做。ZooKeepr经过在每台服务器上的ZooKeeper状态副原本提供高吞吐量的读服务。所以,经过添加机器能够提高容错率和读吞吐量,可是不能提高写吞吐量。相反地,它反而由于广播协议而受限,因此咱们须要一个高吞吐量的广播协议。apache

 

图1展现了ZooKeeper服务的逻辑组成部分。读请求由包含ZooKeeper状态的本地数据库提供服务。写请求由ZooKeeper请求转化成幂等(idempotent)事务,并经过Zab发送,而后再生成响应。许多ZooKeeper写请求是有条件限制的:api

  • 一个znode只能在它没有子结点的状况下被删除安全

  • 一个zonde能够用一个名字加一个序号来建立服务器

  • 对数据的修改只能在指望的版本下进行网络

  • 甚至一些没有条件限制的写请求修改了元数据(meta data),好比版本序号,从某种程序上说这不是幂等的。

经过单一的服务器,也就是Leader来发送全部的更新请求,咱们能够把非幂等的事务转化为幂等事务。在这篇文章里,咱们用事务(transaction)来表示幂等的请求。Leader能够执行这个转换,由于它有数据库副本将来状态的完美视图,而且能够计算出新记录的状态。幂等事务就是这个新状态的一条记录。ZooKeeper充分利用幂等事务的方面不少已经超出本文范畴,可是事务的幂等性容许咱们放松广播协议在恢复过程当中的排序需求。

 

2. 需求

咱们假设有一组实现并使用了原子广播协议的进程集合。为了保证在ZooKeeper中能正确转换成幂等请求,那么同一时刻只能有一个Leader,因此咱们强制规定在协议实现中,只有这样一条进程。咱们在展现该协议更多细节时再深刻地讨论它。

ZooKeeper在广播协议中定义了以下需求:

  • 可靠交付(Reliable delivery):若是一个消息m在一台服务器上被交付,那么它最终将在全部正确的服务器上被交付。

  • 彻底有序(Total order):若是一个消息a在消息b以前被一台服务器交付,那么全部服务器都交付了a和b,而且a先于b。

  • 因果有序(Causal order):若是消息a在因果上先于消息b而且两者都被交付,那么a必须排在b以前。

 

为了保证正确性,ZooKeeper额外地须要以下前置属性(Prefix property):

  • 前置属性:若是m是Leader L交付的最后一条消息,那在m以前L提出的消息都必须已交付。

 

须要注意的是,一条进程可能被选举屡次,可是因为这前置属性,每一次都会被当成不一样Leader。

 

在下面三条保证的前提下,咱们能够保持ZooKeeper数据库副本的正确性:

  1. 可靠性和全序性保证全部副本有一致的状态。

  2. 因果有序保证了从使用Zab的应用的角度来看状态是正确的。

  3. Leader以收到请求的前提下向数据库提出更新。

 

观察到在Zab中有两种因果有序关系很重要:

  1. 若是有两条消息a和b,它们是经过同一服务器发送的,而且a在b以前提出,那么咱们说a在因果上先于b。

  2. Zab假设同时只有一个Leader能够提交提案。若是Leader变化了,任意先前提出的消息在因果上先于新的Leader提出的消息。

 

因果冲突示例

为了展现违反第二条因果关系会出现的问题,咱们来看看下面的场景:

  • 一个ZooKeeper客户端C1请求设置znode结点"/a"的值为1,这会转化成一条消息w1,内容包括("/a", "1", 1),这个元组表明路径,值和znode的版本。

  • 而后C1请求设置结点"/a"的值为2 ,这会转换成一条消息w2 ,内容为("/a", "2", 2)。

  • L1提出并交付w1 ,可是在它故障以前,只将w2发布给它本身。

  • 一个新的Leader L2接管了系统。

  • 一个客户端C2请求设置结点"/a"的值为3,这是在版本1的条件下,因此会转化为一条消息w3 ,内容为("/a", "3", 2)。

  • L2提出并交付w3。

 

在这个场景中,客户端接收到了一个对w1成功的响应,可是因为Leader故障了,对w2收到一条错误响应。若是最终L1恢复,又从新得到了Leader的地位,而后尝试交付w2提案,客户端请求的因果顺序会被破坏,副本的状态也会出现错误。

 

咱们的故障模型是带状态恢复的崩溃模式(crash-fail)。咱们不假设有同步时钟,可是咱们假设服务器能用差很少的速率感知时间的流逝(咱们使用超时来派发故障)。组成Zab的进程有一个持久化的状态,因此进程能够在故障后重启并经过持久状态进行恢复。这就意味着一个进程可能只有部分有效的状态,好比丢失最近的事务,或者更严重的,进程可能有早前没被交付而如今应该丢弃(skipped)的事务。

 

咱们可以处理f个故障,但咱们也必须处理对应的恢复故障,好比电源中断。为了从这些故障中恢复,咱们在消息交付以前,须要将它们保存在多数(Quorum)磁盘的磁盘媒介中。(一些非技术性、实际的和操做导向的缘由使咱们没有将像UPS设备、冗余/专用网络设备和NVRAMs设备加入咱们的设计中。)

 

虽然咱们假设没有拜占庭错误, 但咱们确实会在协议内部处理消化数据的污染。咱们也给协议包添加了额外的元数据(metadata),用来进行正确性检验。若是咱们发现数据被污染或者正确性检验失败,咱们会停止这个服务进程。

 

因为运行环境的独立实现和协议自己的实际问题,咱们发现实现一个彻底的拜占庭容错的系统对咱们的应用来讲是不切实际的。研究也代表为了得到彻底可靠的独立实现须要不止程序资源(programming resources)[11]。迄今为止,咱们产品的大部分问题,要么是影响全部副本的实现bug,要么是在Zab实现范畴之外的问题,可是又影响着Zab,好比网络的错误配置。

 

ZooKeeper使用了一个内存数据库,而且将事务日志和周期性的数据库快照(snapshot)保存到磁盘当中。Zab的事务日志也兼作了数据库写前(write-head)事务日志,因此一个事务只会往磁盘当中写入一次。因为数据库是一个内存数据库,而且咱们使用千兆网卡接口(gigabit interface cards),因此写操做的性能瓶颈就在于磁盘的I/O。咱们采有批量写的方式下降磁盘I/O瓶颈,这样咱们能够在一次向磁盘的写操做中记录多条事务。这个批量操做是在副本实现层面而不是在协议层面,因此这个实现和消息打包(message packing)[5]对比更相似于分组提交(group commits)[4, 6]。咱们选择不用消息打包以下降延迟,可是在批量的磁盘I/O中仍然得到了很高收益。

 

咱们的带状态恢复的故障模型意味着,当一个服务器恢复时,它会去读它的快照,并重播那些在快照以后交付的事务。所以,在原子广播(atomic broadcast)的恢复期间,并不须要保证一次性交付。咱们使用幂等事务意味着一条事务的屡次递交没有问题,只要能保持重启的顺序。这是宽松的全序需求。具体地,若是a是在b以前被交付的,以后因故障致使a再次被交付时,b也将在a以后从新交付。

 

咱们还有其余的操做需求:

  • 低延迟(Low latency):ZooKeeper在众多应用中被普遍使用,咱们的用户指望有低的响应时间。

  • 爆发性高吞吐量(Bursty high throughput):使用ZooKeeper的应用通常进行读导向的工做,可是偶尔错误的激进配置出现致使大量写吞吐峰值。

  • 平滑故障处理(Smooth failure handling):若是一个不是Leader的服务器故障,而且仍有多数服务器能正常工做,那么服务就不会中断。

 

3. 为何开发新的协议

可靠的广播协议能根据应用的需求能够呈现不一样的语义。好比,Birman和Joseph提出两个原语(primitives),ABCAST和CBCAST,这分别知足了所有有序和因果有序[2]。Zab也提供了因果有序和彻底有序的保证。

 

对于咱们的状况,有个好的候选算法是Paxos[12]。Paxos有不少重要的属性,好比无论故障进程有多少仍能保证安全性,而且容许进程崩溃和恢复,在必定实际的假设下,它能在三次通讯步骤以内提交一个操做。咱们观察到存在一些实际的假设可让咱们简化Paxos算法并且能得到很高的吞吐量。首先,Paxos能容忍消息的丢失和乱序,经过使用TCP进行服务器之间的通讯,咱们能够保证交付的消息以FIFO(先进先出)的顺序进行,这就使得咱们能够在服务器进程有多个提案消息时也能知足每一个提案的因果有序。可是Paxos并不直接保证因果有序由于它没有需求FIFO的通道( channel)。

 

Proposer是Paxos中为不一样实例提出value值的代理。为了保证进度,必须只有一个Proposer提出提案,不然Proposer可能会在一个给出实例中不断竞争下去。这样一个有效的Proposer就是Leader。当Paxos从一个Leader故障中恢复过来,新的Leader要保证全部被部分交付的消息要所有交付,而后恢复从旧的Leader中止的那个序号开始提出提案。多个Leader为一个给定实例提出提案会形成两个问题。首先,提案可能会冲突,Paxos使用选票(ballot)去派发和解决冲突的提案。其次,这样的话就不可以知道一个给定序号被提交了,进程须要能获得哪一个value值被提交的信息。Zab经过保证一个给定的提案编号只有一条提案消息来避免这两个问题。这就排除了选票的需求并简化了恢复过程。在Paxos中,若是一个服务器认为本身是Leader,那么它就会用一个更高的选票去从上一个Leader当中取得Leader地位。然而,在Zab中,一个新的Leader在多数服务器放弃上一个Leader以前不能得到Leader地位。

 

一个能够得到高吞吐量的方法就减小每一次广播的协议消息的复杂度,好比使用FSR协议(Fixed-Sequencer Ring)。使用FSR协议,即便系统增加了,吞吐量也不会降低,可是延迟会有所增加,所以不适用于咱们的环境。虚拟同步(Virtual synchrony)在群组稳定了足够长的时间也能够提供高的吞吐量[1]。可是,任一服务器故障都会致使服务的重配置,这就致使了在这样的重配置过程当中有短暂的服务中断。另外,在这样系统里,一个故障监视器(failure detector)须要监视全部服务器。这样一个故障监视器的可靠性对重配置的稳定性和速度来讲就显得很是重要。Leader导向协议(Leader-based)一样也依赖故障监视器来保证活性,可是这样一个故障监视器一次只监视一台服务器,那就是Leader。在咱们接下来章节的讨论中,咱们不为写操做使用固定的多数集和群组,而且当Leader没有故障时就能保证服务的可用性。

 

咱们的协议有固定的计数器(sequencer),根据Defago et al.的分类[3],就是咱们说的Leader。这样一个Leader经过一个Leader选举算法被选举出来,并与大多数服务器进行同步,这些服务器叫Follower。因为Leader须要管理给全部Follower的消息,考虑到这个协议使用固定计数器的决定不能在组成这个系统的服务器之间公平地分配负载。咱们接受这个方法,有如下几个缘由:

  • 客户端能够链接任一服务器,服务器要能在本地提供读操做,并保持与客户端的会话(session)信息。这个对于Follower进程的额外负担(不是Leader进程),可让负载更均匀地被分发。

  • 服务器数量的影响比较小。这意味着网络通讯的上限不会成为能够影响固定序列协议的瓶颈。

  • 实现更复杂的方法没有必要,由于简单的实现能够提供足够性能。

 

好比拥有一个可移动的计数器,提高了实现的复杂度,由于咱们必需要处理一些如token丢失的问题。一样,咱们在通讯历史的基础上移除这些模型,好比发送者导向模型(sender-bassed),以此来避免这些协议引发的二次消息复杂度。最终一致性协议也有一样的问题[8]。

 

使用一个Leader须要咱们从Leader故障中恢复以保证进度。咱们使用一些视图变化相关的技术,好比Keidar and Dolev协议[10]。不一样于他们的协议,咱们不使用群组通讯来操做。若是一个新的服务器加入或者离开(可能由于崩溃),咱们不会引起一个视图变动,除非那是一个表明Leader崩溃的事件。

 

4. 协议

Zab协议包括两个模式:恢复(recovery)和广播(broadcast)。当服务启动或者在Leader故障之后,Zab过渡到恢复模式。在一个Leader出现而且有多数服务器与它进行同步后,恢复模式结束。同步包括保证Leader和新的服务器保持一致的状态。

 

当Leader有了多数已同步的Follower,它就能够开始广播消息。就像咱们在简介当中提到的,ZooKeeper服务使用一个Leader来处理请求。Leader就是那个经过初始化广播协议来处理广播的服务器,其余除了Leader之外的服务器要发送消息的话得先把它发送给Leader。经过从恢复模式选出来的Leader看成处理写请求和协调广播协议的Leader,咱们消除了从写请求Leader到广播协议Leader的网络延迟。

 

若是一个Zab服务器在Leader的广播阶段上线,这个服务器会以恢复模式启动,查找并与Leader进行同步,而后才开始参与消息广播。服务会保持在广播模式直到Leader故障或者它再也不具备多数集合的Follower。任意Follower的多数集对Leader来讲都是足够的,这样服务就能保持活跃。好比,一个Zab服务由3台服务器组成,其中1台是Leader,另外2台是Follower,而后系统进入广播模式。若是其中一个Follower死亡,也不会形成服务中断,由于Leader仍然具备一个多数集。若是这个Follower恢复而另外一个死亡,那样也不会形成服务中断。

 

4.1    广播

在原子广播协议运行时咱们使用的协议叫作广播模式,就像一个简单的二阶段提交(two-phase commit)[7]:Leader提出一个请求,收集投票,最后提交。图2阐述了咱们协议的消息流。咱们能简化二阶段提交协议由于咱们没有中断(aborts),Follower要么接受Leader的提案,要么放弃这个Leader。没有中断也意味着咱们能够在多数服务器回应(ack)时当即提交,而不用等到全部服务器响应。这个简化的二阶段提交它本身是不能处理Leader故障的,因此咱们会添加恢复模式去处理Leader故障。

 

这个广播协议使用FIFO(TCP)通道进行全部通讯。经过使用FIFO通道,维持有序保证就显得很是简单。消息经过FIFO通道被顺序地派发,只要消息能以它们被接收到的顺序被处理,顺序就能保证。

 

Leader为被交付的消息广播一个提案。在发起一条提案消息以前,Leader为它分配一个单调递增的惟一id,叫作zxid。由于Zab保证了因果有序,发送的消息也会在它们的zxid上保持有序。经过将包含信息的提案附着到每一个Follower的输出队列中,并经FIFO通道发送给Follower,以此来进行广播。当一个Follower收到一个提案,会将它写入磁盘,若是有可能的话就批处理它们,而后当提案写到磁盘媒介时发送回应给Leader。当一个Leader从多数Follower中收到回应(ACK)时,它会广播一条提交(COMMIT)指令而后在本地提交消息。当Follower从Leader处收到提交(COMMIT)指令时也提交消息。

 

注意到若是Follower之间互相广播ACK的话,Leader并非必定要发送COMMIT指令。这个改动不只会提高网络负载,它也须要比简单星状拓扑(simple star topology)更完整的通讯图,星状拓扑从TCP链接的角度来看更容易管理。保持这个图而且追踪ACK信息,在咱们的实现中认为这是不可接受的复杂度。

 

4.2    恢复

这个简单广播协议在Leader故障或者失去多数的Follower前都能良好工做。为了保证进度,一个选举新Leader和使全部服务器进入正确状态的恢复过程就显得颇有必要。对于Leader选举,咱们须要一个高成功几率的算法以保障活性。这个Leader选举协议不止让Leader知道本身是Leader,也要让多数服务器赞成这个决定。若是一个选举阶段不能正确完成,服务器不会进行下一步工做,它们最终会超时,而后从新开始Leader选举。咱们有两种不一样的Leader选举实现。若是还存在多数运行正常的服务的话,最快的Leader选举只须要几百毫秒就能完成。

 

在恢复阶段完成过程的一段时间里,会有一些提案正在被传送。这些提案的最大值是个可配置选项,但默认值是1000。为了保证这个协议在Leader故障时也能正常工做,咱们须要两个具体的保证:咱们不会忽略(forget)任何已递交的消息,也不会保留已经跳过(skipped)的消息。

 

若是一条消息在一台机器被交付,那么就应该在全部机器上被递交,哪怕那台机器出现故障。这种状况很容易出现,若是Leader提交了一条消息而后在COMMIT指令到达其余机器前出来故障,如图3所示。由于Leader提交了这条消息,客户端应该已能在这条消息中看到事务的结果,因此该事务最终要发送给全部其余服务器,所以客户端才能看到一个一致的视图。

 

相反地,一条跳过的消息要保持被跳过。一样这个状况也很容易出现,若是Leader生成了一个提案而后在任何人看到以前就出现了一些故障。好比在图3 ,没有其余服务器看到编号为3的提案,因此在图4中,当服务器1从新上线并从新集成到系统中时,它须要保证把编号3的提案丢弃。若是服务器1成为新的Leader,并在消息100000001和100000002被提交后提交消息3,这就违反了咱们的顺序保证。

 

经过对Leader选举协议的简单调整就能解决记住已交付消息的问题。若是这个Leader选举协议保证新的Leader具备多数服务器中最高的提案编号,那么新选出来的Leader就会拥有全部已提交的消息。在提出新提案消息以前,新选出来的Leader要保证全部记录在它事务日志中的消息已经被提出并被多数服务器经过。注意到新的Leader是处理最高zxid的服务器进程刚好是一个优化,这样新选出来的Leader不须要从Follower群组中找到哪一个拥有最高的zxid,而且去拉取(fetch)丢失的事务。

 

全部正常运行的服务器要么是一个Leader,要么就是这个Leader的Follower。Leader保证它的Follower能看到全部提案,而且全部已经过的提案都被交付。它经过把新链接的Follower还没看到的PROPOSAL指令放到队列里,而后将这些提案到最新提案的COMMIT指令也放到队列中来实现这个目的。当全部这样的消息都放到队列中后,Leader将Follower添加到之后的PROPOSAL和ACK的广播列表。

 

跳过那些被提出可是没有被交付的消息一样也很容易处理。在咱们的实现中,zxid是个64位(64-bits)的数字,其中低32位(32-bits)看成一个简单的计数器。每一条提案增长那个计数。高32位表明轮次(epoch)。每次新的Leader选出,它会从它日志中最高的zxid里提取出轮次,增长轮次,并用新轮次和计数0组成的zxid。使用轮次去标记Leader的变动而且让多数服务器认为一台服务器是当前轮次的Leader,可让咱们避免多个Leader使用同一个zxid提出不一样提案的状况。使用这个模式的其中一个好处就是咱们能跳过当Leader故障时的一些实例,所以能够加速和简化恢复过程。若是一台服务器重启并带着一条没被交付的上轮消息,它不能成为一个新的Leader,由于任一多数服务器集,都有一个具备新轮次,即有更高的zxid的提案的服务器。当这个服务器以Follower的身份链接,Leader检查Follower的最大提案轮次的最后提交的消息,并让Follower清空它的事务日志(也就是忽略)直到当前轮次。在图4中,当服务器1链接上Leader,Leader告诉它从事务日志中清除提案3。

 

5. 结束语

咱们快速地实现这个协议,而且在生产环境上证实了它的强健性(robust)。更为重要的是,咱们也实现了的高吞吐量低延迟的目标。因为特殊的4倍于数据包传输的延迟与服务器的数量无关,在非饱和(non-saturated)的系统中,延迟以毫秒计。爆发性负载也获得适当的处理,由于当收到多数提案的回应时,消息就会被提交。缓慢的服务器不会影响爆发性吞吐量,由于快速的多数服务器能够在不包括慢速服务器的前提下响应消息。最后,因为Leader一收到多数Follower回应就提交消息,因此只要大多数和机器运行正常,Follower故障并不会影响性能甚至是吞吐量。

 

因为该协议的高效实现,咱们有一个实现系统达到了每秒数万到数十万操做,读写工做负载比例达到2:1甚至更高。这个系统是目前在生产环境中使用,而且它是用于大型应用的,好比雅虎crawler和雅虎广告系统。

 

致谢

咱们要感谢审稿人的宝贵意见,感谢Robbert van Renesse在关于Zab的离线讨论中帮咱们阐明了几个观点。

 

6. 引用文献

[1] K. Birman and T. Joseph. Exploiting virtual synchrony in distributed systems. SIGOPS Oper. Syst. Rev., 21(5):123–138, 1987. 

[2] K. P. Birman and T. A. Joseph. Reliable communication in the presence of failures. ACM Trans. Comput. Syst., 5(1):47–76, 1987.

[3] X. D ́efago, A. Schiper, and P. Urb ́an. Total order broadcast and multicast algorithms: Taxonomy and survey. ACM Comput. Surv., 36(4):372–421, 2004. 

[4] D. J. DeWitt, R. H. Katz, F. Olken, L. D. Shapiro, M. R. Stonebraker, and D. Wood. Implementation techniques for main memory database systems. SIGMOD Rec., 14(2):1–8, 1984. 

[5] R. Friedman and R. van Renesse. Packing messages as a tool for boosting the performance of total ordering protocols. In HPDC, pages 233–242, 1997. 

[6] D. Gawlick and D. Kinkade. Varieties of concurrency control in ims/vs fast path. IEEE Database Eng. Bull., 8(2):3–10, 1985. 

[7] J. Gray. Notes on data base operating systems. In Operating Systems, An Advanced Course, pages 393–481, London, UK, 1978. Springer-Verlag. 

[8] R. Guerraoui, R. R. Levy, B. Pochon, and V. Quema. High throughput total order broadcast for cluster environments. In DSN ’06: Proceedings of the International Conference on Dependable Systems and Networks, pages 549–557, Washington, DC, USA, 2006. IEEE Computer Society.

[9] http://hadoop.apache.org/zookeeper. Zookeeper pro ject page, 2008. 

[10] I. Keidar and D. Dolev. Totally ordered broadcast in the face of network partitions. In D. R. Avresky, editor, Dependable Network Computing, chapter 3, pages 51–75. Kluwer Academic, 2000. 

[11] J. C. Knight and N. G. Leveson. An experimental evaluation of the assumption of independence in multiversion programming. IEEE Trans. Softw. Eng., 12(1):96–109, 1986. 

[12] L. Lamport. The part-time parliament. ACM Trans. Comput. Syst., 16(2):133–169, 1998. 

[13] F. B. Schneider. Implementing fault-tolerant services using the state machine approach: a tutorial. ACM Comput. Surv., 22(4):299–319, 1990.

相关文章
相关标签/搜索