翻译 ZooKeeper: Wait-free coordination for Internet-scale systems

<center>ZooKeeper:因特网规模系统的无等待协调服务</center>

Patrick Hunt and Mahadev Konar Flavio P. Junqueira and Benjamin Reed Yahoo! Grid Yahoo! Research {phunt,mahadev}@yahoo-inc.com {fpj,breed}@yahoo-inc.com

摘要

在本文中,我们描述了用于协调分布式应用程序进程的服务ZooKeeper。由于ZooKeeper是关键基础结构的一部分,因此ZooKeeper旨在提供一个简单而高性能的内核,以在客户端构建更复杂的协调原语。它在复制的集中式服务中合并了来自组消息传递,共享寄存器和分布式锁定服务的元素。 ZooKeeper公开的接口具有共享寄存器的无等待特性,使用类似于分布式文件系统缓存失效的事件驱动机制来提供简单而强大的协调服务。

ZooKeeper接口可以实现高性能服务。除了无等待属性之外,ZooKeeper改变每个客户端请求执行FIFO和所有ZooKeeper状态改变线性化。这些设计决策可以实现满足本地服务器读取请求的高性能处理管道。我们表示ZooKeeper每秒可以处理成千上万的事务,从2:1到100:1读写比例的工作负载。这种性能允许客户端应用程序广泛地使用ZooKeeper。

1 Introduction

大型分布式应用程序需要不同形式的协调。 配置是最基本的协调形式之一。 在最简单的形式中,配置只是系统过程的操作参数列表,而更复杂的系统具有动态配置参数。 组成员身份和领导者选举在分布式系统中也很常见:通常,进程需要知道哪些其他进程仍在运行以及这些进程负责什么。 锁构成强大的协调原语,实现对关键资源的互斥访问。

协调的一种方法是为每个不同的协调需求开发服务。 例如,Amazon Simple Queue Service特别关注排队。 还专门针对leader选举和配置开发了其他的服务。 实现功能更强大的原语的服务可用于实现功能更弱的原语。 例如,Chubby是具有强同步保证的锁服务。 然后可以使用锁来实施leader选举,集群管理等。

在设计协调服务时,我们不再在服务器端实现特定的原语,而是选择公开一个API,使应用程序开发人员可以实现自己的原语。 这种选择的结果是实现了一个协调内核,该协调内核无需更改服务核心,就可以实现新的原语。 这种方法满足了应用程序对多种形式的协调的需求,而不是将开发人员约束到一组固定的原语上。

在设计ZooKeeper的API时,我们不再使用阻塞原语,比如锁。除了其他问题外,阻塞协调服务的原语会导致客户端变慢或故障,从而对速度更快的客户端的性能产生负面影响。如果处理请求依赖于其他客户端的响应和故障检测,则服务本身的实现将变得更加复杂。因此,我们Zookeeper实现了一个API,可以像文件系统一样操作按层次结构组织的简单的免等待数据对象。实际上,Zookeeper API类似于任何其他文件系统,如果只看API签名,ZooKeeper看起来就像是没有锁方法、开启、关闭的Chubby。但是,实现无等待的数据对象与基于阻塞原语(如锁)的系统有很大的区别。

虽然无等待特性对性能和容错很重要,但对做到协调来说还不够。我们还必须保证操作有序执行。特别是,我们发现,保证所有操作的客户端FIFO顺序和线性化的写操作,就能有效地实现服务,并且足以实现应用程序感兴趣的协调原语。事实上,我们可以用我们的API实现任意数量进程的一致性,并且根据Herlihy(人名)的层次结构,ZooKeeper实现了一个通用对象。

ZooKeeper服务由一组使用复制以实现高可用性和高性能的服务器组成。它的高性能使包含大量进程的应用程序可以使用这种协调内核来管理协调的各个方面。我们能够使用简单的管道体系结构实现ZooKeeper,使我们可以处理数百或数千个请求的同时仍保证低延迟。这样的管道自然支持按FIFO顺序从单个客户端执行操作。保证FIFO客户端顺序使客户端可以异步提交操作。 使用异步操作,客户端一次可以执行多个未完成的操作。 例如,当新客户端成为leader并且必须操纵元数据并相应地对其进行更新时,就需要使用这个特性。 如果不能进行多个未完成的操作,则初始化时间可以是几秒左右,而不是亚秒级。

为了保证更新操作满足线性化,我们实现了一个基于leader的原子广播协议,称为Zab(Zookeeper Atomic Broadcast) 。但是,ZooKeeper应用程序的典型工作负载是读操作,因此需要扩展读吞吐量。在ZooKeeper中,服务器在本地处理读操作,我们不使用Zab对它们进行完全排序。

在客户端缓存数据是提高读取性能的一项重要技术。例如,对于一个进程来说,缓存当前leader的标识符是非常有用的,而不是每次需要了解ZooKeeper时都去获取。ZooKeeper使用一种监视机制使客户端可以缓存数据,而无需直接管理客户端缓存。 使用此机制,客户端可以监视对给定数据对象的更新,并在更新时接收通知。 Chubby直接管理客户端缓存。 它会阻止更新,以使所有缓存更改数据的客户端的缓存无效。 在这种设计下,如果这些客户端中的任何一个运行缓慢或出现故障,更新都会延迟。 Chubby使用租约来防止有故障的客户端无限期地阻塞系统。 但是,租约只能限制运行缓慢或故障客户端的影响,而ZooKeeper watches完全可以避免此问题。

在本文中,我们讨论了ZooKeeper的设计和实现。 使用ZooKeeper,即使只有写入是可线性化的,我们也可以实现应用程序所需的所有协调原语。 为了验证我们的态度,我们展示了如何使用ZooKeeper实现一些协调原语。

综上所述,本文的主要贡献有:

协调内核(Coordination kernel):我们提出了一种无等待的协调服务,该服务具有宽松的一致性保证,可用于分布式系统。 特别是,我们描述了协调内核的设计和实现,并且我们已在许多关键应用程序中使用他来实现了各种协调技术。

协调方法(Coordination recipes):我们展示了ZooKeeper如何用于构建更高级别的协调原语,甚至包括阻塞和强一致性原语,这些原语经常在分布式应用程序中使用。

**协调经验(Experience with Coordination):**我们分享了一些使用ZooKeeper并评估其性能的方式。

2 The ZooKeeper service

