zookeeper


摘要(Abstract)

在本文中,我们描述了一种用于协调分布式应用程序的服务ZooKeeper。 作为关键基础设施的一部分,ZooKeeper旨在提供一个简单和高性能的内核,使得客户端可以构建更复杂的协调原语。 它将组消息传递、共享寄存器和分布式锁等服务整合到一个重新分配的、集中的服务中。 由ZooKeeper暴露出来的接口在共享寄存器方面具有无等待的特性,使用类似于分布式文件系统缓存失效的事件驱动机制来提供简单但是强大的协调服务。

ZooKeeper接口支持高性能的服务实现。 除了无等待属性之外,ZooKeeper 还为每个客户端提供 FIFO 执行请求的保证。将所有改变ZooKeeper 状态的请求线性化。 这些设计使得实现高性能处理流水线成为可能,满足了本地服务器读的请求。 我们显示了目标工作负载,2:1到100:1读取写入比率,ZooKeeper可以每秒处理数以十万计的事务。这个性能使得ZooKeeper可以被客户端应用程序广泛使用。

1、简介(Introduction)

大规模分布式应用需要不同形式的协调。 配置是最基本的协调形式之一。 在这种简单形式的协调中,配置只是系统进程的操作参数列表,而更复杂的系统需要具有动态配置参数。 群组的成员关系和领导选举在分布式系统中也很常见:通常进程需要知道哪些其他进程是有效的。锁构成了一个强大的协调原语(coordination primitive),它实现了对关键资源的互斥访问。

注:primitive,翻译为原语,在系统设计中经常会出现这样的术语,指的是系统对外提供的服务中,不可分割的最小部分,类似于API。例如对于OS而言,read、write函数操作都是系统原语(System Primitive)。对zk而言也是如此。

一种协调办法是为每个不同的协调需要开发服务。 例如,Amazon Simple Queue Service[3]专门用于排队。 其他服务专门用于领导选举 [25] 和配置 [27]。 可以通过使用实现更强大的原语的服务来实现较弱的原语。 例如,chubby [6] 是一个具有强同步保证的锁定服务。 然后锁可以用来实现领导选举、群组的成员关系等。

在设计我们的协调服务时,我们没有在服务器端实现特定的原语,而是选择了暴露特定的 API,使应用程序开发人员能够实现自己的原语。 这样的选择导致了一个协调内核的实现,使得新的原语的实现不需要对服务核心的改变。 这种方式使得多种形式的协调能够适应应用的需求,而不是将开发者约束到固定的原语集合。

在设计ZooKeeper的 API 时,我们放弃了阻塞原语,比如锁。 除其他问题外,协调服务的阻塞原语可能导致,较慢或有故障的客户端对快速客户端的性能造成负面影响。如果处理请求依赖于其他客户端的响应和失败检测,则服务本身的实现变得更加复杂。我们的系统 ZooKeeper,因此实现了一个 API,它可以操作像文件系统那样分层组织的简单的无等待数据对象。 实际上,ZooKeeper API 类似于任何其他文件系统,并且如果只看API函数签名,ZooKeeper似乎是没有锁的Chubby 然而,实现无等待数据对象的ZooKeeper与基于锁定原语(如lock)的系统显著区分开来。

注、

1:”wait free objects”无等待数据对象,指的是可以被多个客户端并发访问的数据结构,而无需任何形式的同步或协调

在无等待系统中,每个客户端都保证向其目标取得进展,而不管其他客户端正在做什么。这意味着客户端可以在有限的时间内完成其操作,而不管访问相同数据结构的其他客户端的数量。这与其他形式的并发控制(如锁或信号量)形成对比,在那里客户端可能被阻塞或必须等待其他客户端完成其操作才能继续。

2:Chubby是一种高可用性分布式锁服务,用于确保享资源在任何时候只有一个客户端可以访问。

虽然无等待特性对性能和容错是很重要的,但对于协调(coordination)是不够的。 我们还必须提供操作的顺序保证。特别地,我们发现,保证所有操作的 FIFO 客户端顺序和可线性化的写入能够有效地实现这个服务,并且足够实现我们应用程序感兴趣的协调原语。借助api,事实上我们可以对任何数量进程达成共识,根据 Herlihy 的层次结构,ZooKeeper 实现了一个通用对象 [14]

ZooKeeper 服务是一个服务器集群,使用复制来实现高可用性和高性能,其高性能使得包含大量进程的应用能够使用这样的协调内核来管理协调的方方面面。 我们能够实现一个简单的流水线架构的ZooKeeper ,允许我们有成百上千的请求,仍然低延迟。 这样的流水线自然能够以 FIFO 顺序从单个client执行操作。 确保 FIFO 客户端顺序使客户端能够异步提交操作。 通过异步操作,客户端一次能够有多个未完成的操作。 这个功能是合乎需要的,当一个新的客户机成为leader时,它必须对元数据进行处理并相应地更新元数据。 没有多个未完成的操作的可能性,初始化时间可以是秒的数量级而不是毫秒。

在客户端缓存数据是提高读取性能的一项重要技术。 例如,在每次需要了解leader时,对当前leader的标识符进行缓存而不是探查ZooKeeper 的过程是有用的。 ZooKeeper 使用监视机制使客户端能够缓存数据,而无需直接管理客户端缓存。 通过这种机制,客户端可以监视给定数据对象的更新,并在更新时接收通知。 Chubby直接管理客户端缓存。 它阻塞更新来使所有客户端缓存无效并缓存被改变的新数据。 在此设计下,如果这些客户端中的任何一个是缓慢或有故障的,更新被延迟。 chubby 使用租约来防止一个错误的客户端对该系统进行无限期的阻塞。 然而,租约只约束了缓慢或有缺陷的客户的影响,而ZooKeeper 则完全避免了问题。

