Zookeeper
Zookeeper
在本文中,我们介绍了 ZooKeeper,一种用于协调分布式应用程序进程的服务。由于 ZooKeeper 是关键基础设施的一部分,因此 ZooKeeper 的目标是为在客户端构建更复杂的协调原语提供一个简单且高性能的内核。它在一个复制的集中式服务中融合了组消息传递、共享寄存器和分布式锁服务的元素。ZooKeeper 所暴露的接口具有共享寄存器的无等待特性,以及与分布式文件系统的缓存失效类似的事件驱动机制,以提供一种简单而强大的协调服务。
ZooKeeper 的接口实现了一种高性能的服务。除了无等待特性之外,ZooKeeper 还为每个客户端提供了请求的先入先出执行保证,以及对所有改变 ZooKeeper 状态的请求的线性一致性。这些设计决策使得能够实现一个高性能的处理管道,其中读取请求由本地服务器满足。我们展示了对于目标工作负载(读取与写入比例为 2:1 到 100:1),ZooKeeper 能够每秒处理数万到数十万个事务。这种性能使得 ZooKeeper 能够被客户端应用程序广泛使用。
1.介绍
大规模分布式应用程序需要不同形式的协调。通常有下面三种形式的协调:
- 配置是最基本的协调形式之一。在其最简单的形式中,配置只是系统进程的一系列操作参数列表,而更复杂的系统则具有动态配置参数。
- 组成员关系和领导者选举在分布式系统中也很常见:通常进程需要知道其他哪些进程是活跃的以及这些进程负责什么。
- 锁构成了一种强大的协调原语,它实现了对关键资源的互斥访问。
一种协调的方法是为不同的协调需求分别开发服务。例如,亚马逊SQS 专门聚焦于队列。其他服务是专门为领导者选举和配置而开发的。实现更强大原语的服务可以用来实现较弱的原语。例如,Chubby 是一种具有强大同步保证的锁服务。然后,锁可以用来实现领导者选举、组成员关系等。
在设计我们的协调服务时,我们不再在服务器端实现特定的原语,而是选择公开一个应用程序开发人员能够用来实现他们自己原语的 API。这样的选择导致了一个协调内核的实现,它能够实现新的原语而不需要对服务核心进行更改。这种方法实现了适应应用程序需求的多种协调形式,而不是将开发人员限制在一组固定的原语中。
在设计 ZooKeeper 的 API 时,我们摒弃了诸如锁之类的阻塞原语。对于协调服务而言,阻塞原语可能会导致诸多问题,比如速度慢或有故障的客户端会对速度较快的客户端的性能产生负面影响。如果服务的实现本身依赖于其他客户端的响应和故障检测来处理请求,那么服务的实现就会变得更加复杂。我们的系统 ZooKeeper 因此实现了一个 API,该 API 操作简单的无等待数据对象,这些对象像在文件系统中一样分层组织。实际上,ZooKeeper 的 API 类似于任何其他文件系统的 API,仅从 API 签名来看,ZooKeeper 似乎是没有锁方法、打开和关闭方法的 Chubby。然而,实现无等待数据对象使 ZooKeeper 与基于诸如锁等阻塞原语的系统有很大的不同。
尽管无等待特性对于性能和容错性很重要,但对于协调来说它是不够的。我们还必须为操作提供顺序保证。特别是,我们发现保证所有操作的先入先出(FIFO)客户端顺序以及线性化写入能够实现高效的服务实现,并且足以实现我们的应用程序所关注的协调原语。实际上,我们可以使用我们的 API 为任意数量的进程实现共识,并且根据赫利希(Herlihy)的层次结构,ZooKeeper 实现了一个通用对象。
ZooKeeper 服务由一组服务器组成,这些服务器使用复制来实现高可用性和高性能。它的高性能使得由大量进程组成的应用程序能够使用这样一个协调内核来管理协调的各个方面。
我们能够使用一个简单的流水线架构来实现 ZooKeeper,该架构允许我们有数百或数千个未完成的请求,同时仍然实现低延迟。这样的流水线自然能够以FIFO的顺序执行来自单个客户端的操作。保证客户端的先入先出顺序使客户端能够异步提交操作。通过异步操作,客户端能够同时有多个未完成的操作。例如,当一个新客户端成为领导者并且必须操作元数据并相应地进行更新时,这个特性是很理想的。如果没有多个未完成操作的可能性,初始化时间可能是秒级而不是亚秒级。
为了确保更新操作满足线性一致性,我们实现了一个基于领导者的原子广播协议,称为 Zab。然而,ZooKeeper 应用程序的典型工作负载以读取操作为主,因此提高读取吞吐量就变得很有必要。在 ZooKeeper 中,服务器在本地处理读取操作,我们不使用 Zab 对它们进行完全排序。
在客户端缓存数据是提高读取性能的一项重要技术。例如,对于一个进程来说,缓存当前领导者的标识符是很有用的,而不是每次需要知道领导者时都去探测 ZooKeeper。ZooKeeper 使用一种观察(watch)机制,使客户端能够缓存数据而无需直接管理客户端缓存。通过这种机制,客户端可以观察给定数据对象的更新,并在更新时接收通知。Chubby 直接管理客户端缓存。它阻止更新以使缓存正在被更改的数据的所有客户端的缓存失效。在这种设计下,如果这些客户端中的任何一个速度慢或有故障,更新就会被延迟。Chubby 使用租约来防止有故障的客户端无限期地阻塞系统。然而,租约只是限制了速度慢或有故障的客户端的影响,而 ZooKeeper 的观察机制则完全避免了这个问题。
2
2.3 ZooKeeper 保证
ZooKeeper 有两个基本的顺序保证:
- 线性化写入:所有更新 ZooKeeper 状态的请求都是可序列化的,并且遵循先后顺序;
- 先进先出的客户端顺序:来自给定客户端的所有请求都按照客户端发送的顺序执行。
请注意,我们对线性一致性的定义与赫利希(Herlihy)最初提出的定义不同,我们称之为 A - 线性一致性(异步线性一致性)。在赫利希对线性一致性的原始定义中,一个客户端一次只能有一个未完成的操作(一个客户端是一个线程)。在我们的定义中,我们允许一个客户端有多个未完成的操作,因此我们可以选择不保证同一客户端的未完成操作的特定顺序,或者保证先进先出顺序。我们为我们的属性选择了后者。重要的是要注意,所有适用于线性一致对象的结果也适用于 A - 线性一致对象,因为满足 A - 线性一致性的系统也满足线性一致性。由于只有更新请求是 A - 线性一致的,ZooKeeper 在每个副本上本地处理读请求。这使得服务能够随着服务器添加到系统中而线性扩展。
为了了解这两个保证是如何相互作用的,考虑以下场景。一个由多个进程组成的系统选举出一个领导者来指挥工作进程。当一个新的领导者接管系统时,它必须更改大量的配置参数,并在完成后通知其他进程。那么我们有两个重要的要求:
- 当新的领导者开始进行更改时,我们不希望其他进程开始使用正在被更改的配置;
- 如果新的领导者在配置完全更新之前死亡,我们不希望进程使用这个部分配置。
注意,分布式锁(例如由 Chubby 提供的锁)有助于满足第一个要求,但对于第二个要求来说是不够的。在 ZooKeeper 中,新的领导者可以指定一个路径作为就绪节点(ready znode);只有当那个节点存在时,其他进程才会使用配置。
新的领导者通过删除 "就绪" 节点、更新各种配置节点并创建 "就绪" 节点来进行配置更改。所有这些更改都可以进行流水线处理并异步发出,以快速更新配置状态。尽管更改操作的延迟约为 2 毫秒,但如果请求一个接一个地发出,那么一个必须更新 5000 个不同节点的新领导者将需要 10 秒钟。
2.4 源语示例
在本节中,我们展示如何使用 ZooKeeper API 来实现更强大的原语。ZooKeeper 服务对这些更强大的原语一无所知,因为它们完全是在客户端使用 ZooKeeper 客户端 API 实现的。一些常见的原语,如组成员关系和配置管理,也是无等待的。对于其他原语,如会合(rendezvous),客户端需要等待一个事件。即使 ZooKeeper 是无等待的,我们也可以用 ZooKeeper 实现高效的阻塞原语。ZooKeeper 的顺序保证允许对系统状态进行高效推理,而观察机制允许进行高效等待。
配置管理:ZooKeeper 可用于在分布式应用程序中实现动态配置。在其最简单的形式中,配置存储在一个节点(znode)zc 中。进程启动时带有 zc 的完整路径名。启动的进程通过读取设置了观察标志为真的 zc 来获取它们的配置。如果 zc 中的配置被更新,进程将被通知并读取新的配置,再次将观察标志设置为真。
请注意,在这个方案中,就像在大多数使用观察机制的其他方案中一样,观察机制被用来确保进程拥有最新的信息。例如,如果一个正在观察 zc 的进程收到了 zc 发生变化的通知,而在它能够对 zc 发出读取请求之前,zc 又发生了三次变化,那么该进程不会再收到三个通知事件。这不会影响进程的行为,因为这三个事件只是会通知进程一些它已经知道的事情:它拥有的关于 zc 的信息已经过时了。
简单锁
尽管 ZooKeeper 不是一个锁服务,但它可以被用来实现锁。使用 ZooKeeper 的应用程序通常会使用根据其需求定制的同步原语,比如上面展示的那些。在这里,我们展示如何用 ZooKeeper 实现锁,以表明它可以实现各种各样的通用同步原语。
锁的创建:
- 客户端在 Zookeeper 中使用临时标志(EPHEMERAL)创建一个特定的标志节点,例如 /simple_lock。如果创建成功,则认为获取到锁。
锁的竞争:
- 多个客户端同时尝试创建 /simple_lock 节点,只有一个客户端能够成功创建,其他客户端创建失败。若失败,客户端可以读取这个节点并设置观察标志,以便在当前持有锁的客户端失效时得到通知。
锁的释放:
- 持有锁的客户端完成任务后,删除 /simple_lock 节点。
- 其他客户端会不断尝试创建该节点,以获取锁。在这种情况下,当锁被释放时,所有等待的客户端都会同时尝试获取锁,可能会出现类似"惊群"的效果,即大量客户端同时竞争锁资源,造成不必要的资源消耗和性能问题。
虽然这个简单的锁协议是有效的,但它确实存在一些问题。首先,它受到羊群效应的影响。如果有很多客户端在等待获取锁,当锁被释放时,它们都会争抢这个锁,尽管只有一个客户端能够获取到锁。其次,它只实现了排他锁。下面的两个原语展示了如何克服这两个问题。
无惊群效应的简单锁:
使用 ZooKeeper 实现分布式锁的基本思想是利用 ZooKeeper 的有序临时节点(ephemeral sequential znode)来创建唯一的锁节点,并通过观察节点的顺序来决定哪个客户端获得锁。当节点被删除时,ZooKeeper 会自动通知下一个客户端,从而实现分布式锁的公平释放与竞争。
ZooKeeper 实现分布式锁的步骤:
- 创建锁节点:每个客户端在特定路径下创建一个有序临时节点(ephemeral sequential znode)。例如,所有客户端在路径 /lock 下创建类似于 /lock/lock-00000001 的节点。
- 获取锁:客户端获取当前路径下的所有节点,并对节点排序。若自己的节点在排序后的节点列表中最小,则获得锁。若不是最小节点,则监视比自己小的上一个节点(确保有序性)。
- 释放锁:当持有锁的客户端完成任务时,删除自己的节点。这时,ZooKeeper 会通知下一个节点(即排名第二小的节点),使其获得锁。
- 故障处理:ZooKeeper 会自动删除已失联客户端的临时节点,因此其他客户端可以检测到锁已释放,并进行重新竞争。
总之,这个锁方案有以下优点:
- 删除一个节点只会唤醒一个客户端,因为每个节点恰好被另一个客户端观察,所以我们没有惊群效应效应
- 没有轮询或超时;
- 由于我们实现锁的方式,我们可以通过浏览 ZooKeeper 数据看到锁竞争的程度、打破锁以及调试锁问题。
读/写锁
实现需要稍微修改锁的过程,并分别设置读取锁和写入锁的程序。解锁过程与全局锁的情况相同。
这个锁的过程与之前的锁略有不同。写锁的区别仅在于命名。由于读锁可以被共享,因此第 3 和第 4 行略有不同,因为只有早期的写锁 znode 会阻止客户端获取读锁。当有多个客户端等待读锁并在较低序号的写znode 被删除时收到通知时,这可能看起来像是出现了"惊群效应";实际上,这是一种期望的行为,所有这些读取客户端都应该被释放,因为它们现在可以获取锁。
双重屏障:
双重屏障使客户端能够同步计算的开始和结束。
当由屏障阈值定义的足够多的进程加入屏障时,进程开始它们的计算,并在完成后离开屏障。我们用一个 ZooKeeper 中的节点(称为 b)来表示一个屏障。每个进程 p 在进入时通过在 b 下创建一个子节点来向 b 注册,并在准备离开时删除该子节点来取消注册。当 b 的子节点数量超过屏障阈值时,进程可以进入屏障。当所有进程都删除了它们的子节点时,进程可以离开屏障。我们使用观察器来有效地等待进入和退出条件得到满足。为了进入,进程观察 b 的一个准备好的子节点的存在,这个子节点将由使子节点数量超过屏障阈值的进程创建。为了离开,进程观察特定的子节点消失,并且仅在该节点被删除后才检查退出条件。
3.zookeeper的应用
现在我们描述一些使用 ZooKeeper 的应用程序,并简要解释它们是如何使用它的。我们将每个示例的基本操作以粗体显示。
抓取服务(The Fetching Service):抓取是搜索引擎的一个重要部分,雅虎抓取数十亿的网页文档。抓取服务(FS)是雅虎抓取器的一部分,目前正在生产环境中使用。从本质上讲,它有主进程来指挥页面抓取进程。主进程为抓取器提供配置,抓取器写回信息以告知其状态和健康情况。对于 FS 来说,使用 ZooKeeper 的主要优势在于从主进程故障中恢复、确保即使出现故障也能保证可用性,以及将客户端与服务器解耦,允许客户端仅通过从 ZooKeeper 读取服务器状态就将请求定向到健康的服务器。因此,FS 主要使用 ZooKeeper 来管理配置元数据,尽管它也使用 ZooKeeper 进行主节点选举。
图 2 展示了在三天的时间段内,抓取服务(FS)所使用的一个 ZooKeeper 服务器的读和写流量。为了生成此图,我们对这段时间内每一秒的操作数量进行计数,每个点对应于该秒中的操作数量。我们观察到读流量比写流量高得多。在速率高于每秒 1000 次操作的时间段内,读操作与写操作的比例在 10:1 到 100:1 之间变化。在此工作负载中的读操作是 getData ()、getChildren () 和 exists (),按照出现的频率依次递增。
Katta:Katta
是一个分布式索引器,它使用 ZooKeeper 进行协调,并且它是一个非雅虎的应用示例。Katta 使用分片来划分索引工作。一个主服务器将分片分配给从服务器并跟踪进度。从服务器可能会出现故障,所以当从服务器来来去去时,主服务器必须重新分配负载。主服务器也可能会出现故障,所以在出现故障的情况下,其他服务器必须准备好接管。Katta 使用 ZooKeeper 来跟踪从服务器和主服务器的状态(组成员关系),并处理主服务器故障转移(领导者选举)。Katta 还使用 ZooKeeper 来跟踪和传播分片到从服务器的分配(配置管理)。
雅虎消息代理
雅虎消息代理(YMB)是一个分布式发布 - 订阅系统。该系统管理着数千个主题,客户端可以向这些主题发布消息以及从这些主题接收消息。主题分布在一组服务器中以提供可扩展性。每个主题都使用主备方案进行复制,确保消息被复制到两台机器上以确保可靠的消息传递。构成 YMB 的服务器使用无共享分布式架构,这使得协调对于正确操作至关重要。YMB 使用 ZooKeeper 来管理主题的分布(配置元数据)、处理系统中机器的故障(故障检测和组成员关系)以及控制系统操作。
4.ZooKeeper 实现
ZooKeeper 通过在组成服务的每个服务器上复制 ZooKeeper 数据来提供高可用性。我们假设服务器因崩溃而出现故障,并且这样的故障服务器可能稍后会恢复。图 4 展示了 ZooKeeper 服务的高级组件。在接收到请求时,服务器为执行请求做准备(请求处理器)。如果这样的请求需要服务器之间进行协调(写请求),那么它们会使用一个一致性协议(原子广播的一种实现),最后服务器将更改提交到完全复制在整个服务集群的所有服务器上的 ZooKeeper 数据库。在处理读请求的情况下,服务器只需读取本地数据库的状态并生成对请求的响应。
复制的数据库是一个内存数据库,包含整个数据树。默认情况下,树中的每个节点最多存储 1MB 的数据,但这个最大值是一个配置参数,可以在特定情况下进行更改。为了实现可恢复性,我们高效地将更新记录到磁盘上,并且在将写入应用到内存数据库之前强制将其写入磁盘介质。实际上,与 Chubby一样,我们保留已提交操作的重放日志(在我们的情况下是预写日志),并定期生成内存数据库的快照。
每个 ZooKeeper 服务器为客户端提供服务。客户端恰好连接到一个服务器来提交其请求。如我们之前所述,读请求由每个服务器数据库的本地副本进行服务。改变服务状态的请求,即写请求,由一个一致性协议进行处理。作为一致性协议的一部分,写请求被转发到一个单独的服务器,称为领导者。其余的 ZooKeeper 服务器,称为跟随者,从领导者接收由状态变更组成的消息提议,并就状态变更达成一致。
4.1 请求处理器
由于消息传递层是原子性的,我们保证本地副本永远不会出现分歧,尽管在任何时间点,一些服务器可能已经应用了比其他服务器更多的事务。与从客户端发送的请求不同,事务是幂等的。当领导者接收到一个写请求时,它会计算当写入被应用时系统的状态将会是什么样,并将其转换为一个事务,这个事务捕捉到这个新状态。
必须计算未来状态是因为可能存在尚未应用到数据库的未完成事务。例如,如果客户端进行有条件的设置数据(setData)操作,并且请求中的版本号与要更新的节点的未来版本号匹配,那么服务会生成一个包含新数据、新的版本号和更新的时间戳的设置数据事务(setDataTXN)。如果发生错误,比如版本号不匹配或者要更新的节点不存在,那么就会生成一个错误事务(errorTXN)来代替。
4.2 原子广播
所有更新 ZooKeeper 状态的请求都被转发到领导者。领导者执行请求,并通过 Zab(一种原子广播协议)将对 ZooKeeper 状态的更改广播出去。接收到客户端请求的服务器在交付相应的状态更改时向客户端做出响应。Zab 默认使用简单多数法定人数来决定一个提议,所以 Zab 以及 ZooKeeper 只有在大多数服务器是正确的情况下才能工作(即,有 2f + 1 个服务器时,我们可以容忍 f 个故障)。
为了实现高吞吐量,ZooKeeper 试图让请求处理管道保持满负荷状态。它可能在处理管道的不同部分有数千个请求。因为状态变化依赖于先前状态变化的应用,所以 Zab 提供比常规原子广播更强的顺序保证。更具体地说,Zab 保证由一个领导者广播的更改按照它们被发送的顺序被交付,并且来自先前领导者的所有更改在新领导者广播其自己的更改之前被交付给这个新领导者。
有一些实现细节既简化了我们的实现又给予了我们出色的性能。我们使用 TCP 作为传输协议,所以消息顺序由网络维护,这使我们能够简化实现。我们将由 Zab 选出的领导者作为 ZooKeeper 的领导者,这样创建事务的同一进程也提出这些事务。我们使用日志来跟踪提议,将其作为内存数据库的预写日志,这样我们就不必将消息两次写入磁盘。
在正常操作期间,Zab 确实会按顺序且仅一次地传递所有消息,但由于 Zab 不会持久记录每个已传递消息的 ID,所以 Zab 可能在恢复期间重新传递消息。因为我们使用幂等事务,只要按顺序传递,多次传递是可以接受的。实际上,ZooKeeper 要求 Zab 至少重新传递在上一个快照开始后已传递的所有消息。
4.3 复制数据库
每个副本在内存中都有一份 ZooKeeper 状态的副本。当一个 ZooKeeper 服务器从崩溃中恢复时,它需要恢复这个内部状态。在服务器运行一段时间后,重放所有已传递的消息来恢复状态将花费非常长的时间,所以 ZooKeeper 使用定期快照,并且只要求重新传递自快照开始以来的消息。我们称 ZooKeeper 快照为模糊快照,因为我们在拍摄快照时不会锁定 ZooKeeper 状态;相反,我们对树进行深度优先扫描,原子地读取每个节点的数据和元数据并将它们写入磁盘。由于生成的模糊快照可能已经应用了在快照生成期间传递的状态变化的一些子集,所以结果可能与 ZooKeeper 在任何时间点的状态都不对应。然而,由于状态变化是幂等的,只要我们按顺序应用状态变化,我们就可以将它们应用两次。
例如,假设在一个 ZooKeeper 数据树中,两个节点/foo
和 /goo
分别具有值 f1 和 g1,并且在模糊快照开始时两者都处于版本 1,然后以下状态变化流到达,具有形式< 事务类型,路径,值,新版本号 i>:
<SetDataTXN, /foo, f2, 2>
<SetDataTXN, /goo, g2, 2>
<SetDataTXN, /foo, f3, 3>
在处理这些状态变化后,/foo
和 /goo
的值分别为 f3
和 g2
,版本号分别为 3 和 2。然而,模糊快照可能记录了 /foo
和 /goo
的值分别为 f3 和 g1,版本号分别为 3 和 1,这不是 ZooKeeper 数据树的有效状态。如果服务器崩溃并使用这个快照恢复,并且 Zab 重新传递状态变化,那么最终的状态将与崩溃前的服务状态相对应。
4.4 客户端 - 服务器交互
当一个服务器处理写请求时,它也会发出并清除与该更新相对应的任何监视的通知。服务器按顺序处理写入,并且不同时处理其他写入或读取。这确保了通知的严格顺序。请注意,服务器在本地处理通知。只有客户端连接到的服务器才会跟踪并为该客户端触发通知。
读请求在每个服务器上本地处理。每个读请求被处理并标记一个 zxid,该 zxid 对应于服务器看到的最后一个事务。这个 zxid 定义了读请求相对于写请求的偏序。通过在本地处理读请求,我们获得了出色的读性能,因为这只是在本地服务器上的内存操作,没有磁盘活动或需要运行的一致性协议。这个设计选择是实现我们在以读为主的工作负载下获得出色性能目标的关键。
使用快速读取的一个缺点是不能保证读操作的先后顺序。也就是说,即使对同一节点的更近更新已被提交,读操作也可能返回一个过时的值。并非我们所有的应用程序都需要先后顺序,但对于确实需要它的应用程序,我们实现了 "sync"(同步)。这个原语异步执行,并在对其本地副本的所有待处理写入之后由领导者进行排序。为了确保给定的读操作返回最新更新的值,客户端在调用读操作之前先调用 "sync"。客户端操作的先进先出顺序保证以及 "sync" 的全局保证使得读操作的结果能够反映在发出 "sync" 之前发生的任何更改。
在我们的实现中,由于我们使用基于领导者的算法,所以我们不需要原子地广播 "sync",我们只需将 "sync" 操作放在领导者和执行 "sync" 调用的服务器之间的请求队列的末尾。为了使其正常工作,跟随者必须确定领导者仍然是领导者。如果有待处理的事务要提交,那么服务器不会怀疑领导者。如果待处理队列是空的,领导者需要发出一个空事务进行提交,并在该事务之后对 "sync" 进行排序。这具有一个很好的特性,即当领导者处于负载下时,不会产生额外的广播流量。在我们的实现中,设置超时使得领导者在跟随者放弃它们之前意识到它们不再是领导者,所以我们不发出空事务。
ZooKeeper 服务器以先进先出的顺序处理来自客户端的请求。响应中包括该响应所对应的 zxid。即使在没有活动的时间段内的心跳消息也包含客户端所连接的服务器所看到的最后一个 zxid。如果客户端连接到一个新服务器,那个新服务器通过将客户端的最后一个 zxid 与它自己的最后一个 zxid 进行比较,确保它对 ZooKeeper 数据的视图至少与客户端的视图一样新。如果客户端的视图比服务器的更新,服务器在赶上进度之前不会与客户端重新建立会话。由于客户端只看到已复制到大多数 ZooKeeper 服务器的更改,所以客户端肯定能够找到另一个对系统有最新视图的服务器。这种行为对于保证持久性很重要。
为了检测客户端会话故障,ZooKeeper 使用超时机制。如果在会话超时时间内没有其他服务器从一个客户端会话接收到任何东西,领导者就确定发生了故障。如果客户端足够频繁地发送请求,那么就不需要发送任何其他消息。否则,在低活跃度期间客户端发送心跳消息。如果客户端不能与服务器通信以发送请求或心跳,它就连接到一个不同的 ZooKeeper 服务器以重新建立会话。为了防止会话超时,ZooKeeper 客户端库在会话空闲了 s = 3 毫秒后发送一个心跳,如果在 2s = 3 毫秒内没有收到来自服务器的消息就切换到一个新服务器,其中 s 是以毫秒为单位的会话超时时间。
5 评估
我们在一个由 50 台服务器组成的集群上进行了所有的评估。每台服务器有一个至强双核 2.1GHz 处理器、4GB 内存、千兆以太网和两个 SATA 硬盘。我们将下面的讨论分为两部分:请求的吞吐量和延迟。
5.1 吞吐量
为了评估我们的系统,我们在系统饱和时对吞吐量进行基准测试,并测试在各种人为注入的故障下吞吐量的变化情况。我们改变组成 ZooKeeper 服务的服务器数量,但始终保持客户端的数量相同。为了模拟大量客户端,我们使用 35 台机器来模拟 250 个同时连接的客户端。
我们有一个 ZooKeeper 服务器的 Java 实现版本,以及 Java 和 C 两种客户端。在这些实验中,我们使用配置为将日志记录到一个专用磁盘并在另一个磁盘上进行快照的 Java 服务器。我们的基准测试客户端使用异步 Java 客户端 API,并且每个客户端至少有 100 个未完成的请求。每个请求包括对 1K 数据的读或写。我们没有展示其他操作的基准测试,因为所有修改状态的操作的性能大致相同,而除了 "sync" 之外的非状态修改操作的性能也大致相同。("sync" 的性能近似于一个轻量级的写操作,因为请求必须发送到领导者,但不会被广播。)客户端每 300 毫秒发送已完成操作的数量计数,我们每 6 秒进行一次采样。为了防止内存溢出,服务器会限制系统中并发请求的数量。ZooKeeper 使用请求节流来防止服务器不堪重负。在这些实验中,我们将 ZooKeeper 服务器配置为最多有 2000 个总请求在处理中。
7.结论
ZooKeeper 采用无等待方法来解决分布式系统中的进程协调问题,向客户端公开无等待对象。我们发现 ZooKeeper 在雅虎内部和外部的几个应用中都很有用。通过使用带有监视的快速读取(两者都由本地副本提供服务),ZooKeeper 在以读为主的工作负载下实现了每秒数十万次操作的吞吐量值。尽管我们对读取和监视的一致性保证似乎很弱,但我们通过用例表明,这种组合允许我们在客户端实现高效且复杂的协调协议,即使读取不是按先后顺序进行的,并且数据对象的实现是无等待的。无等待属性已被证明对高性能至关重要。虽然我们只描述了几个应用,但还有许多其他应用在使用 ZooKeeper。我们认为这样的成功是由于其简单的接口以及人们可以通过这个接口实现的强大抽象。此外,由于 ZooKeeper 的高吞吐量,应用程序可以广泛使用它,而不仅仅是粗粒度锁定。
ps
文中多次提到了无等待(Wait-free)怎么理解?
- zooKeeper 的接口采用了事件驱动机制。这意味着客户端可以注册对特定数据变化的关注(类似于监听事件)。当数据发生变化时,ZooKeeper 会主动通知注册了该事件的客户端,触发相应的处理逻辑。