客户端使用ZooKeeper客户端库通过clientAPI向ZooKeeper提交请求。 除了通过clientAPI公开ZooKeeper服务接口外,客户端库还管理客户端与ZooKeeper服务器之间的网络连接。

在本节中,我们首先提供theZooKeeper服务的高级视图。 然后,我们讨论客户端用来与ZooKeeper交互的API。

术语。在本文中,我们使用client来表示ZooKeeper服务的用户,使用server来表示提供ZooKeeper服务的进程,使用znode来表示ZooKeeper数据中的内存数据节点,它被组织在称为数据树的分层命名空间中。我们还使用术语update和write来指代修改数据树状态的任何操作。客户端在连接到ZooKeeper时建立一个会话,并获得一个会话句柄,通过它发出请求。

2.1 Service overview

ZooKeeper向其客户提供了根据分层命名空间组织的一组数据节点(znode)的抽象。这个层次结构中的znode是客户端通过ZooKeeper API操作的数据对象。 分层命名空间通常在文件系统中使用。 这是组织数据对象的一种理想方式,因为用户已经习惯了这种抽象,并且可以更好地组织应用程序元数据。 要引用给定的znode,我们使用标准的UNIX方式表示文件系统路径。 例如,我们使用*/A/B/C*表示到znode C的路径,其中C的父节点是B,而B的父节点是A。 所有znode都存储数据,并且除临时znode之外的所有znode都可以有子节点。

<center>图1:ZooKeeper分层名称空间的图示。</center>

客户端可以创建两种类型的znode: 常规(Regular):客户端显式的创建和删除常规znode; 临时性(Ephemeral):客户端创建这样的znode后,要么显式地删除它们,要么让系统在(创建这个节点的)会话终止时自动删除它们(故意或由于失败)。

此外,在创建新znode时,客户端可以设置一个顺序标志。使用顺序标志集创建的节点的名称后面附加了一个单调递增的计数器的值。如果n是新的znode, p是父znode,那么n的序列值永远不会小于在p下创建的任何其他顺序znode的名称中的值。

ZooKeeper实现了监视器(Watcher),允许客户端不需要轮询能够及时接收更改通知。当客户端发送带有监视标志的读取操作时,该操作会正常完成,并且服务器保证在返回的信息已更改时通知客户端。监视器(Watcher)是一次性触发的;一旦触发或会话关闭,监视器就会被移除。监视器只会通知更改的发生,不会提供更改的内容。例如,如果客户端在两次修改"/foo"之前发出了一个getData("/foo", true),客户端会受到一个观察事件,得知数据已更改。会话事件,如连接丢失事件,也会被发送到监视回调,以便客户端知道监视事件可能被延迟。

数据模型(Data model)。ZooKeeper的数据模型本质上是一个文件系统,它有一个简化的API,只能读写完整的数据,或者是一个键有层次结构的键/值表,它有层次键。层次命名空间对于为不同应用程序的命名空间分配子树和设置这些子树的访问权限非常有用。我们还利用客户端目录的概念来构建更高级别的原语,我们将在2.4节中看到。

与文件系统中的文件不同,znode不是为常规数据存储设计的。 相反,znode映射到客户端应用程序的抽象,通常对应于用于协调目的的元数据。 为了说明这一点,在图1中,我们有两个子树,一个用于应用程序1 (/app1) ,另一个用于应用程序2(/app2)。 应用程序1的子树实现了一个简单的组成员身份协议:每个客户端进程p<sub>i</sub>在/ app1下创建一个znode p_i,只要该进程正在运行,它就一直存在。

虽然znodes并不是为通用数据存储而设计的,但是ZooKeeper确实允许客户端存储一些信息,用于分布式计算中的元数据或配置。例如,在一个基于leader的应用程序中,对于一个刚刚开始了解当前哪个服务器是领导的应用程序服务器来说,这是非常有用的。为了实现这个目标,我们可以让当前的领导者在znode空间中一个已知的位置写入这个信息。znode还具有与时间戳和版本计数器相关联的元数据,这允许客户端跟踪对znode的更改,并根据znode的版本执行条件更新。

会话(Sessions)。客户端连接到ZooKeeper会启动一个会话。会话有一个关联的超时时间。如果在超过该超时时间内,ZooKeeper没有受到会话的任何消息,则会认为客户端故障。当客户端显式关闭会话句柄或ZooKeeper检测到客户端出错时,会话结束。在一个会话中,客户端观察一系列反映其操作执行情况的状态变化。会话使客户端能够在ZooKeeper集合中透明地从一个服务器移动到另一个服务器,从而在整个ZooKeeper服务器之间持续存活。

2.2 Client API

我们在下面展示ZooKeeper API的相关子集,并讨论每个请求的语义。

create(path, data, flags): 用路径名path创建一个znode,在其中存储data [],并返回新znode的名称。flags允许客户端选择znode的类型:regular(常规), ephemeral(短暂)和序列类型;

delete(path, version):删除znode如果它的版本跟传入version一致;

exists(path, watch):如果路径名称为path的znode存在,则返回true,否则返回false。 watch使客户端可以在znode上设置监视器;

getData(path, watch):返回与znode关联的数据和元数据,例如版本信息。watch的工作方式与exists()一样,不同之处在于,如果znode不存在,ZooKeeper不会设置监视器。

setData(path, data, version):如果version是znode当前版本,则把data写入路径为path的znode。

getChildren(path, watch):返回路径为path的znode的子集合。

sync(path):等待操作开始时所有未决的更新传播到客户端连接到的服务器。path目前被忽略。

所有方法都有一个同步和一个异步版本,可以通过API使用。当应用程序需要执行单个ZooKeeper操作且没有要执行的并发任务时,它会使用同步API,因此它会执行必要的ZooKeeper调用并阻塞。然而,异步API使应用程序能够同时执行多个未完成的ZooKeeper操作和其他任务。ZooKeeper客户端保证为每个操作依次调用相应的回调。

请注意,ZooKeeper不使用句柄访问znodes。相反,每个请求都包含正在操作的znode的完整路径。这种选择不仅简化了API(没有open()或close()方法),而且还消除了服务器需要维护的额外状态。

每个更新方法都采用一个预期的版本号,从而支持条件更新的实现。如果znode的实际版本号与预期的版本号不匹配,则更新失败,并出现意外的版本错误。如果􀀀1版本号,它不执行版本检查。

每个更新方法都采用一个预期的版本号,从而可以实现条件更新。如果znode的实际版本号与预期的版本号不匹配,则更新失败,并抛出意外的版本错误。如果版本号为-1,则不执行版本检查。