本文讨论了ZooKeeper的设计与实现。 使用 ZooKeeper ,我们能够实现我们的应用程序所需要的所有协调原语,即使只有写入是可线性化的。 为了验证我们的方法,我们展示了如何用ZooKeeper实现一些协调原语。

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

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

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

协调经验:我们分享了我们使用ZooKeeper 的方法,并评估了它的性能。

2、Zookeeper服务(The ZooKeeper service)

客户端使用 ZooKeeper 客户端库通过客户端 API 向ZooKeeper提交请求。 除了通过客户端 API 发布ZooKeeper服务接口以外,客户端库还管理客户端和ZooKeeper服务器之间的网络连接。

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

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

2.1、服务概述(Service overview)

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

客户端可以创建两种类型的 Znode:

普通节点 : 客户机通过显式地创建和删除来操作普通 Znode;

临时节点 : 客户机创建这样的 Znode之后,要么显式地删除它们,要么当创建它们的会话终止时让系统自动删除它们 (有意的或意外)。

另外,当创建新的 Znode 时,客户端可以设置顺序标志。 用序列标记集创建的节点具有附加到其名称的单调递增计数器的值。 如果 N 是新的 Znode 并且 P 是父 Znode,那么 N 的序列值决不会小于在 P 下创建的任何其他顺序 Znode 的名称中的值。

ZooKeeper 实现了watch机制,允许客户在不需要轮询的情况下及时收到更改通知。当一个客户端使用一个watch标志集进行读取操作时,操作将正常完成,并且服务器承诺在返回的信息发生更改时通知客户机。Watches 是与会话相关联的一次性触发器,一旦被触发或会话关闭,它们就被注销。 watches会通知发生了一个更改,但不会告知具体的更改内容。 例如,如果一个客户机在“/ foo”发生两次更改之前发出getData(“/ foo”,true),那么客户机将得到一个watch事件,告诉客户机“/ foo”的数据发生了更改。 会话事件,例如连接丢失事件,也会被发送到监视回调函数,以便客户知道(已注册的其他)watches事件可能被延迟。

数据模型:ZooKeeper的数据模型本质上是一个具有简化 API 和仅完整数据读取和写入的文件系统,或是一个具有层次键的key/value表。 分层命名空间对于定位不同应用程序的命名空间子树和对这些子树设置访问权限是有用的。 我们还利用客户端的目录概念来构建更高级的原语,正如我们将在 2.4 节中看到的。

与文件系统中的文件不同,Znode 不是为通用数据存储设计的。 相反,Znode 是客户端应用程序的抽象,即用来协调应用的元数据(meta-data)。 在图 1 中,我们有两个子树,一个用于应用程序 1(/ app1),另一个用于应用程序 2(/ app2)。 应用程序 1 的子树实现了一个简单的组成员身份协议 : 每个客户端进程 pi 在/ app1下创建一个 Znode p_i,它在进程运行的时候一直会存在。

虽然 Znode 还没有设计用于一般的数据存储,但是 ZooKeeper 确实允许客户端存储一些可以用于分布式计算中的元数据或配置信息。 例如,在主从架构(leader-based)的应用中,知道哪个服务器是leader对应用服务器是有用的。 为了实现这个目标,我们可以让当前的leader在 Znode 空间中的已知位置写入这个信息。 Znode 还具有与时间戳和版本计数器相关的元数据,它允许客户端跟踪 Znode 的改变,并基于 Znode 的版本执行进行有条件的更新。

会话:客户端连接到ZooKeeper之后会启动会话。 会话有关联的超时时间。 如果ZooKeeper在一定时间内没有从收到会话消息,ZooKeeper会认为客户有故障。 当客户端显式地关闭会话句柄或 ZooKeeper 检测到客户端有故障时,会话结束。 在会话中,客户观察一系列反映其运行执行情况的状态变化。 会话使客户端能够在 ZooKeeper集群中从一个物理服务器透明地移动到另一个物理服务器上,因此会话能在 ZooKeeper 集群上持续存在。

2.2、客户端API(Client API)

下面我们将介绍ZooKeeper API的相关子集,并讨论每个请求的语义:

**create(path, data, flags)**:创建在路径path下的znode,data是要存放的数据,并返回新znode的名称。标志flag可以让客户端选择znode的类型:普通的、临时的,并设置顺序标志。

**delete(path, version)**: 删除给定路径和版本下的znode。

**exists(path, watch)**:如果有路径名路径的znode存在,返回true,否则返回false。watch参数允许客户端在znode上设置监视器。

**getData(path, watch)**:返回与znode相关的数据和元数据,如版本信息。watch标志的工作方式与exists()相同(),但是如果znode不存在的话,ZooKeeper就不会设置监视器。

**setData(path, data, version)**: 如果版本号是version,写入数据data[]到znode路径。

**getChildren(path, watch)**:返回一个znode的子节点集。

**sync(path)**:把所有在sync之前的更新操作都进行同步,达到每个请求都在半数以上的 ZooKeeper 服务器上生效。path参数目前没有用。

所有方法都有同步版本和异步版本,可通过 API 获得。 应用程序在执行单个ZooKeeper操作且没有并发任务时往往会使用同步 API,因此让ZooKeeper阻塞是必要的。 但是,异步 API 可以使应用程序进行多个未完成的 ZooKeeper 操作和其他并行任务。 ZooKeeper 客户端保证每个操作的相应回调函数是按顺序被调用的。

注意,ZooKeeper不使用句柄来访问 Znode,而是通过完整路径来访问。 这个选择不仅简化了 API(没有 open () 或 close () 方法),而且还消除了服务器需要维护的额外状态。

每个更新方法都采用预期的版本号,这使得可以实现条件更新。 如果 znode 的实际znode与预期版本号不匹配,则更新失败,报版本不一致的错误。 如果版本号为 -1,则不执行版本检查。

2.3、Zookeeper的保证(Zookeeper guarantees)

ZooKeeper有两个基本的顺序保证: 线性写:所有更新ZooKeeper状态的操作是串行的,先来先服务; FIFO客户端顺序:来自客户端的所有请求按客户端发送的顺序依次执行。

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

