Zookeeper:面向互联网的无等待协调
概要
在本文中,我们描述了Zookeeper,一个分布式应用协调程序。因此Zookeeper是关键基础设施的一部分,Zookeeper目标是提供简单高可用的核心以在客户端构建更复杂的协调单元。在复重复的集中服务中,它结合了群消息,共享注册和分布式锁服务。Zookeeper公开的接口具有寄存器无等待等特性,具有类似分布式文件系统的缓存失效机制的事件驱动机制,从而提供了简洁的方式,同时增强了协调服务。
Zookeeper 接口使用了高可用服务实现。添加了无等待的特性,Zookeeper提供了每一个客户端保证请求的FIFO(先入先出)执行,并且线性化所有能改变Zookeeper状态的请求。这个设计决策允许实现高可用的处理流水线同时读请求可以被本地服务处理。我们展示了工作负载,2:1到100:1的读写比,Zookeeper可以处理数万到十万个事务。这种性能允许Zookeeper被客户端应用程序广泛使用。
1 引言
大尺度的分布式系统需要不同形式的协调。配置是协调最基本的形式。在这个最简单的形式中配置只是系统进程中可操作的参数,而更复杂的系统有动态的配置参数。组成员和领导选举在分布式系统中也是共通的:通常程序需要知道那个程序是存活的以及这些程序负责什么。锁构成了一个强大的协调原语,它实现了对关键资源的互斥访问。
一个协调的方法是对于每一个不同的协调需要开发服务,例如,Amazon Simple Queue Service关注与特定的队列。还为leader选举开发了特定的服务。实现了更强大原语的服务可以用来实现不那么强大的原语。例如,Chubby是一个强同步保证的锁服务。锁可以用来实现leader选举组成员等。
在计划我们的协调服务时,我们不再服务端实现特定的原语。相反我们选择公开一个API,让应用程序开发人员能够实现自己的原语。这样的选择导致了协调内核(coordination kernel)的实现,该内核支持新的原语,而不需要更改服务核心。这种方法能够适应应用程序需要的多种协调。而不是将开发人员限制在固定的原语中。
当设计zookeeper的API,我们放弃了阻塞原语比如锁。协调服务的阻塞原语可能导致大量的其他问题,缓慢有故障的客户端会影响速度更快的客户端。如果处理请求依赖于对于其他客户端的于响应和失败探测,那么服务的视线将会变得更加复杂。因此我们的zookeeper系统实现了一个API,可以想文件系统一样按层次组织简单的无等待(wait-free)的数据对象。事实上 ,zookeeper API类似于任何其他的文件系统,只看API的锁签名,Zookeeper看起来像没有锁方法打开和关闭的Chubby。然而实现无等待数据对象,将Zookeeper与基于阻塞原语(如锁)的系统显著区分开来。
Maurice Herlihy 提出的“可扩展性层次结构” (Scalability Hierarchy) 主要用于衡量并发数据结构和算法在并发环境中的可扩展性。
- 无等待:等待自由的算法保证在有限步内完成操作,任何线程都不会因为其他线程的暂停或延迟而被阻塞。这是并发算法中最强的进度条件,因为它确保每个线程都能独立完成操作,具有最好的公平性和响应时间。
- 无锁:无锁的算法允许至少一个线程在有限步内完成操作,但不保证所有线程都能快速完成。它确保系统整体的进展,避免死锁和优先级倒置问题,但在高竞争情况下可能会导致部分线程的延迟。
- 有锁:有锁算法是最弱的层次结构,它仅在没有竞争的情况下保证操作的完成。当多个线程竞争访问时,有可能导致部分线程被阻塞或重试,因此无法确保系统进度。
虽然无等待特性对于执行和容错性是很重要的,但这不足以进行协调。我们必须对于操作提供顺序保证。事实上我们已经发现对于所有的操作和保证所有操作的FIFO和顺序写,能够支持服务的有效实现,并且足够实现我们应用程序感兴趣的协调原语。事实上,我们可以用我们的API为任何数量的过程实现共识,根据Herlihy的层次结构,Zookeeper实现了一个通用对象,根据herlihy的层次结构,Zookeeper实现了一个通用的对象。
Zookeeper服务服务包含了一个服务整体,它使用复制实现高可用和性能。它的高可用允许应用包含大量的进程的应用程序能使用这种协调内核来管理所有的方面。我们使用一个简单的流水线架构来实现zookeeper,这允许我们可以在实现低延迟时同步处理数百或数千个未完成的请求。这样的管道自然能够以FIFO的顺序从单个客户端执行操作。保证了FIFO的执行顺序,使得客户端可以异步的提交操作。使用异步的操作,客户端可以同时执行多个未完成的操作。这个特征是很合适的,例如,当新的客户端变成leader时,它必须操作元数据并相应的更新它。如果不能同时执行多个未完成的操作,初始化时间可能达到秒级而不是亚秒级。
为了确保更新操作满足线性一致性,我们实现了基于leader的原子广播协议,ZAB。然而,Zookeeper应用的典型工作负载是以读操作为主导的,因此需要扩展读吞吐量。在Zookeeper中,服务进程本地处理读操作,并且我们不用Zab协议来编排他们。
客户端测的缓存数据是一个很重要的技术以提高读的性能。例如,对于一个进程来说缓存当前的leader标识符而不是每次需要知道leader时都探测Zookeeper是有用的。Zookeeper使用了watch机制,使客户端可以缓存数据,而无需直接管理客户端缓存。通过这种机制,客户端可以监听给定数据的更新,并且在更新时收到通知。Chubby直接管理客户端缓存。它阻止更新以使缓存正在更改的数据的所有客户机的缓存无效。在这种设计下,如果这些客户端中的任何一台出现较慢或者故障则会延迟更新。Chubby使用租约以防止客户端的错误无限期的阻塞系统。然而租约只是限制了缓慢或者错误客户端的影响,但是zookeeper的watch完全避免了这个问题。
在这个论文中我们讨论了我们Zookeeper的设计和实现。通过Zookeeper我们能够实现应用程序所需的协调原语,即便只有写是线性化的。
总结,这个paper我们主要的贡献是:
- 协调核心:我们提出了一个无等待的协调服务,具有冠松的一致性保证,用于分布式系统。现实中我们描述了我们的设计并且实现了协调核心,并且用在了我们的许多关键应用中以实现多样的协调技术
- 协调方法:我们展示了Zookeeper如何使用以构建高级的协调原语,即使是分布式系统中经常使用的阻塞和强一致的原语。
- 协调经验:我们分享了一些方法使用Zookeeper和评估它的性能
2 Zookeeper 服务
客户端使用Zookeeper客户端库通过客户端API提交请求到Zookeeper。此外,为了通过客户端API暴露Zookeeper服务接口,客户端库也管理客户端和Zookeeper服务之间的网络链接。
在这一章我们首先提供了Zookeeper服务的高级试图。我们讨论客户端使用和Zookeeper交互的API。
术语:在这个文章中我们使用客户端指代Zookeeper服务的用户,服务指代提供Zookeeper的进程,znode代表Zookeeper数据中的内存数据节点,这些数据节点以被称为数据树(data tree)的层次命名空间进行组织。客户端在连接到时建立会话,并且获取一个会话句柄,它们通过会话句柄发出请求
2.1 服务总览
zookeeper提供了数据节点(znode)集的客户端抽象,根据层次命名空间组织。znode在这个中是客户端通过ZookeeperAPI操作的数据对象。分层命名空间经常被用在文件系统。这是组织数据对象的理想形式,因此用户已经习惯了这种抽象,而且它可以更好的组织应用元数据。为了引用指定的节点,我们使用标准的UNIX文件系统路径标记。例如,我们使用/A/B/C代表节点到znode的c的路径,C有B作为父结点B有A作为父结点。所有的znode存储数据,并且处理可临时znode所有znode都有子节点。
图1描述了zookeeper的分层命名空间。
客户端可以创建的znode分为俩种
- 常规:客户端通过创建和删除清晰的操作常规znode
- 客户端创建这样的znode,并且要么明确的删除它们要么当创建的会话到期的时候自动删除它们(故意或者由于失败)
此外,当创建一个新的znode,客户端可以设置连续的标志。使用顺序标志集创建的节点在其名称后面附加一个单调递增计数器的值。如果n是新的znode并且p是父znode。则n的序列盒子永远不会小于在p下创建的任何其他序列znode的名称中的值。
Zookeeper实现了watch机制以允许客户端接受变化的时序请求而不用依赖轮询。当客户端发出带有watch标志的读取操作的时候,除了服务器承诺在返回的信息发生变化的时候通知客户端,操作正常完成。watch是与会话相关的一次性出发器;一旦出发或者会话关闭,它们将会被注销。watch表示发生了更改,但不提供更改。比如,如果客户端在"/foo"
更改俩次之前发出了请求数据getData("/foo",true)
,客户端将获得一个watch事件,告诉客户端“/foo”的数据已经更改。同时,像连接丢失事件这样的会话事件也会发送到监视回调,以便客户端知道监视事件可能会延迟。
数据模型。Zookeeper的数据模型是基本的文件系统与一个简单的API,只有只有全量的读和写,或者具有层次结构的k-v表。层次结构的命名空间被用来分配不同应用命名空间的子树,并且设置这些子树的访问权限。我们也在客户端测开拓了目录的概念以构建高级别的级别的原语,我们将会在2.4章看到。
和文件系统中的文件不同,znode不是被设计给常规数据存储使用的。相反,znode映射到客户端程序的抽象,通常用于应对协调目的的元数据。为了描述,在图一中我们有俩个子树,一个是应用1的,另一个是应用2的。应用程序1的子树实现了一个简单的组成员协议:每个客户端进程$p_i$在/app1下创建一个znode$p_i$,只要进程在运行,它就会持续存在。
虽然znode不是为一般的数据库存储而设计的,Zookeeper允许客户端存储一些可以用作元数据或者分布式计算的配置数据,它对于刚开始了解其他服务当前处于leader的应用服务器来说很有用。为了接近这个目标,在当znode空间中的当前已知的位置中我们可以有一个当前的leader写入这些信息。znode还将元数据与时间戳和版本计数器相关联。它允许客户端追踪znode的变化和基于znode的版本执行有条件的更新。
会话。客户端连接了Zookeeper并且初始化会话。会话有一个相关的超时。如果在超过timeout时间接收不到任何的来自它会话的请求,那么Zookeeper将这个客户端视为故障。当客户端明确的关闭会话句柄或者Zookeeper探测到客户端故障,会话就结束了。在会话中,客户端观察到连续不断的状态改变,客户端观察反应其操作执行一系列的变化。会话使客户端能够在zookeeper集成中透明低从一个服务器移动到另一个服务器,从而实现zookeeper服务的跨服务持久化。
2.2 客户端API
我们提出了相关的zookeeper API相关的子集,并且讨论每个请求的语义:
- create(path,data,flags):在路径path上创建一个znode,存储data[]在其中,并且返回一个新的znode。flag使客户护短能够选择znode的类型,常规(regular),临时(ephmeral)并且设置连续的flag;
- delete(path,version):删除path下的znode,如果znode是期望的版本
- exists(path,watch):如果znode和路径名已存在返回true,否则返回false。watch flag允许客户端在znode上设置一个watch
- getData(path,watch):返回数据和元数据,比如版本信息,相关联的znode。watch标志的工作方式和exists()的工作方式相同,除了如果znode不存在的话zookeeper不设置watch
- setData(path,data,version):如果版本号是当前的znode版本,写入data[]到znode 的path中 。
- getChildren(path,watch):返回znode子节点的名字集合
- sync(path):等待在操作开始时所有待处理的更新传播到客户端所连接的服务器。当前路径被忽略。
通过API所有点方法有俩个同步和非同步的版本可用。当执行单一的zookeeper操作并且它没有当前的并发任务需要执行时应用使用同步API,它会进行必要的zookeeper调用并进行阻塞。然而非同步的API允许应用有多个未完成的Zookeeper操作,以及并行的执行这些任务。Zookeeper客户端保证了每个操作对应的回调是按顺序调用的。
注意,Zookeeper不适用句柄来访问znode。每个请求都包含正在操作的znode的完整路径。这种选择不仅简化了API(没有 打开和关闭方法),同时也消除了额外的服务需要维护的状态。
每个更新方法都一个期望的版本号,它支持条件更新的实现。如果真正的版本号不匹配预期的版本号,更新失败,出现意外的版本错误。如果版本号是-1,它就不会执行版本的检查。
2.3 Zookeeper 保证
Zookeeper有俩个基本的顺序保证:
- 线性写:所有的更新zookeeper状态的请求是线性的根据优先级的。
- 先进先出的的客户端顺序:所有的来自给定客户端的请求都是根据客户端发出的顺序来执行的。
注意我们对线性写的定义是和最初Herlihy提出的线性写不同的,我们吧它叫做A-linearizability(异步的线性化asynchronous linearizability)。在原来Herlihy对于线性的定义中,客户端一次只能有一个未完成的操作(客户端是一个线程)。而我们允许客户端有多个待处理的操作,因此,我们可以选择保证同一客户端的未完成操作没有特定顺序,或者保证先入先出(FIFO)顺序。我们选择了后者作为我们的特性。重要的是要注意,所有的线性化对象的结果也适用于A-linearizable对象,因为满足a-linearizable线性化的系统也满足线性化。zookeeper在每个副本上处理读请求。这使得服务随着服务器添加到系统中线性扩展。
为了展示俩个保证的交互,考虑如下的设想。由多个进程组成的系统选举出一个leader来指挥工作进程。当新的leader接管了系统,它必须更改大量的配置参数,并且完成后通知其他进程。我们有俩个重要的要求:
- 因为新领导人做出改变,我们不想其他进程使用改变之前的配置。
- 如果新的leader在配置完全更新之前死了,我们不希望进程使用这个不分的配置。
注意到如Chubby提供的分布式锁,将会有助于满足第一个要求,但不足以满足第二个需求。对于Zookeeper,新的leader可以指定一个路径,就像一个ready的znode;其他的进程将只能使用该znode存在的路径。新的leader通过删除ready来改变配置,更新各种配置节点,并且创建ready。所有的这些更改都可以被流水线化并发布,以快速更新配置状态。尽管潜在的修改操作延迟大概是2毫秒,一个新的leader必须更新5000个不同的znode,如果一个接一个的发出请求将要花费10s。通过异步的发出请求,请求将花费不到1秒钟的时间。因为顺序的保证,如果进程看到了ready znode,它必须也看到了leader所做的所有配置变更。如果新leader在ready znode创建完之前死了,其他的进程知道配置还没有完成,所以不使用它。
上述方案仍然存在一个问题:如果一个进程在新的领导者开始进行更改之前看到 ready
节点存在,然后在更改进行中开始读取配置,会发生什么?这个问题通过通知的顺序保证得到了解决:如果一个客户端在监视变化,客户端将在看到系统状态变化的新状态之前,先看到通知事件。因此,如果读取 ready
znode 的进程请求接收有关该 znode 更改的通知,它将在读取任何新配置之前,看到通知来告知客户端发生了更改。
在 ZooKeeper 的 Watch 机制中,当节点发生变化时,Watcher 会被触发,但是 不会 直接将改变的内容(如节点的新数据或状态)推送给客户端。相反,Watcher 只会通知客户端某个事件发生了。客户端需要根据这个通知去查询节点的最新状态。
另一个问题产生了,当客户端除了Zookeeper之外还有自己的通信通道时,可能会出现另一个问题。例如,考虑俩个客户端A和B,它们在Zookeeper中有共享的配置,并且通过一个共享的通道进行通信。如果A修改 了Zookeeper中的共享配置并且通过共享的沟通渠道告诉B这个变化,B希望重新读取配置时看到更改。如果B的Zookeeper副本稍稍落后于A的,它可能就看不到新的配置。使用上述的保证,B可以通过在重新读取配置之间发出写操作来确保它看到最新的信息。为了处理这个场景,Zookeeper提供了sync请求:当后面跟着读时构成慢读(slow read)。sync使服务在处理读请求之前,应用所有挂起的写请求,而无需完整写入的开销。这个原语类似ISIS中的flush原语。
Zookeeper也有俩个活跃性和持久性保证:如果大部分的Zookeeper服务是活动的并且通信服务时可用,以及如果Zookeeper服务成功响应请求,那么只要代发多数服务最终能够恢复这个改变就会在任何数量的故障中存在。
2.4 原语的例子
在这一章节,我们展示了如何使用Zookeeper API来实现更强大的原语。Zookeeper对这些更强大的原语一无所知,因为它们完全是使用客户端的Zookeeper API来实现的。 一些通用的原语比如组成员和配置管理也是无等待的。对于其他情况,比如“rendezvous”(集合点),客户端需要等待某个事件的发生。尽管Zookeeper API是无等待的,我们也可以有效的使用Zookeeper实现阻塞的原语。Zookeeper的有序性保证允许对系统状态有效的推理,watch使得更高的等待成为可能。
配置管理 Zookeeper可以被用来实现分布式应用中的动态配置。最简单的形式配置存储在znode $z_c$中。经常以完整的路径名$z_c$启动。通过读取$z_c$和将watch的标志位设置为true,启动的进程可以观测它的配置。如果$z_c$中的配置被更新,进程将会收到通知并且读取新的配置。
注意在这个方案中,与其他大多数使用watch的方案一样,watch是用来保证进程能知道最新的信息。举个例子,如果进程watch$z_c$,被通知$z_c$发生了变化,并且在它可以对$z_c$发出读操作之前,$z_c$又发生了三次变化,进程不会再接受三次通知事件。这不会影响进程的行为,因为这三个事件只是通知进程它已经知道的事情:$z_c$所拥有的信息是陈旧的。
当客户端与 Zookeeper 的连接断开时,临时节点会被自动删除。这一特性使得 Zookeeper 能自动感知服务的状态变化。例如,当服务实例失联或崩溃时,Zookeeper 会删除该服务对应的临时节点,从而帮助系统保持准确的服务列表。
集合点(rendezvous) 在分布式系统中,一些时候,最终的系统配置看起来是什么样子并不总是清楚的。例如,一个客户端可能想要启动master 进程和几个工作进程,但是启动进程是由调度程序完成的,所以客户端不知道提前的信息,例如,它可以为工作进程提供链接到master进程的地址和端口。我们在Zookeeper中使用rendezvous znode来处理这个场景,它是一个被客户端创建的znode。客户端传递$z_r$的完整路径作为主进程和工作进程的启动参数。当master启动,它在$z_r$中填充有关它正在使用的地址和端口信息。当工作节点启动,它读取$z_r$同时设置watch为true。如果$z_r$还没有被填充,当$z_r$更新时,工作节点等待收到通知。如果$z_r$是临时节点,master和工作节点进程可以watch$z_r$,以在客户端结束的时候删除和清理它们。
组成员 (group membership)我们利用了临时节点的特性去实现了组成员。具体来说,我们使用了临时节点使得我们可以看到被创建节点的会话状态这一事实。我们从指定一个znode开始,$z_g$代表组。当该组中的进程成员启动,它在$z_g$下创建了临时子节点。如果每个进程有独特的名字或者标识符,那么这个名字可以被用来作为子节点的znode名字。否则进程将会创建一个带有顺序(SEQUENTIAL)标志以获得独立的名称指派。进程可能设置进程信息在子节点的znode中,例如进程所使用的地址和端口。
在子节点在$z_g$下创建之后,进程正常启动。它不需要什么其他的。如果进程失败或者结束,那个在$z_g$下对应的znode也会自动被删除。
因为这里多次改变只能通知一次所以监听到发生了修改就需要再设置监听,然后才能请求新的修改。这样才不会乱序。
进程可以通过简单列出$z_g$下的子节点的观测组信息。如果进程想要监听组成员的修改,当接收到了更新的信息,进程可以设置监听标志为true并且刷新组信息(总是设置监听标志为true)
简单锁 虽然zookeeper不是一个锁服务,但它可以用来实现锁。应用使用Zookeeper通常使用同步原语来定制它们的需求,比如在上面展示的。这里我们展示如何用Zookeeper实现锁以展示它可以实现各种各样的通用同步原语。
最简单的锁的实现使用 lock file。锁由一个znode表示。为了获取锁,客户端使用EPHEMERAL 标志创建指定的znode。如果创建成功,那么客户端持有锁。不然,客户端可以watch带有标志的znode,如果当前的leader死亡,则会收到通知。当客户端死亡或者指定删除znode锁才会被释放。一旦观测到znode被删除,其他等待锁的客户端再次常熟获取锁。
虽这个简单的锁协议可以可以工作,它由一些问题。首先,它收到羊群效应的影响。如果有许多客户端尝试获取锁,当锁被释放它们将会竞争锁,即便只有一个客户端才能获取到锁。其次,它只实现了排它锁。
没有羊群效应的简单锁 我们定义了一个锁 znode l来实现这样的锁。我们将所有请求锁的客户端组成一行,并且每个客户端按照请求到达的顺序获取锁。因此,希望获得锁的客户端做以下操作:
- 创建一个临时顺序节点,路径为
l + "/lock-"
。这个节点代表当前尝试获取锁的客户端。使用 临时节点 是因为当客户端失去连接时,EPHEMERAL
表示这个节点是临时的,SEQUENTIAL
表示这个节点的名称会附加一个序列号。- 获取在路径
l
下的所有子节点(即所有的锁节点),并将它们存储在集合C
中。第二个参数false
表示不设置监听(watch),即当前只获取节点信息,而不对后续变化进行监听。- 检查当前创建的节点
n
是否是集合C
中序号最小的节点。如果是,说明当前客户端获得了锁,可以安全地退出并继续执行后续操作- 找到在集合
C
中,序号位于n
之前的节点p
。这个节点代表了在n
之前获取锁的其他客户端。- 如果节点
p
存在,并且为其设置了监听(watch),那么当前客户端会等待p
的删除事件。这意味着当前客户端在等待锁的释放。- 如果节点
p
不存在,或者p
被删除,回到第 2 行重新获取子节点信息,再次检查当前节点n
是否是最小的序号节点。这是为了处理可能有新的锁请求进来并影响当前锁状态的情况。
Lock
1 n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL)
2 C = getChildren(l, false)
3 if n is lowest znode in C, exit
4 p = znode in C ordered just before n
5 if exists(p, true) wait for watch event
6 goto 2
Unlock
1 delete(n)
在Lock的第1行中使用sequence标志,将客户端获取锁的尝试与所有其他尝试进行排序。如果客户端的zonode在第三行有最低的序号,客户端持有锁。否则,客户端会等待被删除的 znode,该 znode 要么已经持有锁,要么将在该客户端的 znode 之前获得锁。通过只watch只观察客户端之前的znode,在所被释放或者锁请求被放弃时,我们通过只唤醒一个进程避免了羊群效应。一旦客户端监听的znode消失了,客户端必须检查它现在是否还持有锁。(之前的锁请求可能被抛弃了,并且有一个序列号较低的znode仍在等待或持有锁。)
释放锁和删除表示锁请求的znode n一样简单。通过在创建的时候使用EPHEMERAL标志,崩溃的进程将自动清理任何锁请求或者释放任何它可能拥有的任何锁。
总之,这个锁模式有以下优点:
- znode的移出只会导致一个客户端被唤醒,因此每个znode只会被一个其他的客户端观测到,因此我们不再会有羊群效应;
- 没有轮询或者超时
- 因为我们实现锁的方式,我们可以通过浏览Zookeeper的数据节点看到锁竞争,离开锁的的数量。并且debug锁的问题。
读写锁 为了实现速写锁我们稍微改变了锁的步骤,区分了读锁和写锁的程序。解锁的程序和全局锁的例子中一样
在以下代码中,写的优先级是比读来的更高的。因此读有概率被饿死。
Write Lock
1 n = create(l + “/write-”, EPHEMERAL|SEQUENTIAL)
2 C = getChildren(l, false)
3 if n is lowest znode in C, exit
4 p = znode in C ordered just before n
5 if exists(p, true) wait for event
6 goto 2
Read Lock
1 n = create(l + “/read-”, EPHEMERAL|SEQUENTIAL)
2 C = getChildren(l, false)
3 if no write znodes lower than n in C, exit
4 p = write znode in C ordered just before n
5 if exists(p, true) wait for event
6 goto 3
稍微的修改了之前的锁程序。写锁只是在命名上有所不同。因此读锁可以共享,第三四行稍有不同。因为只有较早的写锁znode才会组织客户端获取读锁。当几个客户端在等待读锁的时候,当序列号较低的写znode被删除时,我们可能会出现羊群效应。事实上这是一种期望的行为,因为它们持有了锁所以这些读客户端被释放了。
Double Barrier 的基本思想可以分为两个步骤:
第一次屏障(前屏障):
- 当参与者到达第一个屏障时,它们会被阻塞,直到所有参与者都到达此屏障。所有参与者在此处等待,以确保每个参与者都准备好开始执行下一阶段的任务。
第二次屏障(后屏障):
- 一旦所有参与者都到达第一次屏障,它们就可以继续执行某些操作。当所有参与者完成这些操作后,它们会到达第二个屏障。在这个屏障,所有参与者再次被阻塞,直到所有人都到达此屏障。这样,所有参与者就可以确保在继续下一轮操作之前,它们在某个点上是同步的。
双重屏障 双屏障使得客户端能够同步计算的开始和结束。当被屏障阈值定义的足够多的进程都加入了屏障;进程开始他们的计算并且一旦完成就离开屏障。在Zookeeper中我们使用znode表示屏障,称为b。每个进程p通过在入口处创建一个znode作为b的子节点向b注册,当子节点准备离开,取消注册,删除子节点。当b的子znode的数量超过屏障阈值。进程可以进入屏障。当所有进程都删除了它们的子进程时,进程可以离开屏障。我们使用监听有效的等待进入和退出需要满足的条件。为了进入,进程会监视一个名为 b 的子节点是否存在,该子节点是由使子节点数量超过障碍阈值的进程创建的。为了离开,进程会监视某个特定的子节点是否消失,并且只有在该 znode 被移除后才会检查退出条件。
3 Zookeeper应用
我们现在描述一些使用了Zookeeper的应用和一些简单的解释如何使用它。我们以粗体展示每个例子的原语。
雅虎的 Fetch Service 是一个用于处理和获取大规模数据集的分布式服务,主要用于支持雅虎的各类产品和服务。Fetch Service 的设计目标是高效、可扩展,并能够处理大量的请求和数据。
获取服务 Crawling是搜索引擎很重要的一部分,雅虎爬取了数十亿的web文档。Fetching service(获取服务 FS)是雅虎爬虫的一部分,现已投产。从本质上讲,它具有命令页面获取进程的master进程。master为访问者提供了配置,访问者写入它们状态和健康的回复。使用ZookeeperFS的主要好处是可以从master的失败中恢复过来,保证失败时的可用性和将服务端和客户端解耦,通过读取他们在Zookeeper的状态,允许它们将请求重定向到蒋康的服务器。因此,FS主要使用Zookeeper管理配置元数据,尽管它也使用Zookeeper来选master(leader选举)。
图2:ZK在获取服务中的工作负载。每个点代表一秒钟的样本。
图2展示了FS使用的Zookeeper服务的读写流量经过三天的时间。为了生成这张图,我们统计了这段时期每秒的操作数量,每个都代表那一秒的操作数量。我们观察到比比写流量来的更高。当操作的速率大于1000次每秒的时候,读写比例在10:1-100:1之间变化。这个工作负载中的读操作是getData()、getChildren()和exists(),按流行程度的递增顺序排列
雅虎的 Katta 是一个开源的分布式搜索解决方案,主要用于处理大规模的文本数据。Katta 是构建在 Apache Lucene 和 Apache Hadoop 基础上的,旨在提高搜索性能和可扩展性。
Katta Katta是一个分布式的索引器它使用了Zookeeper进行协调,这是一个非雅虎的应用程序。Katta使用分片来划分索引任务。主服务分配分片给从服务并且跟踪进度。从服务可能会失败,因此主服务必须在从服务器来往时重新分配负载。master服务器也可能会失败,因此其他服务器必须准备在发生故障时进行接管。Katta使用Zookeeper监听slave服务和master的状态(组成员),并且处理master的故障切换(leader选举)。Katta也使用Zookeeper来追踪和传播分到slave的任务(配置管理)。
类似消息队列
Yahoo! Message Broker Yahoo! Message Broker (YMB)是一个分布式的发不系统。系统管理上千个主题,客户端可以向其发送消息和接受消息。主题分布在几个提供可缩放能力的服务中。每个主题都使用主备的方案,这保证了信息被复制在俩个机器以确保可靠的信息交付。组成YMB的服务使用无共享的分布式架构,这导致了协调是正确操作的必要条件。YMB使用Zookeeper来管理主体的分布(配置元数据),处理系统中机器的故障(异常探测和组成员),并且控制系统的操作
图3:Zookeeper中Yahoo! Message broker(YMB)的布局结构
图3展示了YMB中znode数据布局的一部分。每一个broker域有一个znode叫做nodes,对于每一个活动的,构成YMB服务的服务都一个临时节点。每个YMB服务在nodes下创建一个临时znode,其中包含负载和状态信息,通过Zookeeper提供组成员和状态信息。shutdown和migration prohibited这样的节点由组成服务的所有服务器进行监控并且允许对YMB进行集中控制。topics目录对于由YMB管理的每个主题都有一个子znode。这些主题znode有子节点,它们代表了每个主题的主服务器和备份服务器以及该主题的订阅者。主服务器和备份服务器的节点不仅允许服务器发现负责某个主的服务器,也允许管理leader 选举和服务崩溃。
4 Zookeeper 实现
Zookeeper通过在组成服务上的每个服务器进行复制Zookeeper的数据已实现高可用。我们假设服务器因崩溃而失败,并且这样的失败可能在之后会恢复:图4展示了Zookeeper服务的高级别的组件。收到请求后,服务器准备执行请求(请求处理器)。如果这样的请求需要服务器之间的协调(读请求),那么它们使用一致性协议(原子广播协议的实现),最终服务提交改变到Zookeeper数据库,在所有的服务器集合上完全复制。在读请求的例子中,服务简单的读取本地数据库的状态并且生成请求的响应。
复制的数据库是内存数据库包含了整个数据数。每个树中的每个znode存储默认最大1MB的数据,但是最大值是一个可以被修改的配置参数。为了有效性,我们高效的更新到磁盘中,我们强制在它应用到内存中的数据库之前写入到磁盘中。事实上,如Chubby,我们保持提交操作的重放日志(在我们例子中是写前入职),并且生成内存数据库中阶段性的快照。
每个Zookeeper服务器都服务于客户端。客户端只连接到一台服务器来提交请求。如我们早期所言读请求由每个服务器数据库的本地提供服务。修改服务状态的请求,写请求,由一致性协议处理。
作为一致性协议的一部分,写请求被转发给单个服务被叫做leader。其余的Zookeeper服务器被叫做follower,接受来自leader由状态变化组成的信息提案并且同意状态变化。
4.1 请求处理器
因为消息传递层是原子的,我们保证本地的副本不会相异,尽管任何时候一些服务器可能比其他服务器应用了更多的事物。与客户端发出的请求不同,事务是幂等的。当leader接收了写请求,它计算应用写操作时的系统状态,将其转化为一个事务,以捕捉这个事务的新状态。必须计算未来的状态,因为可能存在尚未应用到数据库的未完成事务。例如客户端执行条件setData并且请求中的版本号匹配znode正在更新的未来版本号。 生成一个setDataTXN,其中包含新的数据、新的版本号和更新的时间戳。如果发生错误,比如不匹配的版本号或者要更新的znode不存在,则生成errorTXN。
4.2 原子广播
所有改变Zookeeper状态的请求都会被转发到leader。leader执行请求并且通过Zab(原子广播协议)广播修改到Zookeeper 状态。当它交付响应的状态修改时,接受客户端请求的服务响应给客户端。Zab默认使用简单的多数法定人数来决定提案,所以Zab和Zookeeper只有大多数服务正常的情况下才能工作(即在2f+1个服务中我们可以允许f个失败)。
为了达到高吞吐量,Zookeeper试图保证请求处理管道是满的。在处理管道的不同部分可能有上千个请求。因为状态改变取决于先前状态更改的应用,Zab提供了比常规院子广播更强的顺序保证。具体来说Zab保证由leader广播的更改按顺序交付,并且前任leader的更改都会在新的leader广播其自身之前传递给它。
这里有几个细节简化了我们的实现并且给了我们很好的性能。我们使用TCP因此消息的顺序由网络维护,这简化了我们的实现。我们使用Zab选择的Leader作为Zookeeper的leader,因此相同的创建事务的进程也负责提议他们。我们使用日志以保证对天的跟踪,就好比内存数据库的写前日志一样,这样我们不用将信息写入磁盘俩次。
正常操作期间Zab按顺序传递所有消息,并且只传递一次,但是因为Zab没有持续的记录每个交付信息的id,Zab可能在恢复期间重新传递消息。因为我们使用幂等的处理,只要按顺序进行交付多次交付是可以接受的。事实上,Zookeeper需要Zab只少重新交付上次快照开始后的所有信息。
4.3 复制的数据库
每个副本都有一个备份在Zookeeper的状态内存中。当Zookeeper服务从崩溃中恢复,它需要恢复这个内部状态。重放所有的交付信息以恢复状态需要在服务运行之后花费很长的时间,所以Zookeeper使用定时快照,只需要从快照开始后重新发送消息。我们叫Zookeeper快照fuzzy快照,因为我们不需要锁定Zookeeper的状态进行快照;相反我们对树做深度的扫描读取每个znode的data和meta-data并且将他们写入到磁盘。由于产生的fuzzy可能在生成快照期间应用了一些状态修改交付的子集,结果可能不符合ZooKeeper在任何时间点的状态。然而,因为状态的修改是幂等的,我们可以应用这些修改俩次如同我们按顺序的应用状态的修改一样。例如,假设假设Zookeeper数据数的俩个节点/foo和/goo有值f1和g1俩者都是版本1,当fuzzy快照开始,状态变化流到达的形式是:(transactionType,path,value,new-version)
SetDataTXN, /foo, f2, 2
SetDataTXN, /goo, g2, 2
SetDataTXN, /foo, f3, 3
在处理这些状态变化之后,/foo,/goo有值f3和g2分别版本为3和2。然而,fuzzy快照可能已经记录了/foo,/goo有值f3和g1版本号分别为3和1。这不是Zookeeper数据数的有效状态。如果服务崩溃并且使用此快照恢复,则Zab将重新交付状态更改,最终的状态将会与服务交付之前的状态一致。
4.4 客户端-服务器 交互
当服务处理写请求,它还发送并清除与该更新对应的任何watch相关的通知。服务进程按顺序处理写操作,并且不并发处理写和读操作。这保证了通知的严格顺序性。请注意,服务器在本地处理通知。只有与客户端连接的服务器会跟踪并触发该客户端的通知。
读请求在每个服务器上本地处理。每个读请求都会被处理并且使用与服务器看到的最后一个事务相对应的zxid进行标记。这个zxid定义了读请求相对于写请求的部分顺序。通过本地处理读取,我们得到了优秀的读性能因为它只是本地服务器中的内存操作,并且它们不需要磁盘活动或者运行一致性协议。这个设计是到在以读为主的工作负载下达到全局高频性能的关键。
为了确保读取操作返回最新的值,Zookeeper引入了
sync
原语。调用sync
后,所有后续的读取操作都会在这次同步之后进行,从而保证读取的数据是最新的。
使用快速读取的缺点是它不保证读操作的优先顺序。即,读操作可能返回固定的值,即便更新操作已经提交了。并不是所有的应用程序需要优先顺序,但对于需要优先顺序的应用程序,我们也实现了sync。这个原语异步执行并且在所有挂起的写操作完成后进行排序。为了保证给定的读操作返回最新更新的值,客户端调用同步然后进行读取操作。客户端操作的FIFO顺序保证与全局同步保证相结合,使得读取操作的结果能够反映在发出同步之前发生的任何更改。在我们的实现中,我们不需要自动广播sync,因为我们使用基于leader的算法,并且我们简单防止sync操作在leader和执行同步调用的服务器之间的请求队列的结尾。为了做到这一点,follower必须确保leader仍然是leader。如果有提交挂起的事物,那么服务器就不会怀疑leader。如果ped加载队列是空的,leader需要发出空事物来提交,并在该事物之后命令同步。当leader处于负载状态下的时候这是一个很好的特性,没有额外的广播流量会被生成。在我们的实现中,超时是设置为leader在follower放弃之前意识到自己不是leader,因此我们不会发出空事物。
Zookeeper用FIFO的顺序服务处理来自客户端的请求。响应包括相对应的zxid。即便是没有活动的间隔期间的心跳消息也包括客户端连接服务器看到的最后一个zxid。如果客户端连接到新的服务器,新服务器通过检查客户端最后一个zxid与自己最后的zxid进行比较,确保Zookeeper的数据视图只少和客户端的数据视图一样新。如果客户端有比服务端更多最新的视图,在服务端赶上之前,服务器不会重新建立与客户端的会话。客户端保证能够找到另一个有最近系统视图的服务,因为客户端只看到已经复制到大多数Zookeeper服务器的更改。这个行为对持久化保证很重要。
为了探测客户端会话失败,Zookeeper使用超时。如果没有其他服务接收到任何来自客户端的会话在超市时间内,leader决定这个客户端故障了。如果客户端发送请求足够频繁,那么它不需要发送其他信息。否则,客户端在低活跃度的周期发送心跳信息。如果客户端无法和服务端通信以发送请求或者心跳,它会连接不同的Zookeeper服务以重建会话。为了防止会话超时,Zookeeper客户端库在会话空闲 s/3 ms后发送心跳,如果在2s/ 3ms内没有收到服务器的消息,则切换到新服务器,其中s是会话超时,以毫秒为单位。
5 评估
我们在50台服务器的集群上执行了所有的评估。每台服务器都有一个至强双核2.1GHz处理器、4GB内存、千兆以太网和两个SATA硬盘驱动器。我们将下面的讨论分为两个部分:吞吐量和请求延迟。
5.1吞吐量
为了评估我们的系统,我们基准测试了系统满载和各种注入的故障导致的吞吐量改变时的吞吐量。我们改变了组成Zookeeper服务的数量,但是总是保持一定数量的相同客户端。为了模拟大量的客户端,我们使用了35台机器模拟250个同时的客户端。
我们使用了Java实现了Zookeeper服务,并且Java和C实现了客户端。我们使用了java服务器,将其配置为在一个专用磁盘上记录日志,并且在另一个磁盘上进行快照。我们的基准测试客户端使用了异步的java客户端API,每个客户端有只少100个请求待处理。每个请求包含读写或写的数据1K。我们没有展示其他操作的基准测试,因为所有操作的性能他们都是修改状态是近似相同的,并且没有状态修改的操作除了sync,是近似相同的。(sync的性能近似于轻量级写入,因为请求必须到达leader,但是不会进行广播。)客户端发送没300ms完成的操作数每6s采集一次。为了防止溢出,服务器限制系统中并发请求的数量。Zookeeper使用请求限制来防止服务器过载。对于这些实验,我们配置了Zookeeper服务有最多2000的总请求在进程中。
图5:饱和系统的吞吐量性能随着读写的比例而变化。
表1:极端满载的系统的吞吐量
图5中,我们展示了我们改变读写请求的比例来调整吞吐量,每条曲线对应提供不同数量的提供Zookeeper服务的服务器。表1展示了极端读负载的数量。如吞吐量比写吞吐量更高,因为读不适用原子广播。图中也展示了服务的数量对广播协议的性能有负面的影响。从图中我们观察到,系统中服务的数量不仅影响服务可以处理故障的数量,还影响服务可以处理负载的数量。三个服务器的曲线与其他服务器的曲线交叉在60%左右。这种情况不仅限于三服务配置,由于本地读启用了并行性所有的配置都会发生这种情况。在途中其他的配置是无法观察到的,然而,因为我们限制了y轴吞吐量。
写请求比读请求花费更长时间有俩个原因。首先,写请求必须通过原子广播,它需要一些额外的处增加了额外的延迟。写请求长时处理的其他原因是,在返回acknowledgments给leader之前,服务必须确保写请求被记录到稳态的存储中。原则上这个要求是过分的,但是对于我们的生产系统,我们以可用性换取可靠性,因为Zookeeper构成了应用程序的基础真理。我们使用更多的服务来容许更多的故障。我们将Zookeeper数据划分为多个Zookeeper集群来提高写的吞吐量。Gray等人之前已经观察到复制和分区之间的这种性能权衡。
图6:满载系统的吞吐量,当所有的客户端连接到leader改变读与写的比例,
Zookeeper能够通过在组成的服务服务之间分布负载达到高吞吐量。我们可以放松负载因为我们宽松的一致性保证。Chubby客户端直接把所有的请求给leader。图6展示了如果我们不使用这个宽松的特性强制客户端只连接leader会发生什么。如预期,吞吐量远小于读占主导的请求,但是即便是写主导工作负载吞吐量也更低。服务客户端造成额外的CPU和网络负载导致了影响leader协调提案广播的能力,这反过来又会对总体写性能产生不利的影响。
图7:原子广播协议组件在隔离状态下的平均吞吐量。Error bar代表了最小和最大的值。
原子广播协议做了大部分系统的工作,并且因此比其他任何部分更限制了Zookeeper的性能。图7展示了原子广播协议的吞吐量。为了基准测试它的性能我们通过直接在leader上生成事务来模拟客户机,因此没有客户端的连接客户端的请求和应答。在最大吞吐量时,原子广播组件受到CPU的限制。理论上图7中的性能Zookeeper 100%的写入的性能。然而Zookeeper客户端交流,访问控制列表(Access Control List, ACL)检查,和请求事务改变到所有的CPU。对于CPU的竞争使Zookeeper的吞吐量大大低于隔离状态下的原子广播组件。因为Zookeeper是一个项目组件,到目前为止我们的开发重点是正确性和鲁棒性。通过消除一些额外的拷贝,有大量的机会可以明显的改变性能。同一个对象的多个序列化,更有效的数据结构等。
为了展示系统随着故障注入的时间行为,我们运行由5台机器组成的Zookeeper服务。我们运行相同的满载基准测试和之前一样,但是此时我们保证写入百分比保持在30不变,这在我们期望的工作负载中是一个保守的比例。图8展示了系统的吞吐量随着时间改变。图中的事件标记如下:
- follower的失败和恢复
- 不同follower的失败和恢复
- leader的失败
- 俩个follower(a,b)的失败,并且在c点康复
- leader的失败
- leader的恢复
在途中有几个重要的观察。首先它遵循快速失败和快速恢复,其次Zookeeper允许保证高吞吐量,即使失败了。单个follower的失败不会影响服务器形成法定人数,因为共享了服务器在失败之前处理读请求的份额,只会大体的减少吞吐量。其次,我们的leader选举算法可以让回复足够的块以防止吞吐量的大幅下降。在我们的观察中Zookeeper花费少于200ms来选举新leader。因此,尽管服务器会在零点几秒内停止处理请求,由于我们的采样周期是秒数量级,我们没有观察到请求归零。第三,即便follower花费了很长时间重启,Zookeeper也能够在开始处理请求后再次提高吞吐量。在事件1、2、4之后我们没有完全恢复到吞吐量水平的原因是,客户端只有当follower的连接断开的时候才会切换follower。因此,在事件4之后客户端不重分配自己知道leader在事件3和事件5的时候故障了。事实上这种不平衡随着时间的负载会自行解决
5.2 请求延迟
为了评估请求延迟我们在Chubby之后创建了基准测试模型。我们创建了工作节点处理等吗发送创建,等待他们完成,发送新节点的异步删除操作,然后进行下一次创建。我们响应改变了工作节点的数量,对于每次运行,我们创建了5000个节点。我们通过将完成的请求数除以所有工作完成所需的总时间来计算吞吐量。
表2展示了基准测试的结果。创建包含1K数据的请求,而不是Chubby 基准测试中的5个字节,以更好符合我们预期的使用。即便在更大的请求下,Zookeeper的吞吐量也比Chubby公布的吞吐量大三倍。单个Zookeeper工作节点的基准测试的吞吐量代表了平均请求延迟是三台服务器1.2ms,九台服务器1.4ms。
5.3 屏障性能
在这个实验中我们执行一定数量的屏障以评估Zookeeper原语实现的性能。对于给定数量的屏障b每个服务首先进入所有b屏障,然后依次离开所有b个屏障。就像我们在2.4章节使用二重屏障那样,客户端首先等待所有客户端在移动到下一次调用之前(类似leave())执行enter()步骤。
表3:以秒为单位进行屏障实验。每个点是每个客户完成五次运行所需时间的平均值。
我们上报我们的实验结果在表3中。在这次实验,我们有50,100和200个客户端连续不断的进入b个屏障,$b\in \{200,400, 800, 1600\}$。虽然应用可以有上千个Zookeeper的客户端,通常客参与每个协调的子集要小得多,因为客户端通常根据应用的具体情况进行分组。
这个实验中俩个有趣的观察是,处理所有障碍的时间与障碍的数量大致呈线性增长,表明数据数同意部分的并发访问不会产生任何意外延迟,这种红延迟随着客户端数量的增加而增加。这是Zookeeper服务没有饱和的结果。我们观察到即使客户端保持一致,在所有例子中,屏障操作的吞吐量介于1950和3100操作每秒之间。在Zookeeper操作中,这个对应的吞吐量值在10700和17000操作每分钟之间。在我们的实现中我们有4:1的读写比例(80%是读操作),我们基准测试代码使用的吞吐量比Zookeeper可以实现的原始吞吐量要低得多。(根据图5 超过了40000)这是由于客户端在等待其他客户端。
6相关工作
Zookeeper的目标是解决协调进程在分布式系统应用中的问题。为了达到这个目标,它设计使用的想法来自先前的协调服务,容村系统,分布式算法和文件系统。
我们首先提出了分布式应用的协调系统。一些早期的系统提出了事务应用的分布式锁服务,和在集群中共享信息。最近Chubby提出了系统来管理分布式系统询问锁。Chubby和Zookeeper都有几个共同的目标。它们都类似于文件系统的接口。都使用了一致性协议来保证副本的共识。然而Zookeeper不是一个锁服务。它可以被用在客户端实现锁,但是它的操作API是无锁的。不像Chubby,Zookeeper允许客户端连接任何的Zookeeper服务,而不只是Leader。Zookeeper客户端可以使用他们的本地服务来服务数据管理watch,因为它的一致性模型比Chubby的来的松。这允许Zookeeper提供更好的性能,允许应用更广泛的使用Zookeeper。
文件已经提出了一些容错系统,其目标是减轻构建分布式应用程序的问题。一个早期的文件系统ISIS。ISIS系统转换抽象的类型规范到容村分布式对象中,从而使容错机制对用户透明。Horus和Ensemble是由ISIS演化来的。Zookeeper拥抱了ISIS的虚拟同步(virtual syncrony)的概念。最后Totem保证了消息传播的总顺序。Zookeeper工作节点适用于广泛的网络拓补,这促使我们依赖于服务和进程之间的TCP连接而不是假设任何的特殊功拓补或者硬件特性。我们也没有暴露Zookeeper内部在使用的任何集成通信。
一个用于构建容错服务的重要的技术是状态机复制,paxos算法它允许有效的复制状态机到异步系统的实现。我们使用的算法和paxos有一些共同点,但是它结合了一致性所需的事物日志和数据树所需的预写日志,以实现高效的实现。已经提出了用于拜占庭容忍复制状态机的实际实现。Zookeeper不认为服务可以是拜占庭式的,但是我们使用校验和与完整性检查以缓存无恶意的拜占庭故障。Clement等人讨论了一种方法可以让Zookeeper完全拜占庭容错而不修改当前服务库代码的方法。到目前为止,我们还没有在生产中观察到使用完全拜占庭容错协议可以防止的错误。
Boxwood是一个使用了分布式锁的系统。Boxwood提供高级的应用抽象,它依赖于基于paxos的分布式锁之上。类似Boxwood,Zookeeper是一个用来构建分布式系统的组件。然而Zookeeper有更高的性能要求和更广泛的备用在客户端应用。Zookeeper公开了应用程序用来使用高级原语的低级原语。
Zookeeper类似于文件系统,但是它只提供文件系统操作的子集添加了在大多数文件系统中不存在的函数,比如顺序保证和有条件的写。然而,Zookeeper监听在理念上与AFS的缓存回调类似。
Sinfonia引入了mini-transactions,一个新的构建可扩展的分布式系统的范例。Sinfonia已经设计了存储应用的数据,然而Zookeeper存储应用的元数据。Zookeeper将其状态完全复制并保存在内存中,已实现高性能和一致性。我们对文件系统的使用,比如操作和排序实现了类似于迷你事物的功能。znode是一个方便的抽象,我们可以在其上面添加watch,这是Sinfonia缺少的功能。Dynamo允许客户端得到设置相关的少量的数据(少于1MB)在分布式的键值存储中。不像Zookeeper,Dynamo的键空间不是分层的。Dynamo也没提供强大的持久性和对写的一致性保证,而是在写的时候解决冲突。
DepSpace使用了元组空间已提供拜占庭容错服务。类似Zookeeper在客户端使用简单的服务接口来实现强同步原语。其中Depspace的性能明显低于Zookeeper,他提供了更强大的容错性和保密性保证。
7 结论
Zookeeper通过暴露一个无等待对象的客户端,解决了无等待的方法给分布式系统的协调进程的问题。我们发现Zookeeper对于雅虎内外的几个程序都很有用。Zookeeper使用快速读和watch对于以读占主导的服务,达到了每秒钟数以十万的操作的吞吐量,这俩中操作都由本地副本提供服务。虽然我们对于读取的一致性和watch是比较弱的。我们已经展示了在我们使用的案例中,集群允许我们在客户端使用快照和复杂的协调协议,尽管读取不是按优先级排序的,并且数据对象的实现是无等待的。无等待特性已经被证明是高性能的必要条件。
虽然我们只描述了几个应用,还有许多其他的应用使用Zookeeper。我们相信这样的成功是因为它简单的接口以及可以通过这个接口实现的强大的抽象。因为Zookeeper的高吞吐量,应用可以广泛的使用它,而不只是粗粒度的锁定。