2.3 ZooKeeper guarantees

ZooKeeper具有两个基本的顺序保证:

Linearizable writes:所有更新ZooKeeper状态的请求都是可线性化的,并且遵循优先级;

FIFO client order:来自给定客户端的所有请求均按照客户端发送的顺序执行。

注意,我们对线性化的定义与Herlihy最初提出的定义不同,我们称之为A-linearizability (异步线性化)。在Herlihy最初对线性化的定义中,客户端一次只能有一个未完成的的操作(客户端是一个线程)。在我们的操作系统中,我们允许一个客户端有多个未完成的操作,因此我们可以选择对同一个客户端未完成的操作保证没有特定的顺序,或者保证FIFO顺序。我们选择后者作为我们的特性。重要的是要观察到所有对linearizable 对象成立的结果对A-linearizable对象也成立因为一个满足A-linearizable的系统也满足linearizable 。因为只有更新请求是可线性化的,所以ZooKeeper在每个副本上本地处理读请求。这使得服务可以随着服务器的添加而线性扩展。

若要查看这两个保证如何相互作用,请考虑以下情形。包含多个进程的系统选举领导者指挥工作进程。当新领导负责系统时,必须更改大量配置参数,并在完成后通知其他进程。 然后,我们有两个重要要求:

  • 当新领导开始进行更改时,我们不希望其他进程使用正在更改的配置;
  • 如果新的leader在配置完全更新之前死亡,我们不希望进程使用这个部分配置。

注意,分布式锁(例如由Chubby提供的锁)有助于满足第一个需求,但不足以满足第二个需求。使用ZooKeeper,新leader可以指定一条路径作为ready znode;其他进程仅在znode存在时才使用配置。新的leader通过删除ready、更新各种配置znodes和创建ready来更改配置。所有这些更改都可以被流水线化异步发出,以快速更新配置状态。尽管更改操作的延迟大约为2毫秒,但是如果请求一个接一个地发出,则更新5000个不同znode的新leader将花费10秒;通过异步发出请求,请求将花费不到一秒的时间。由于顺序保证,如果一个进程看到ready的znode,它还必须看到新leader所做的所有配置更改。如果新的leader在就绪的znode创建之前死亡,那么其他进程就知道配置还没有完成,所以不会使用它。

上面的方案仍然有一个问题:如果进程在新领导开始进行更改之前看到就绪,然后在更改过程中开始读取配置,那么会发生什么情况?这个问题通过通知的排序保证来解决:如果客户端正在监视更改,那么客户端将在更改后看到系统的新状态之前看到通知事件。因此,如果读取就绪的znode请求的进程在该znode发生更改时得到通知,那么在它可以读取任何新配置之前,它将看到通知客户端更改的通知。

当客户端除了ZooKeeper之外还有自己的通信渠道时,就会出现另一个问题。例如,考虑两个客户端A和B,它们在ZooKeeper中具有共享配置,并且通过共享通信通道进行通信。如果A更改了ZooKeeper中的共享配置,并通过共享通信通道将更改告知B,则B将在重新读取配置时看到更改。如果B的ZooKeeper副本稍微落后于A,则可能看不到新配置。使用上述保证,B可以在重新读取配置之前发出写操作,从而确保看到最新的信息。为了更有效地处理这种情况,ZooKeeper提供了同步请求:当后面跟着一个读时,就构成了一个慢读。同步会导致服务器在处理读之前应用所有挂起的写请求,而不会产生完全写的开销。这个原语在概念上类似于ISIS的flush本原语。

ZooKeeper还具有以下两个活动性和持久性保证:如果大部分的ZooKeeper服务器都是活动的,并且可以通信,则服务可用;如果ZooKeeper服务成功地响应了一个更改请求,只要最终能够达到法定服务器数量,这个更改就会在任何数量的故障中持续存在。

2.4 Examples of primitives

在本节中,我们将展示如何使用ZooKeeper API来实现更强大的原语。ZooKeeper服务对这些功能更强大的原语一无所知,因为它们完全是在客户端使用ZooKeeper客户端API实现的。一些常见的原语(如组成员关系和配置管理)也是wait-free(无等待)的。对其他的,比如rendezvous(集合点),客户端需要等待一个事件。尽管ZooKeeper是无等待的,但我们可以使用ZooKeeper实现高效的阻塞原语。ZooKeeper的顺序保证允许对系统状态进行有效的推理,而watches(监视器)则允许高效的等待。

Configuration Management(配置管理) ZooKeeper可用于在分布式应用程序中实现动态配置。它以最简单的形式存储在znode zc中。进程以zc的完整路径名启动。活动进程通过将watch标志设置为true来读取zc并获取其配置。如果zc中的配置被更新,进程将收到通知并读取新的配置,(必须)再次将watch标志设置为true。

注意,在这个方案中,就像在其他大多数使用watch的方案中一样,watch用于确保进程拥有最新的信息。例如,如果监视zc的进程收到zc变更的通知,而在它发出对zc的读取之前,zc还有三次变更,那么该进程不会再收到这个三个(变更的)通知事件。这不会影响进程的行为,因为这三个事件只会通知进程它已经知道的事情:他拥有的zc的信息过时了。

Rendezvous 有时候,在分布式系统中,并不总是预先清楚最终的系统配置会是什么样的。例如,客户可能想要启动一个主进程和几个工作进程,但进程是通过一个调度程序开始,所以客户没法提前知道它可以给到连接的主进程的信息,比如地址和端口。我们用ZooKeeper使用一个rendezvous znode zr来处理这个场景,zr是客户端创建的一个节点。客户端将zr的完整路径名作为启动参数传递给主进程和工作进程的。当主程序启动时,它把正在使用的地址和端口的信息设置到zr。当工作进程开始启动时,他们将watch设置为true来监听zr。如果zr还没有被设置,worker将等待zr被修改时的通知。如果zr是临时节点,主进程和工作进程可以监视zr是否被删除,并在客户端结束时清理自己。

Group Membership 我们利用临时节点来实现组成员关系管理。 确切地说,我们利用了一个事实,即我们可以看到创建临时节点的会话的状态。我们首先指定一个znodezg来表示一个组。当该组的进程成员启动时,它将在zg下创建临时子节点znode。如果每个进程都有唯一的名称或标识符,那么该名称将用作子进程znode的名称;否则,进程将创建带有顺序标志的znode,以获得唯一的名称分配。进程可以将进程信息放入子znode的数据中,例如进程使用的地址和端口。