要了解这两个保证如何交互,请考虑以下场景。 包括多个进程的系统选举leader来命令worker进程。 当新的leader负责该系统时,它必须改变大量的配置参数,并且一旦它完成就会通知其他进程。 我们有两个重要的要求:

  1. 当新的leader进行更改时,我们不希望其他进程使用正在被改变的配置;
  2. 如果新的leader在配置被完全更新之前宕机,我们不希望其他进程使用这个被部分更新的配置。

注意,分布式锁,如 Chubby 提供的锁,将有助于满足第一个要求,但满足不了第二个。 在 ZooKeeper 中,新的leader可以指定一个路径作为Ready Znode;其他进程只在Ready Znode 存在时才使用这个配置。新的leader通过删除Ready Znode、更新各种配置 Znode 和创建Ready Znode来使配置改变。 所有这些更改都可以流水线化并异步发布以快速更新配置状态。尽管改变操作的等待时间是 2 毫秒,但是如果请求被一个接一个地发布,则更新 5000 个不同的 Znode 的leader将必须花费 10 秒;通过异步地发出请求将花费少于一秒。 由于有顺序的保证,如果一个进程看到Ready Znode,它也必须看到新leader所做的所有配置更改。 如果新的Leader在创建Ready Znode 之前宕机,其他进程知道配置尚未完成并且不使用它。(Hades注:Ready Znode相当于一个分布式锁)

上述方案仍然存在问题 : 如果一个进程在新的leader开始进行改变之前已经看到ready存在,并且在改变正在进行时开始读取配置,会发生什么。 该问题由通知的顺序来保证:如果客户端正在监视变更事件,则客户端将在做出改变之后、看到系统的新状态之前收到通知事件。因此,如果读取Ready Znode的进程同时也监听了在该Znode上发生的更改,它将在读取任何新配置之前,收到Ready Znode被更改的通知。

另一个问题可能出现在客户除了ZooKeeper还有自己的通信渠道。 例如,考虑在ZooKeeper中具有共享配置的两个客户端 A 和 B,并且通过共享信道进行通信。 如果 A 更改了 ZooKeeper 中的共享配置并通过共享信道告知 B 更改,则 B 将会在重新读取配置时看到更改。 如果 B 的ZooKeeper 副本稍微落后于 A,则可能看不到新的配置。 通过上述保证,B可以确保在重新读取配置之前,它可以通过发出一个写来查看最新的信息。 为了更有效地处理这个场景,ZooKeeper 提供sync请求:当sync之后有一个read操作时,它俩会构成一个慢读取操作。sync使一个服务器在执行read操作之前会执行所有被挂起的write请求,而没有一个完整写的开销。 这种原语的思想与 ISIS [5] 的原语相似。

ZooKeeper 还具有以下两个可用性和持久性保证:

  • 如果半数以上的 ZooKeeper 服务器是活跃的,则集群服务是可用的;
  • 如果ZooKeeper 服务成功地响应了改变状态的请求,那么只要失败的服务器是可恢复的,则该变更就会在任何数量的故障服务器中被持久化。

2.4、原语的例子(Examples of primitives)

在本节中,我们展示了如何使用 ZooKeeper API 来实现更强大的原语。 ZooKeeper 服务对这些更强大的原语一无所知,因为它们完全是在 ZooKeeper客户端 API 上实现的。 一些常见的原语,如组成员身份和配置管理,也是无等待的。 对于其他的人,如集合点,客户需要等待事件。 即使ZooKeeper 是免等待的,我们可以使用ZooKeeper实现有效的阻塞原语 ZooKeeper的顺序保证允许有效的推理系统状态,watches 允许有效等待。

配置管理(Configuration Management)

在分布式应用中,可以使用ZooKeeper 来实现动态配置。 在其最简单的形式中,配置被存储在 Znode Zc 中。 进程以 Zc 的完整路径名开始。 启动进程通过读取 Zc 而获得它们的配置,其中watch设置为 True。 如果更新了 Zc 中的配置,则通知进程处理并读取新配置,再次将watch标志设置为True。

注意,在这个方案中,在使用watches的大多数其他方案中,使用watches来确保进程具有最新的信息。 例如,如果监视 Zc 的进程被通知Zc有改变,并且在读取新的 Zc 之前存在对 Zc 的三个或者更多改变,则该进程不会接收三个或者更多通知的事件。 这并不影响进程的行为,因为这三个事件只是简单地通知进程它已经知道的事情 : 它对于 Zc 的信息是陈旧的。

汇合(Rendezvous)

有时在分布式系统中,最终的系统配置将看起来是什么样子并不总是清楚的。 例如,客户机可能想要启动主进程和几个工作进程,但是启动进程是由调度程序完成的,因此客户机不知道在时间之前的信息,诸如地址和端口,这些用来给予worker进程连接到主设备的信息。 我们用ZooKeeper来处理这个场景,Znode Zr 是由客户端创建的节点。 客户机将 Zr 的完整路径名作为master和worker进程的启动参数传递。 当主控器开始填充 Zr 时,它就会使用关于地址和端口的信息。 当worker开始工作时,他们读了 Zr,watch设置为真。 如果尚未填写 Zr,则在 Zr 更新时,worker会被通知。 如果 Zr 是短暂节点,则主进程和worker进程可以在客户端结束时监视 Zr 被删除并清除自己。

群组关系(Group Membership)

我们利用临时节点来实现组成员身份。 具体而言,我们使用临时节点基于它允许我们看到创建节点的会话状态的事实。我们开始设计一个 Znode,Zg 来代表这个组。 当组的进程成员开始时,它在Zg 下创建临时子 Znode。 如果每个进程都有唯一名称或标识符,则该名称被用作子 Znode 的名称;否则,进程将创建带有顺序标志的 Znode,以获得唯一名称分配。 进程可以将进程信息放入进程所使用的子 Znode、地址和端口的数据中。