zg下创建子节点znode之后,进程将正常启动。它不需要做任何其他的事情。如果该进程失效或结束,在zg下表示该进程的znode将自动删除。

进程可以通过列出zg的子进程来获得组信息。如果进程希望监视组成员关系的变化,则可以将watch标志设置为true,并在收到变更通知时刷新组信息(始终将watch标志设置为true)。

**Simple Locks ** 虽然ZooKeeper不是锁服务,但它可以用来实现锁。使用ZooKeeper的应用程序通常使用根据需要定制的同步原语,就像上面说的那样。这里,我们将展示如何用ZooKeeper实现锁,以表明它可以实现各种各样的通用同步原语。

最简单的锁实现使用“锁文件”。该锁由znode表示。为了获取锁,客户端尝试创建带有临时标志的指定znode。如果创建成功,客户端将持有锁。否则,客户端可以读取znode,并设置watch标志,以便在当前leader死亡时收到通知。客户端在锁失效或显式删除znode时释放锁。其他等待锁的客户端在观察到znode被删除后再次尝试获取锁。

虽然这个简单的锁定协议可以使用,但它确实存在一些问题。首先,它受到羊群效应的影响。如果有很多客户端在等待获取锁,那么当锁被释放时,即使只有一个客户端可以获取锁,它们也会一起争夺锁。其次,它只实现了独占锁。下面两个原语显示了如何同时解决这两个问题。

**Simple Locks without Herd Effect **我们定义了一个锁znode l来实现这个锁。直观地,我们将请求锁的所有客户端排列起来,每个客户端按照请求到达的顺序获得锁。因此,希望获得锁的客户可以执行以下操作:

Lock 1 n = create(l + “/lock-”, EPHEMERAL|SEQUENTIAL) 2 C = getChildren(l, false) 3 if n is lowest z node 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)

在锁的第1行中使用SEQUENTIAL标志,让所有客户端按照顺序尝试获取锁。在第3行如果发现客户端的znode的序列号是最低的,则客户端持有锁。否则,客户端将等待拥有锁的znode删除,或者在客户端znode之前获取锁。通过只监视客户端znode之前的znode,我们避免了羊群效应,只在锁被释放或锁请求被放弃时唤醒一个进程。一旦客户端监视的znode消失,客户端必须检查它现在是否持有锁。(之前的锁请求可能已经被放弃,有一个序列号较低的znode仍在等待或持有锁。)

释放锁就像删除表示锁请求的znoden一样简单。通过在创建时使用EPHEMERAL标志,崩溃的进程将自动清除所有锁请求或释放它们可能拥有的任何锁。

综上所述,该锁方案有以下优点:

  1. 删除一个znode只会导致一个客户端唤醒,因为每个znode都被另一个客户端监视,所以我们没有羊群效应;
  2. 没有轮询或超时;
  3. 由于我们实现锁定的方式,通过浏览ZooKeeper数据,我们可以看到锁争用的数量、中断锁和调试锁问题。

Read/Write Locks 为了实现读/写锁,我们稍微改变了锁过程,将读锁和写锁过程分开。解锁过程与全局锁的情况相同。

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

这个锁过程与之前的锁略有不同。写锁只在命名上有所不同。因为读锁可以共享,所以第3行和第4行略有不同,因为只有以前的写锁znodes才会阻止客户端获得读锁。当多个客户端在等待一个读锁,并在删除序列号较低的“write-”znode时得到通知时,似乎出现了“羊群效应”;事实上,这是我们所期望的行为,所有那些读客户端都应该被释放,因为它们现在可以拥有锁。

Double Barrier(双重屏障) 双重屏障使客户端能够同步计算的开始和结束。当barrier阈值定义的足够多的进程加入barrier时,进程开始它们的计算并在完成之后离开barrier。我们用znode在ZooKeeper中表示一个barrier,这个障碍被称为b。每个进程p在进入时都通过创建一个znode作为b的子进程来注册b -,当它准备离开时取消注册删除子进程。当b的子节点znodes的数量超过barrier阈值时,进程可以进入barrier。当所有进程都删除了它们的子进程时,进程可以离开barrier。我们使用watch有效地等待进入和退出条件得到满足。如果要离开,进程会观察是否存在b的就绪子进程,该子节点将由导致子节点数超过障碍阈值的进程创建。 如果要离开,进程会观察某个特定的子节点是否消失,并且仅在删除znode后检查退出状态。

3 ZooKeeper Applications

我们现在描述一些使用ZooKeeper的应用程序,并简要解释它们是如何使用它的。我们用粗体显示每个示例的原语。

The Fetching Service(获取服务) 爬虫是搜索引擎的一个重要部分,Yahoo!抓取的Web文档有数十亿个。抓取服务(FS)是Yahoo!爬虫的一部分,且仍在运行中。本质上,它有由页面获取进程控制的的主进程。主服务器向获取进程提供配置,获取进程回写它们的状态和运行状况。在FS中使用ZooKeeper的主要优点是:可以从主服务器的故障中恢复,保证在出现故障时仍然可用,并将客户端与服务器解耦,允许它们仅通过从ZooKeeper读取状态就可以将请求指向健康的服务器。因此,FS主要使用ZooKeeper来管理配置元数据,尽管它也使用ZooKeeper来选择master (leader election)。

<center>图2:带有抓取服务的ZK服务器的工作负载。每个点代表一个一秒的样本。</center>

图2显示了FS在三天内使用的ZooKeeper服务器的读写流量。为了生成这个图,我们计算周期内每秒的操作数,每个点对应于这一秒内的操作数。我们观察到读流量比写流量高得多。在速率高于每秒1000次操作的时间里,读写操作比例在10:1和100:1之间变化。这个工作负载中的读操作是getData()、getChildren()和exists(),按流行度递增。

Katta Katta是使用ZooKeeper进行协调的分布式索引器,是一个非yahoo !应用程序。Katta使用分片划分索引工作。主服务器将分片分配给从服务器并跟踪进度。从服务器可能会失败,所以主服务器必须随着从服务器的来来去去重新分配负载。主服务器也可能出现故障,因此其他服务器必须准备好在出现故障时接管。Katta使用ZooKeeper跟踪从服务器和主服务器(组成员关系)的状态,并处理主服务器故障转移(leader选举)。Katta还使用ZooKeeper来跟踪和传播分配给从服务器的分片(配置管理)。