在 Zg 下创建子 Znode 之后,进程正常开始。 它不需要做任何别的事情。如果进程失败或结束,则自动删除表示它的 Znode。

进程可以通过简单列出 Zg 的孩子来获得群组的信息。 如果进程想要监视组成员身份的改变,则该进程可以在接收到改变通知时将watch标志设置为True,并且刷新群组信息 (watch标志总是被置为True)。

简单锁(Simple Locks)

虽然ZooKeeper 不提供锁服务,它可以用来实现锁。 使用ZooKeeper的应用程序通常使用针对其需要定制的同步原语,如上面所示。 在这里,我们演示如何使用 ZooKeeper 实现锁,以显示它可以实现广泛的各种通用同步原语。

最简单的锁实现使用 “锁文件 ”。锁由一个 Znode 表示。 为了获取锁,客户端尝试创建具有短暂标志的指定 Znode。 如果创建成功,则客户端持有锁。 否则,客户端使用watch标志来读取 Znode以得到通知,如果当前的leader死亡,客户端在其死亡或非法删除 Znode 时释放锁。 其他正在等待锁的客户端一旦观察到被删除的 Znode,就会再次尝试获取锁。

虽然这种简单的锁定协议工作,但确实存在问题。 首先,它受到羊群效应的影响。 如果有许多客户端等待获取锁,即使只有一个客户端可以获取锁,它们都会在释放锁的时候争夺锁。 第二,它只实现了排他锁。 下面的两个原语说明如何克服这两个问题。

无羊群效应的简单锁(Simple Locks without Herd Effect)我们定义一个锁znode l来实现这样的锁。直观上,我们排列所有请求锁的客户端,每个客户端都按请求到达的顺序获得锁。因此,希望获得锁的客户如下:

在锁定操作的第一行中使用SEQUENTIAL标志,使得客户端的请求与其他的请求按顺序请求锁。 如果客户端的 Znode 在第 3 行具有最低序列号,则客户端持有锁。 否则,客户端要么等待具有锁的 Znode 被删除,要么将在该客户端的znode之前接收到该锁。通过只监视客户端的znode之前的znode,我们只在释放锁或放弃锁请求时唤醒一个进程,从而避免了羊群效应。 一旦客户端监视的 Znode 消失,客户端必须检查它现在是否持有锁。 (先前的锁定请求可能已经被放弃,并且存在具有较低序列号的 Znode 仍然等待或持有锁)。

释放锁与删除 表示锁请求的 Znode N 一样简单。 通过使用创建时的EPHEMERAL标志,崩溃的进程将自动清除任何锁请求或释放他们可能拥有的任何锁。 总之,这种锁定方案具有以下优点 :

  1. Znode 的移除仅导致一个客户端醒来,因为每个 Znode 被恰好另一个客户端监视,所以我们没有羊群效应;
  2. 没有轮询或超时;
  3. 我们实现锁的方式,使得我们可以通过浏览 ZooKeeper的 数据来查看锁竞争、中断锁并debug锁中存在的问题。

读写锁(Read/Write Locks)为了实现读写锁,我们稍微改变了锁相关的程序代码,并有独立的读锁和写锁过程。解锁过程与普通锁的情况相同。

这段锁程序代码与先前的代码稍有不同。 写锁只在命名方面不同。 由于读锁可以被共享,所以第三行和第四行稍稍有改变,只有较早的写锁 Znode 才能阻止客户端获取读锁。 如果有几个客户端等待读锁,并且在删除具有较低序列号的 “write - ”znode 时得到通知,这似乎出现会“惊群效应 ”;实际上,这是我们所需要的行为,所有读操作的客户端都应该被释放,因为它们现在有了锁。

双重屏障(Double Barrier)双重屏障使客户端能够同步一个计算的开始和结束。当由屏障阈值定义的足够多的进程加入屏障时,进程开始计算,并在完成后离开屏障。 我们用 Znode 来表示ZooKeeper的屏障,称为 B。 每一个进程 P 都在 B 通过创建一个 Znode 作为 B 的孩子来注册,在它准备好离开时取消注册。 当 B 的子 Znode 的数目超过屏障阈值时,进程可以进入屏障。 进程可以在所有的进程都已移除其孩子时离开屏障。 我们使用监视器高效地等待进入和退出条件得到满足。 如果要进入屏障的话,进程需要监视 B 的准备好的子节点,当子节点数量超过屏障阈值时,该进程才可以进入。 如果要离开屏障,进程监视某个特定的孩子消失,并且只有在这个特定的 Znode 被删除后才检查退出条件。

3、ZooKeeper的应用(ZooKeeper Applications)

现在我们描述一些使用ZooKeeper的应用程序,并简要说明它们的使用。 我们以粗体显示每个例子的原型。

爬取服务(The Fetching Service)

爬虫是搜索引擎的一个重要组成部分,而 Yahoo! 抓取数十亿的 Web 文档。 获取服务(FS)是 Yahoo! 爬虫的一部分,目前正在生产环境中。 本质上,它有一个主进程,来指挥页面抓取进程。 主节点为抓取器(fetcher)提供配置,抓取器将其状态和健康状况写回来。使用 ZooKeeper for FS 的主要优点是从主机的故障恢复,尽管失败了,但仍然保证可用性,并且将客户端与服务器分离,从而允许他们通过从ZooKeeper中读取他们的状态来引导他们重新搜索健康服务器。 因此,FS 使用ZooKeeper 主要是管理配置元数据,虽然它还使用ZooKeeper 选举master(leader选举)。

图 2 显示了 FS 使用的ZooKeeper服务器的读取和写入流量,为期三天。 为了生成这个图,我们计算周期期间每秒的次数,每个点对应于那一秒操作的次数。 我们观察到,读取流量比写入流量高得多。 在速率高于每秒 1, 000 次操作的时段期间,读取 : 写入比率在 10∶1 到 100∶1 之间变化。 在此工作负载中的读取操作是 GetData ()、GetPages () 和 Exists (),以增加的顺序。

Katta

Katta [17] 是一个使用 ZooKeeper协调的分布式索引器,它是一个非雅虎应用程序的例子。 Katta 通过使用碎片划分了索引的工作。 master将碎片分配给slaves并跟踪进度。 slave可能失败,所以这时master必须重新分配负荷。 master也可能失败,因此其他服务器必须准备好在故障的情况下接管。 Katta 使用ZooKeeper跟踪slave服务器和master服务器的状态(组成员身份),并处理master故障转移(leader选举)。 Katta 还使用ZooKeeper跟踪和分配数据分片给slave服务器(配置管理)。

Yahoo! Message Broker

雅虎消息代理 (YMB) 是分布式发布 - 订阅系统。 该系统管理数千个主题,客户端可以发布消息并接收消息。 主题分布在一组服务器中以提供可扩展性。 使用主备份方案复制每个主题,确保将消息复制到两个机器中以确保可靠的消息传递。 组成 YMB 的服务器使用无共享的分布式体系结构,这使得协调对于正确操作至关重要。 YMB 使用ZooKeeper来管理主题的分布(配置元数据),处理系统中机器的故障 (故障检测和组成员身份),以及控制系统操作

图 3 显示了 YMB 的 Znode 数据布局的一部分。 每个broker domain都有一个 Znode,称为节点,节点具有组成 YMB 服务的每个活动服务器的临时 Znode。 每个 YMB 服务器节点下创建一个短暂 Znode,它具有负载和状态信息,通过 ZooKeeper 提供组成员资格和状态信息。 所有组成服务的服务器都对禁用的节点(如禁用和迁移)进行了维护,并允许对 YMB 进行集中控制。 每个主题在topics下都有一个子节点。这些 Znode 指示了每个主题的主服务器和备份服务器和该主题的订阅者。 主服务器和备份服务器 Znode 不仅允许服务器发现那个负责某一个主题的服务器,而且还管理领导选举和服务器崩溃。

4、ZooKeeper的实现(ZooKeeper Implementation)

ZooKeeper通过在构成服务的每个服务器上复制 ZooKeeper 数据提供高可用性。 我们假设服务器崩溃,服务器可能会恢复。 图 4 显示了ZooKeeper 服务的高级组件。 接收请求后,服务器将准备执行(请求处理器)。 如果这样的请求需要服务器之间的协调 (写请求),那么它们使用一致性协议 (原子广播的实现,即ZAB协议),并且最终服务器将更改提交到ZooKeeper集群的所有服务器的数据库上。 在读取请求的情况下,服务器简单地读取本地数据库的状态并生成对请求的响应。

复制数据库(replicated database)是包含整个数据树的内存数据库。 树中的每个 Znode 默认存储最多 1MB 的数据,但是这个最大值是在特定情况下可变的配置参数。 对于可恢复性,我们有效地将更新记录到磁盘上,并且在磁盘介质被应用到内存数据库之前,我们强制写入磁盘介质。 事实上,与 chubby [8]相比,我们保留了提交的操作的重放日志(在我们这里是预写日志),并生成内存中数据库的定期快照。

每个ZooKeeper 服务器都能服务客户端。 客户端连接一个服务器来提交请求。 正如我们前面提到的,使用每个服务器数据库的本地副本来服务读取请求。 改变服务状态的请求,即写请求,由ZAB协议处理。

作为协议的一部分,写请求被转发到一个称为 leader 的服务器。 其余的ZooKeeper服务器,称为Follower,它们从leader接收消息(主要是状态),并商定状态更改。

4.1、请求处理器(Request Processor)

由于消息层是原子的,我们保证本地副本从不偏离,尽管在任何时间点,一些服务器可能比其他服务器应用更多的事务。 与从客户端发送的请求不同,事务是幂等的。 当leader接收到写请求时,它计算当应用写入时系统的状态将是什么,并且将它转换成捕获这个新状态的事务。 由于可能存在尚未应用于数据库的可能存在的事务,因此必须计算该状态。 例如,如果一个客户端在请求中的条件 setData 且其版本号能够与正在更新的 Znode 的未来的版本号相匹配,则该服务生成一个 SetDataTxN,该 SetDataTxN 包含新数据、新版本号和更新的时间戳。 如果出现错误,如不匹配版本号或要更新的 Znode 不存在,则改为生成 ErrorTXN。

4.2、原子广播(Atomic Broadcast)