Yahoo! Message Broker Yahoo! Message Broker (YMB)是一个分布式发布-订阅系统。该系统管理数以千计的topic,客户端可以向这些topic发布消息,也可以从这些topic接收消息。topic分布在一组服务器中,以提供可伸缩性。使用主备份模式复制每个topic,该模式确保将消息复制到两台机器,以确保消息的可靠传递。组成YMB的服务器使用无共享的分布式架构,这使得协调对于正确的操作至关重要。YMB使用ZooKeeper管理topic的分配(配置元数据),处理系统中机器的故障(故障检测和组成员关系),以及系统运行。

<center>图3:Yahoo! Message Broker(YMB)在ZooKeeper的结构布局</center>

图3显示了YMB的znode数据布局的一部分。每个代理域都有一个名为nodes的znode,它对组成YMB服务的每个活动服务器都有一个临时的znode。每个YMB服务器在nodes下创建一个带有负载和状态信息的临时znode,并通过ZooKeeper提供组成员和状态信息。像shutdown和migration prohibition这样的节点,由构成该服务的所有服务器监控,并允许对YMB进行集中控制。对于YMB管理的每个topic,topic目录都有一个子znode。这些topic znodes具有子znodes,它们记录着每个topic的主服务器和备份服务器以及该topic的订阅者。主服务器和备份服务器znodes不仅允许服务器发现负责某个topic的服务器,还可以管理leader election和服务器崩溃。

4 ZooKeeper Implementation

ZooKeeper通过在组成服务的每个服务器上覆制ZooKeeper数据来提供高可用性。我们假定服务器是因为崩溃而发生故障的,这种服务器故障可能稍后就会恢复。图4显示了ZooKeeper服务的高层组件。在接收到请求后,服务器为执行请求做准备(请求处理器)。如果这样的请求需要在服务器(写请求)之间进行协调,那就使用一致性协议(原子广播的一种实现),最后,服务器将更改提交给ZooKeeper数据库,并将其完全复制到全体服务器中。对于读取请求,服务器只需读取本地数据库的状态并生成对请求的响应。

<center>图4:ZooKeeper服务组件</center>

复制的数据库是包含整个数据树的内存数据库。默认情况下,树中的每个znode最多存储1MB的数据,但是这个最大值是可以在特定情况下更改的可配置参数。为了提高可恢复性,我们有效地将记录更新到磁盘,并在将他们应用到内存中的数据库之前强制写入磁盘介质中。实际上,就像Chubby,我们保留已提交操作的重放日志(在本例中为预写日志),并定期生成内存数据库的快照。

每个ZooKeeper服务器都向客户端提供服务。客户端只需要连接到一个服务器来提交它的请求。就像前面说的,读取请求来自每个服务器数据库的本地副本。更改服务状态的请求(写请求)由一致性协议处理。

作为一致性协议的一部分,写请求被转发到称为leader的单个服务器。其他的ZooKeeper服务器称为followers,接收来自leader的包含状态改变的proposals(提案)信息,并就状态改变达成一致。

4.1 Request Processor

由于消息传递层是原子的,所以我们保证本地副本永远不会偏离,尽管在任何时间点,某些服务器可能应用了比其他服务器更多的事务。与客户端发送的请求不同,事务是幂等的。当leader接收到一个写请求时,它会计算应用写请求时系统的状态,并将其转换为捕获这个新状态的事务。因为可能有未完成的事务尚未应用到数据库,所以必须计算将来的状态。例如,如果客户端执行附带条件的setData,并且请求中的版本号与正在更新的znode的未来版本号相匹配,则服务将生成一个包含新数据、新版本号和更新的时间戳的setDataTXN。如果出现错误,如版本号不匹配或要更新的znode不存在,则生成errorTXN。其他的动物管理员服务器称为追随者,接收来自领导者的包含状态改变的信息建议,并就状态改变达成一致。

4.2 Atomic Broadcast

所有更新ZooKeeper状态的请求都会转发给leader。leader执行请求,并通过原子广播协议Zab将状态更改广播到ZooKeeper。接收客户端请求的服务器在提交相应状态更改时会响应给客户端。Zab默认使用简单的quorums原则来决定一个提案,所以Zab和ZooKeeper只有在大多数服务器都正确的情况下才能工作(即当使用2f + 1服务器时,我们可以容忍f个服务器故障)。

为了实现高吞吐量,ZooKeeper尝试保持请求处理管道满载。在处理管道的不同部分可能有数千个请求。由于状态更改依赖于先前状态更改的应用程序,因此Zab提供了比常规原子广播更强的顺序保证。更具体地说,Zab保证一个leader广播的变更按照发送的顺序传递,并且在之前leader广播自己的变更之前,所有来自前任领导的变更都会传递给一个已经建立的领导(leader变更后,之前的消息顺序不变)。

有一些实现细节可以简化我们的实现并为我们提供出色的性能。我们使用TCP进行传输,因此消息顺序由网络维护,这允许我们简化实现。我们使用Zab选择的leader作为ZooKeeper leader,由这个进程创建和提出事务。我们使用日志来跟踪提案,将其作为内存数据库的预写日志,这样就不必将消息两次写入磁盘。

在正常操作期间,Zab会按顺序准确地一次交付所有消息,但由于Zab不会持久地记录所交付的每条消息的id,所以Zab可能会在恢复期间重新交付一条消息。因为我们使用幂等事务,所以只要按顺序交付,多次交付是可以接受的。事实上,ZooKeeper要求Zab重新传递至少所有在最后一个快照开始后传递的消息

4.3 Replicated Database

每个副本在内存中都有一个ZooKeeper状态的拷贝。当ZooKeeper服务器从崩溃中恢复时,它需要恢复这个内部状态。在服务器运行一段时间后,重放所有已发送的消息以恢复状态将花费非常长的时间,因此ZooKeeper使用定期快照,并且只需要在快照开始后重新发送消息。我们称ZooKeeper快照为模糊快照,因为我们获取快照时不锁定ZooKeeper状态;相反,我们对树进行深度优先扫描,原子地读取每个znode的数据和元数据,并将它们写入磁盘。由于产生的模糊快照可能应用了在生成快照期间交付的状态更改的某个子集,因此结果可能在任何时间点上都不对应于ZooKeeper的状态。然而,由于状态更改是幂等的,因此只要按顺序应用状态更改,我们就可以应用它们两次。

例如,假设在一个ZooKeeper数据树中,两个节点/foo和/goo的值分别为f1和g1,当模糊快照开始时,它们的版本都是1,接下来的状态更改流到达时,其形式为(transactionType, path, value, new-version): (SetDataTXN, /foo, f2, 2) (SetDataTXN, /goo, g2, 2) (SetDataTXN, /foo, f3, 3)

在处理了这些状态更改之后,/foo和/goo的版本3和版本2的值分别为f3和g2。然而,模糊快照可能已经记录了/foo和/goo在版本3和1中分别拥有f3和g1的值,这不是ZooKeeper数据树的有效状态。如果服务器崩溃并使用此快照恢复,并且Zab重新提交状态更改,则结果状态对应于崩溃前的服务状态。

4.4 Client-Server Interactions

当服务器处理写请求时,它还会发送和清除与该更新相对应的任何watch的通知。服务器按顺序处理写操作,而不同时处理其他写操作或读操作。这确保了通知的严格连续性。注意,服务器在本地处理通知。只有客户端连接到的服务器才能跟踪并触发该客户端的通知。

读取请求在每个服务器的本地处理。每个读取请求都被处理并使用zxid标记,该zxid对应于服务器看到的最后一个事务。这个zxid定义了读请求相对于写请求的部分顺序。通过本地处理读取,我们获得了优异的读取性能,因为它只是本地服务器上的内存操作,不需要运行磁盘活动或一致协议。这种设计选择是在以读为主的工作负载下实现卓越性能目标的关键。

使用快速读取的一个缺点是不能保证读取操作的优先顺序。也就是说,读操作可能返回一个过时的值,即使已经提交了对同一znode的最新更新。并不是所有的应用程序都需要优先顺序,但是对于需要优先顺序的应用程序,我们实现了同步。该原语异步执行,并在对其本地副本执行所有挂起的写操作之后由leader排序。为了保证给定的读操作返回最新的更新值,客户端在读操作之后调用sync。客户端操作的FIFO顺序保证和全局同步保证使读操作的结果能够反映发出同步之前发生的任何更改。在我们的实现中,我们不需要原子广播同步,因为我们使用基于leader的算法,我们只是将同步操作放在leader和执行同步调用的服务器之间的请求队列的末尾。为了做到这一点,follower必须确保领导者仍然是领导者。如果存在提交的挂起事务,则服务器不会怀疑leader。如果挂起队列为空,leader需要发出一个空事务来提交,并在该事务之后命令同步。这样做的好处是,当leader处于负载时,不会产生额外的广播流量。在我们的实现中,设置了超时,以便领导者在follower抛弃它们之前意识到它们不是领导者,因此我们不会发出null事务。

ZooKeeper服务器按照FIFO顺序处理来自客户端的请求。响应包括响应相对的zxid。即使在没有活动的间隔期间,心跳消息也包括客户端所连接的服务器所看到的最后一个zxid。如果客户端连接到一个新服务器,新服务器通过检查客户端的最后一个zxid和它的最后一个zxid来确保它的ZooKeeper数据视图至少与客户端的视图一样新。如果客户端的视图比服务器的更近,服务器在服务器跟上之前不会重新建立与客户端的会话。客户端保证能够找到另一个具有系统最新视图的服务器,因为客户端只看到已经复制到大多数ZooKeeper服务器上的更改。这种行为对于保证持久性非常重要。

为了检测客户端会话失败,ZooKeeper使用了超时(机制)。如果在会话超时内没有其他服务器从客户端会话接收到任何信息,leader将确定出现了故障。如果客户端发送请求的频率足够高,那么就不需要发送任何其他消息。否则,客户端在低活动期间发送心跳消息。如果客户端无法与服务器通信来发送请求或心跳,它将连接到另一个ZooKeeper服务器来重新建立会话。为了防止会话超时,ZooKeeper客户端库会在会话空闲时间s/3 ms后发送一个心跳,如果在2s/3ms没有收到服务器信号,则切换到新的服务器,其中s是会话超时,单位为毫秒。

5 Evaluation

我们在一个50台服务器的集群上执行了所有评估。每台服务器都有一个Xeon双核2.1GHz处理器,4GB内存,千兆以太网和两个SATA硬盘。我们将下面的讨论分为两个部分:吞吐量和请求延迟。

5.1 Throughput

为了评估我们的系统,我们对系统饱和时的吞吐量以及各种注入故障时吞吐量的变化进行了基准测试。我们改变了组成ZooKeeper服务的服务器数量,但始终保持客户端数量不变。为了模拟大量客户端,我们使用35台机器模拟250个并发客户端。

我们有ZooKeeper服务器的Java实现,以及Java和C客户端。在这些实验中,我们使用配置为记录到一个专用磁盘并在另一个专用磁盘上拍摄快照的Java服务器。我们的基准测试客户端使用异步Java客户端API,每个客户端至少有100个未完成的请求。每个请求都包含对1K数据的读取或写入。我们没有展示其他操作的基准测试,因为所有修改状态的操作的性能大致相同,而非状态修改操作(不包括同步)的性能大致相同。(同步的性能近似于轻量级写操作,因为请求必须发送给leader,但不会广播)。客户端每300毫秒发送一次已完成操作的计数,我们每6s发送样品。为了防止内存溢出,服务器会限制系统中并发请求的数量。ZooKeeper使用请求节流来防止服务器过载。在这些实验中,我们将ZooKeeper服务器配置为最多处理2000个请求。

<center>图5:饱和系统的吞吐量性能,随读写比的变化而变化</center>

Servers 100% Reads 0% Reads
13 460k 8k
9 296k 12k
7 257k 14k
5 165k 18k
3 87k 21k

<center>表1:饱和系统极端情况下的吞吐量性能</center>

在图5中,显示了当我们改变读与写请求的比率后的吞吐量,每条曲线对应于提供ZooKeeper服务的不同数量的服务器。表1显示了读取负载的极端值。读吞吐量比写吞吐量高,因为读不使用原子广播。该图还显示了服务器的数量对广播协议的性能也有负面影响。从这些图中,我们可以看到,系统中的服务器数量不仅会影响服务可以处理的故障数量,还会影响服务可以处理的工作负载。请注意,三个服务器的曲线与其他服务器的曲线交叉在60%左右。这种情况并不只存在于三服务器配置中,由于本地读取启用了并行性,所有配置都会出现这种情况。但是,在图中的其他配置中是看不到的,因为我们为可读性限制了最大y轴吞吐量。