所有更新ZooKeeper状态的请求都被转发到leader。 leader执行这个请求并通过 一个原子广播协议ZAB [24] 向ZooKeepeer广播变更。接收客户端请求的服务器,在传递其对应的状态改变时,响应客户端。 ZAB 用大多数的意见来决定一个建议,所以只有大多数服务器是正确的, Zab 和 ZooKeeper 才能工作(即,用 2f + 1 个服务器我们可以容忍 f 个服务器的故障)。(注:这里关于半数的描述与Raft协议极为相似,或者说ZooKeeper是构建在Raft协议之上的一个应用程序

为了获得高吞吐量,ZooKeeper 试图保持请求处理流水线化。 它可能会处理成千上万个位于管道不同部分的请求。 由于状态改变依赖于应用先前状态改变,所以 ZAB 提供比常规原子广播更强的次序保证。 更具体地,ZAB 保证由leader广播的改变按照它们被发送的顺序递送,并且来自先前leader的所有改变在广播其自身改变之前被递送到一个已经建立的leader。

有一些实现细节简化了我们的实现并给我们出色的性能。我们使用 TCP 为我们的传输层协议,所以消息顺序由网络主导,这允许我们简化我们的实现。我们使用由 ZAB 选举出的leader作为ZooKeeper的leader,以便与创建事务相同的进程也提出。 我们使用日志来跟踪提议,作为内存中数据库的预写日志(write-ahead log,WAL),这样我们就不必两次将信息写入磁盘。

Write-ahead log(WAL)是一种常见的技术,用于在数据库或其他应用程序中记录事务性操作。在WAL中,所有的写操作都首先被记录到一个日志文件中,然后再被写入到磁盘或内存中。

4.3、复制数据库(Replicated Database)

每个副本都有一个在内存里的ZooKeeper 状态的副本。 当管理员服务器从崩溃中恢复时,它需要恢复此内部状态。 在运行服务器一段时间后,重放所有日志来恢复状态将非常耗时,所以 ZooKeeper 使用定期快照,只需要自快照开始后重新发送消息。 我们称ZooKeeper的快照为模糊快照(fuzzy snapshots),因为我们不锁定 ZooKeeper 状态来获取快照;相反,我们首先进行树的深度扫描,以原子读取每个znode 的数据和元数据并将它们写入磁盘。 由于所得到的模糊快照可能已经应用了在生成快照期间所传递的状态改变的一些子集,所以结果可能不对应于在任何时间点的ZooKeeper 的状态。 然而,由于状态变化是幂等的,所以只要我们按顺序应用状态改变,我们就可以应用两次。(Hades注:在大多数分布式系统中经常采用的两种复制策略,增量复制和全量复制,全量复制可能需要日志压缩

具体来说,第一次应用状态更改是在恢复快照后,将快照包含的状态更改应用到ZooKeeper状态中;第二次应用状态更改是将快照之后未包含在快照中的状态更改重新应用到ZooKeeper状态中。通过这种方式,可以确保所有状态更改都被应用,并恢复完整的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、客户端与服务器端的交互

当服务器处理写请求时,它还发送并清除与该更新相对应的任何watch相关的通知。Servers 按顺序处理写入, 并且不同时处理其他写入或读取。 这确保了通知的严格连续。 注意服务器只处理本地通知。 只有客户端所连接到的服务器才跟踪并触发该客户端的通知。(Hades注:所有的写操作都会转发到leader上处理,因此也就能保证串行化了

Read 请求在每个服务器上本地处理。 每个读请求都被处理并标记有一个 zxid,该 zxid 对应于服务器看到的最后一个事务。 此 zxid 定义读取请求的部分顺序,并重新检查写入请求。 通过本地处理读取,我们获得了优异的读取性能,因为它只是本地服务器上的内存操作,并且没有读的磁盘的操作或者协议去运行。 这样设计是实现具有读为主要工作负载的优良性能的目标的关键。

使用快速读取的一个缺点是不能保证读取操作的优先顺序。 也就是说,即使已经提交了对同一 Znode 的最近更新,读操作也可以返回旧值。 并不是所有的应用程序都需要优先顺序,但是对于需要优先顺序的应用程序,我们实现了 sync。 此原语异步执行, 并由领导在所有挂起的写入其本地副本之后对其进行排序。 为了保证给定的读操作返回最新的更新值,client在每次读操作之前调用一个sync操作。 客户端操作的 FIFO 顺序保证以及同步的全局保证使得读取操作的结果能够反映在调用同步之前发生的任何改变。 在我们的实现中,我们不需要原子广播同步,因为我们使用基于leader的算法,并且我们简单地将同步操作放置在请求队列的末端,在leader和follower之间进行调用。 为了实现这一目标,follower必须确保leader仍然是leader。如果存在提交的挂起事务,则服务器不会怀疑leader。 如果挂起的队列为空,领导需要发出一个空事务来提交,并把sync排在该事务之后同步。这具有良好的特性,即当leader处于高负载之下时,集群不会生成额外的广播流量。 在我们的实现中,设置超时可以使得leader在follower放弃他们之前意识到他们不是leader,因此我们不发布空事务。

ZooKeeper 服务器以 FIFO 顺序处理来自客户端的请求,并响应与之相关的 zxid。 甚至在没有活动的间隔期间的心跳消息,也包括了客户端连接到的服务器看到的最后一个 zxid。 如果客户端连接到新服务器,则该新服务器通过对照客户端的最后 zxid 检查客户端的最后 zxid 来确保其对 ZooKeeper 数据的状态视图至少与客户端的状态视图一样新。 如果客户端具有比服务器更新的状态视图,则服务器直到追赶上才与客户端建立连接。 由于客户端只看到已经复制到大多数 ZooKeeper 服务器的更改,因此保证客户端能够找到具有系统最新视图的另一服务器。 这种行为对保证持久性很重要。

为了检测客户端会话失败,ZooKeeper 使用超时机制。 如果在会话超时内没有其他服务器从客户端接收到任何信息, leader将确定发生了故障。 如果客户端发送的请求足够频繁,则不需要发送任何其他消息。 否则,客户端在低活动期间发送心跳消息。 如果客户端无法与服务器发送请求或心跳的服务器通信,则它将连接到不同的 ZooKeeper 服务器以重新建立其会话。 为了防止会话超时,ZooKeeper 客户端库在会话空闲 s / 3ms 之后发送心跳,如果没有在2s / 3ms时间内收到服务器的消息,则切换到新服务器。这里的 s 代表会话超时时间,单位是ms。

5、评估(Evaluation)

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

5.1、吞吐量(Throughput)

为了评估我们的系统,我们测试了系统饱和时的吞吐量和各种故障时的吞吐量变化。我们改变了组成 ZooKeeper 服务的服务器的数量,但是总是保持客户端的数量相同。 为了模拟大量的客户端,我们使用 35 台机器来模拟 250 个同时的客户端。

我们有Java 实现的 ZooKeeper 服务器,Java 和 C 实现的客户端。 基于经验,我们使用Java 服务器配置日志记录到专用磁盘,并在另一个磁盘上获取快照 。 我们的基准测试客户端使用异步 Java 客户端 API,每个客户端至少有 100 个待完成的请求。每个请求由 1K 数据的读或写组成。我们不显示其他操作的benchmark,因为修改状态的所有操作的性能近似相同,而不包括sync的非状态修改操作的性能近似相同( 同步的性能与轻量级写入的性能一致,因为请求必须传递给leader,但不能广播)。 客户每 300ms 发送完成操作次数的计数,我们每 6s 采样一次。 为了防止内存溢出,服务器限制了系统中并发请求的数量。 ZooKeeper 对请求进行限流来防止服务器被淹没。 对于这些示例,我们将 ZooKeeper 服务器配置为在进程中具有最多 2000 个请求。

Benchmark是一种测试方法,用于评估计算机系统或组件的性能。它通常涉及运行一系列标准化的测试,以测量系统在不同负载下的响应时间、吞吐量、延迟等指标。Benchmark可以帮助开发人员和系统管理员了解系统的性能瓶颈,并确定如何优化系统以提高性能。在计算机科学中,Benchmark也可以用于比较不同系统或组件之间的性能差异。

在图 5 ,我们显示了当我们改变读与写的比率时的吞吐量,每个曲线对应于提供 ZooKeeper 服务的服务器的不同数量。 表 1 显示了读操作的负载上限。 读取吞吐量高于写入吞吐量,因为读取不使用原子广播。 该图还示出了服务器的数量也对广播协议的性能具有负面影响。 从这些图表中,我们观察到系统中服务器的数量不仅影响服务能够处理的故障的数量,而且影响服务能够处理的工作负载。 注意,3 servers 曲线与其他数量服务器的曲线在横轴约 60% 的时候交叉。这种情况下包括三服务器配置,并且由于启用了并行本地读取,所以配置变化会发生在所有配置中。但是,对于图中的其他配置,这是不可见的,因为我们为了可读性设置了最大 y 轴吞吐量的上限。

写入请求比读取请求花费更长的时间有两个原因。 首先,写请求必须经过原子广播,这需要一些额外的处理并增加请求的延迟。 对写请求进行更长的处理的另一个原因,是服务器必须确保在将ACK发送回leader之前将事务日志记录到非易失性存储介质。 在实际应用中,这种要求是过度的,但是对于我们的生产系统,我们用性能来换取可靠性,因为我们使用 ZooKeeper 构成应用基础。 我们使用更多服务器来保证容错。 我们通过将 ZooKeeper 数据划分成多个 ZooKeeper 整体来增加写入吞吐量。 Gray 等人先前已经观察到复制和分区之间的这种性能折衷。 [12].

ZooKeeper 能够通过在服务器集群上分配负载来实现高吞吐量。 之所以可以分配负载,是因为我们降低了一致性保证。Chubby中的客户端将所有请求定向到leader。 图 6 显示了如果我们不利用这种弱一致性,并强迫客户只连接到leader会发生什么。 正如预期的,对于以读为主的工作负载,吞吐量要低得多,但即使对于以写为主的工作负载,吞吐量也较低。 给客户端提供服务引起的额外 CPU 和网络负载,影响了leader协调选举的广播能力,这反过来也会对总体写入性能产生坏影响。

原子广播协议完成系统的大部分工作,因此比任何其他组件更多地限制了 ZooKeeper 的性能。 图 7 显示了原子广播组件的吞吐量。 为了评估其性能,我们通过直接在leader处生成事务来模拟客户端,因此没有客户端连接或客户端请求和回复。 在最大吞吐量,原子广播组件变为 CPU 绑定。 理论上,图 7 的性能将与具有 100%写入的 ZooKeeper 的性能相匹配。 但是,ZooKeeper 客户端通信、 ACL 检查和请求事务转换都需要 CPU。 CPU 的争用使得 ZooKeeper 吞吐量比单独的原子广播组件小。 由于 ZooKeeper 是一个关键的生产组件,到目前为止我们对 ZooKeeper 的开发重点是正确性和健壮性。通过消除额外副本、同一对象的多个序列化、更高效的内部数据结构等,有大量的机会显著提高性能。

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

  1. follower的故障和恢复;
  2. 另一个不同的follower的故障和恢复;
  3. leader故障;
  4. 在前两个标记下两个follow (a,b) 的故障,并且在第三标记 (c) 处恢复;
  5. leader故障。
  6. leader恢复。

这幅图中有一些重要的观察结果。

  • 首先,如果follower失败并快速恢复,则 ZooKeeper 能够维持高吞吐量,尽管失败。一个follower的失败并不能阻止服务器集群对外提供服务,并且仅粗略地通过服务器在失败之前正在处理的读请求来减少吞吐量。
  • 第二,我们的leader选择算法能够足够快地恢复,以防止吞吐量大幅下降。 在我们的工作中,ZooKeeper 需要不到 200ms 的时间来选举新的leader。 因此,尽管服务器在几分之一秒内停止服务请求,但是由于我们的采样周期的缘故(大约为秒),我们没有观察到零吞吐量。
  • 第三,即使follower需要更多的时间来重新恢复,ZooKeeper 也能够在他们开始处理请求时再次提高吞吐量。 在事件 1、2 和 4 之后,我们没有恢复到完全吞吐量水平的一个原因,是客户端仅在与follower的连接中断时切换follower。 因此,在事件 4 之后,直到leader在事件 3 和 5 处失败,client才重新分配它们自己的请求。 实际上,随着客户端连接的建立与释放,这种不平衡随着时间的推移而起作用。

5.2、请求延迟(Latency of requests)

为了评估请求的延迟,我们创建了一个以 Chubby benchmark [6] 为模型的benchmark。 我们创建了一个工作进程,它简单地发送一个 create,等待它完成,发送一个新节点的异步删除,然后启动下一个 create。 我们相应地改变worker的数量,并且对于每次运行,我们使每个worker创建 50, 000 个节点。 我们通过将完成的创建请求的数量除以所有worker完成的总时间来计算吞吐量。

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

5.3、性能屏障(Performance of barriers)

在本实验中,我们执行多个屏障来评估使用 ZooKeeper 原本的性能。 对于给定数量的屏障 b,每个客户端首先进入所有的 b 屏障,然后它连续地离开所有的 b 屏障。 当我们使用第 2. 4 节的双屏障算法时,client首先等待所有其他client执行 enter () 过程之后才进行下一个调用(类似于 leave ())。

我们在表 3 中报告了我们的实验结果。 在这个实验中,我们有 50、100 和 200 个客户相继进入数量为 b 的屏障,b属于集合 {200, 400, 800, 1600} 虽然应用程序可以有数以千计的 ZooKeeper 客户端,但通常有小得多的子集参与每个协调操作,因为客户端通常根据应用程序的特性进行分组。

本实验的两个有趣的观察结果是,处理所有屏障的时间与屏障的数量大致成正比,表明对数据树的相同部分的并发访问不会增加任何意外延迟。延迟的增长和客户端的数量成正比。 这是没使 ZooKeeper 服务饱和的结果。 事实上,我们观察到,即使在客户在锁定的步骤下下,屏障操作 (进入和离开) 的吞吐量在所有情况下都在每秒 1, 950 到 3, 100 个操作之间。 在 ZooKeeper 操作中,这对应于每秒 10, 700 到 17, 000 次操作之间的吞吐量值。 在我们的实现中,我们的读取与写入的比率为 4∶1(读取操作占 80%),与 ZooKeeper 可以实现的原始吞吐量相比,我们的benchmark代码使用的吞吐量要低得多(超过图 5 的 40, 000)。 这是由于客户端需要等待其他客户端。

6、相关工作(Related work)

ZooKeeper 的目标是提供一种服务,该服务解决了分布式应用程序中协调进程的问题。 为了实现这个目标,它的设计使用了之前的协调服务、容错系统、分布式算法和文件系统的思想。

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

在文献中已经提出了容错系统,其目的是减轻构建容错分布式应用程序的问题。 一个早期的系统是 ISIS [5]。 ISIS 系统将抽象的类型规范转换为容错的分布式对象,从而使容错机制对用户透明。 Horus [30] 和Ensemble [31] 是由 ISIS 进化而来的系统。 ZooKeeper 支持 ISIS 的虚拟同步概念。 最后,Totem 保证在利用局域网的硬件广播的体系结构中的消息传递的总顺序 [22]。 ZooKeeper 使用各种各样的网络拓扑,这些拓扑促使我们依赖于服务器进程之间的 TCP 连接,而不是假定任何特殊的拓扑或硬件特性。 我们也不公开在 ZooKeeper 中内部使用的任何集成通信。

构建容错服务的一项重要技术是状态机复制 [26],而 Paxos [20] 是一种能够有效实现异步系统的复制状态机的算法。 我们使用 Paxos 的某些特性的算法,但是将达成共识所需的事务日志记录与数据树恢复所需的提前写入日志记录结合起来,以实现高效的实现。 已经提出了用于拜占庭容忍复制状态机的实际实现的协议 [7、10、18、1、28]。 ZooKeeper 并不假设服务器可以是拜占庭,但是我们确实使用诸如校验和和理智检查之类的机制来捕捉非恶意的拜占庭错误。 Clement et al等人讨论了在不修改当前服务器代码库的情况下使 ZooKeeper 完全容忍拜占庭错误的方法 [9]。 到目前为止,我们还没有观察到使用完全 Byzantine 容错协议可以防止的生产中的故障。 [29].

Boxwood [21] 是一个使用分布式锁服务器的系统。 Boxwood 为应用程序提供了更高级的抽象,它依赖于一种基于 Paxos 的分布式锁服务。 ZooKeeper 和 Boxwood 一样,是构建分布式系统的重要工具。 然而,ZooKeeper 具有高性能要求,并且更广泛地用于客户端应用程序。 ZooKeeper 暴露了应用程序用于实现高级原语的低级原语。(Hades注:即提供了更基础的功能模块的API

ZooKeeper 类似于小型文件系统,但它只提供文件系统操作的一小部分,并添加大多数文件系统中不存在的功能,例如排序保证和条件写入。 然而,ZooKeeper watch在本质上类似于 AFS 的缓存回调 [16]。

Sinfonia [2] 介绍了 微事务(mini-transactions),一种用于构建可伸缩分布式系统的新范式。 Sinfonia 被设计为存储应用程序数据,而 ZooKeeper 存储应用程序元数据。 ZooKeeper保持其状态全复制,同时为了高性能和一致的延迟,状态会在内存中。 我们使用的文件系统(如操作和排序)功能上类似于mini-transactions。 Znode 是一个便捷的抽象,我们在上面添加了监视器watch,这是 Sinfonia 缺少的功能。 dynamo [11] 允许客户端在分布式键值存储中获得和放置相对少量 (小于 1M) 的数据。 与 ZooKeeper 不同,Dynamo 中的键空间不是分层的。 Dynamo 也不能为写入提供强大的持久性和一致性保证,但是解决了读取上的冲突。

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

7、结论(Conclusions)

ZooKeeper 采用了一种无等待的方法来解决分布式系统中的进程协调问题,方法是向客户端暴露无等待的对象。 我们发现 ZooKeeper 对于 Yahoo! 内外的一些应用是有用的。 ZooKeeper 通过使用带有 watch 的快速读取实现了每秒几十万次操作的吞吐量,这些 watch 都由本地副本提供服务。 尽管我们对读取和watch的一致性保证似乎很弱,但是我们已经用我们的用例表明,这种组合允许我们在客户端实现高效和复杂的协调协议,即使读不是先后顺序的,数据对象的实现是无需等待的。 已经证明,无等待属性对于高性能是必不可少的。

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


文章作者: wck
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wck !
评论
 上一篇
CRAQ CRAQ
Object Storage on CRAQ:High-throughput chain replication for read-mostly workloads
2023-05-05
下一篇 
vm-ft vm-ft
这是论文The Design of a Practical System for Fault-Tolerant Virtual Machines翻译及研读笔记
2023-04-22
  目录