写请求比读请求花费的时间长有两个原因。首先,写请求必须通过原子广播,这需要一些额外的处理并增加请求的延迟。处理写请求时间较长的另一个原因是,服务器必须确保在向leader发送确认之前,将事务记录到非易失性存储中。原则上,这个要求是过分的,但是对于我们的生产系统,我们用性能来换取可靠性,因为ZooKeeper构成了应用的基础。我们使用更多的服务器来容忍更多的错误。通过将ZooKeeper数据分割成多个ZooKeeper集合来增加写吞吐量。这种复制和分区之间的性能权衡已经被Gray等人观察到。

<center>图6:饱和系统的吞吐量,当所有客户端都连接到leader时,读写比率的变化</center>

ZooKeeper能够通过在组成服务的服务器之间分配负载来实现如此高的吞吐量。我们可以分配负载,因为我们宽松的一致性保证。Chubby的客户端把所有的要求都交给leader。图6显示了如果我们不利用这种宽松,并强制客户端只连接到leader会发生什么。正如预期的那样,读占主导地位的工作负载的吞吐量要低得多,但即使是写占主导地位的工作负载,吞吐量也要低得多。服务客户端导致的额外CPU和网络负载会影响领导者协调提案广播的能力,这反过来会对总体写性能产生负面影响。

<center>图7:隔离的原子广播组件的平均吞吐量。 误差线表示最小值和最大值。</center>

原子广播协议完成了系统的大部分工作,因此对ZooKeeper的性能的限制比其他任何组件都要大。图7显示了原子广播组件的吞吐量。为了对其性能进行基准测试,我们通过直接在leader上生成事务来模拟客户端,因此不存在客户端连接或客户端请求和响应。在最大吞吐量时,原子广播组件会受到CPU的限制。理论上,图7的性能可以与100%写操作的ZooKeeper的性能相匹配。但是,ZooKeeper客户端通信、ACL检查和请求事务转换都需要CPU。对CPU的争用将ZooKeeper吞吐量降低到实质上低于隔离状态下的原子广播组件。因为ZooKeeper是关键的生产组件,所以到目前为止,我们对ZooKeeper的开发重点一直是正确性和健壮性。通过消除额外副本、同一对象的多次序列化、更有效的内部数据结构等,可以有很多机会显著提高性能。

<center>图8:故障时吞吐量。</center>

为了显示随着时间的推移系统在注入故障时的行为,我们运行了一个由5台机器组成的ZooKeeper服务。我们运行了与以前相同的饱和度基准,但这一次我们将写入百分比保持在恒定的30%,这是预期工作负载的保守比率。我们定期终止一些服务器进程。图8显示了随时间变化的系统吞吐量。图中标注的事件如下:

  1. follower的故障和恢复;
  2. 不同follower的故障和恢复;
  3. leader故障;
  4. 前两个标记中有两个followers (a,b)失败,第三个标记(c)中恢复;
  5. leader故障。
  6. 恢复leader。

从这个图表中可以看到一些重要现象。首先,如果followers故障并快速恢复,那么ZooKeeper能够在失败的情况下维持高吞吐量。单个followers的故障不会阻止服务器产生仲裁(quorum),只会降低吞吐量,大致相当于服务器在失败前处理的读请求的份额。其次,我们的leader选举算法能够足够快速的恢复,以防止吞吐量大幅下降。在我们的观察中,ZooKeeper只需要不到200毫秒就可以选出一个新的leader。因此,尽管服务器会在一秒内停止服务请求,但由于采样周期(以秒为单位),我们不会观察到吞吐量为零。第三,即使followers需要更多的时间恢复,但是一旦他们开始处理请求,ZooKeeper也可以在开始处理请求后再次提高吞吐量。在事件1、2和4之后,我们不能恢复到完全吞吐量水平的一个原因是,客户端仅在其与跟随者的连接断开时才切换跟随者。因此,在事件4之后,直到leader在事件3和事件5故障,客户端才重新分配自己。实际上,随着客户的来来去去,这种不平衡会随着时间的推移自行解决。

Table 3: Barrier experiment with time in seconds. Each point is the average of the time for each client to finish over five runs.

5.2 Latency of requests

为了评估请求的延迟,我们创建了一个以Chubby基准为模型的基准测试。我们创建一个工作进程,它只发送一个create,等待它完成,发送一个对新节点的异步删除,然后开始下一个create。我们相应地改变工作程序的数量,每次运行,我们让每个工作程序创建50,000个节点。我们通过将创建请求的完成数量除以所有工作程序完成所花费的总时间来计算吞吐量。

Number of servers
Workers 3 5 7 9
1 776 748 758 711
10 2074 1832 1572 1540
20 2740 2336 1934 1890

表2显示了基准测试的结果。create请求包含1K的数据,而不是Chubby基准测试中的5字节,以便更好地符合我们的预期使用。即使有这些更大的请求,ZooKeeper的吞吐量也比Chubby公布的吞吐量高出3倍以上。单个ZooKeeper worker基准测试的吞吐量表明,3台服务器的平均请求延迟为1.2ms, 9台服务器的平均请求延迟为1.4ms。

# of clients
# of barriers 50 100 200
200 9.4 19.8 41.0
400 16.4 34.1 62.0
800 28.9 55.9 112.1
1600 54.0 102.7 234.4

<center>表3:以秒为单位的屏障实验。 每个点是每个客户完成五次运行的平均时间</center>

5.3 Performance of barriers

在这个实验中,我们依次执行了一些barrier(屏障)来评估用ZooKeeper实现的原语的性能。对于给定数量的b个barrier,每个客户首先进入所有b个barrier,然后依次离开所有b个barrier。当我们使用2.4节中的双屏障算法时,客户端首先等待所有其他客户端执行enter()过程,然后再转移到下一个调用(类似leave())。

表3展示了我们的实验结果。在本次实验中,我们有50个、100个、200个客户端陆续进入b个barrier,b ∈{200,400,800,1600}。尽管一个应用程序可以有数千个ZooKeeper客户端,但通常只有更小的子集参与到每个协调操作中,因为客户端通常根据应用程序的具体情况进行分组。

这个实验有两个有趣观察结果,处理所有barrier的时间随barrier数量的增加而线性增长,这表明并发访问同一数据树的一部分没有产生任何意外的延迟,并且延迟随客户端数量的增加而成正比。这是不使ZooKeeper服务饱和的结果。实际上,我们观察到,即使客户端以lock-step( 锁定步进)进行,barrier操作(进入和离开)的吞吐量在所有情况下都在每秒1,950到3,100个操作之间。在ZooKeeper操作中,这相当于每秒10,700到17,000个操作之间的吞吐量值。在我们的实现中,读写比率为4:1(读操作的80%),与ZooKeeper可以达到的原始吞吐量(根据图5超过40,000)相比,我们的基准代码使用的吞吐量要低得多。 这是由于客户端在等待其他客户端。

6 Related work

ZooKeeper的目标是提供减轻分布式应用程序中协调流程问题的服务。 为了实现此目标,其设计使用了以前的协调服务,容错系统,分布式算法和文件系统的思想。

我们并不是第一个提出分布式应用协调系统的人。一些早期的系统提出了一种分布式锁服务,用于事务性应用程序,以及用于在计算机集群忠共享信息。最近,Chubby提出了一个系统来管理分布式应用程序的咨询锁。Chubby和ZooKeeper有一些共同的目标。它还有一个类似文件系统的界面,并使用一致性协议来保证副本的一致性。然而,ZooKeeper并不是一个锁服务。客户端可以使用它来实现锁,但是在它的API中没有锁操作。与Chubby不同,ZooKeeper允许客户连接到任何ZooKeeper服务器,而不仅仅是leader。ZooKeeper客户端可以使用本地副本来提供数据和管理watch,因为它的一致性模型要比Chubby宽松得多。这使得ZooKeeper能够提供比Chubby更高的性能,允许应用程序更广泛地使用ZooKeeper。

为了减轻构建容错分布式应用程序的问题,文献中已经提出了一些容错系统。早期的一个系统是ISIS[5]。ISIS系统将抽象的类型规范转换为容错的分布式对象,从而使容错机制对用户透明。荷鲁斯[30]和合奏[31]是由ISIS演化而来的系统。动物园管理员拥抱ISIS虚拟同步的概念。最后,图腾保证了在一个利用局域网[22]硬件广播的体系结构中消息传递的总顺序。动物园管理员工作与各种各样的网络拓扑,这激励我们依赖TCP连接之间的服务器进程,不假定任何特殊的拓扑或硬件功能。我们也不会暴露在ZooKeeper内部使用的任何集成通信。

为了减轻构建容错分布式应用程序的问题,文献中已经提出了一些容错系统。早期有一个系统ISIS。ISIS系统将抽象的类型规范转换为容错的分布式对象,从而使容错机制对用户透明。Horus和Ensemble是由ISIS演化而来的系统。ZooKeeper包含ISIS虚拟同步的概念。最后,Totem保证了在一个利用局域网硬件广播的体系结构中消息传递的总顺序。ZooKeeper可使用多种网络拓扑,这促使我们依靠服务器进程之间的TCP连接,而不需要假设任何特殊的拓扑或硬件功能。我们也不会暴露在ZooKeeper内部使用的任何集成通信。

构建容错服务的一项重要技术是状态机复制,而Paxos是一种能够有效实现异步系统的复制状态机的算法。我们使用的算法具有Paxos的一些特征,但是它将事务日志和数据树恢复所需的预写日志结合在一起,从而完成一种高效的实现。已经有了一些协议提案,可以实际实现Byzantine-tolerant的复制状态机[7、10、18、1、28]。 ZooKeeper并不假设服务器是Byzantine,但我们确实使用了检查和和健康检查等机制来捕获非恶意的Byzantine故障。Clement等人讨论了一种不修改当前服务器代码就能使ZooKeeper完全具有Byzantine容错能力的方法。目前为止,我们还没有在产品中观察到使用完全Byzantine容错协议可以防止的错误。

Boxwood是一个使用分布式锁服务器的系统。Boxwood为应用程序提供了更高层次的抽象,它依赖基于Paxos的分布式锁服务。和Boxwood一样,ZooKeeper也是一个用于构建分布式系统的组件。然而,ZooKeeper具有高性能要求,并且在客户端应用程序中使用更广泛。ZooKeeper公开应用程序用于实现高级原语的低级原语。

ZooKeeper类似于一个小型文件系统,但它只提供了文件系统操作的一个小子集,并添加了大多数文件系统中不存在的功能,比如顺序保证和条件写。然而,ZooKeeper 在watch精神上类似于AFS的缓存回调。

Sinfonia引入了迷你事务,这是一种用于构建可伸缩分布式系统的新模型。Sinfonia被设计用来存储应用程序数据,而ZooKeeper存储应用程序元数据。 ZooKeeper保持状态完全复制并存储在内存中,以实现高性能和一致的延迟。我们对文件系统的使用,如操作和排序,让功能类似于迷你事务。znode是一个方便的我们在上面添加watch的抽象,一个功能是Sinfonia。Dynamo允许客户端在分布式键值存储中获取和放置相对较小(小于1M)的数据量。不像ZooKeeper,Dynamo中的关键空间不是分层的。Dynamo也不提供强大的持久性和一致性保证或写入,而是解决了读取冲突。

DepSpace使用一个元组空间来提供Byzantine容错服务。像ZooKeeper一样,DepSpace使用一个简单的服务器接口在客户机上实现强同步原语。虽然DepSpace的性能远低于ZooKeeper,但它提供了更强的容错性和机密性保证。

7 Conclusions

ZooKeeper通过将无等待对象公开给客户端,来解决分布式系统中协调进程的问题。我们发现ZooKeeper在Yahoo!内部和外部的多个应用中都很有用。ZooKeeper通过使用watch的快速读取,为以读取为主的工作负载实现了每秒数十万次操作的吞吐量值,这两种操作都由本地副本提供。虽然我们的一致性保证读取和watch似乎很弱,但是我们已经展示的用例表明,即使读取没有优先级排序并且数据对象的实现也是如此。事实证明,免等待属性对于高性能至关重要。

虽然我们只描述了少数几个应用,还有许多其他使用ZooKeeper的应用。我们相信这样的成功是由于它简单的接口以及可以通过这个接口实现强大的抽象。此外,由于ZooKeeper的高吞吐量,应用程序可以广泛使用它,而不仅仅是粗粒度锁。

Acknowledgements

我们要感谢Andrew Kornev和Runping Qi为ZooKeeper做出的贡献;Zeke Huang和Mark Marchukov提供的有价值的反馈;布莱恩·库珀和劳伦斯·拉蒙蒂亚努感谢他们对ZooKeeper的早期贡献;Brian Bershad和Geoff Voelker对演讲做了重要的评论。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章