[论文翻译]ZooKeeper: Wait-free coordination for Internet-scale systems
Patrick Hunt and Mahadev Konar Yahoo! Grid {phunt,mahadev}@yahoo-inc.com Flavio P. Junqueira and Benjamin Reed Yahoo! Research {fpj,breed}@yahoo-inc.com 摘要 在本文中,我们描述了Zookeeper,一种用于协调分布式应用程序进程的服务。由于Zookeeper是关键基础设施的一部分,它旨在为客户端构建更复杂的协调原语提供一个简单且高性能的核心。它在一个复制的集中式服务中结合了组消息传递、共享寄存器和分布式锁服务的元素。Zookeeper提供的接口具有共享寄存器的无等待特性,并带有一个类似于分布式文件系统中缓存失效的事件驱动机制,从而提供一个简单而强大的协调服务。 Zookeeper的接口支持高性能服务实现。除了无等待特性外,Zookeeper还为客户机提供了请求的先进先出(FIFO)执行保证,并对所有改变Zookeeper状态的请求提供线性一致性。这些设计决策使得实现高性能处理流水线成为可能,读请求可以由本地服务器满足。我们展示了针对目标工作负载(读写比例为2:1到100:1),Zookeeper每秒能够处理数万到数十万次事务。这种性能允许客户端应用程序广泛使用Zookeeper。 1 引言 大规模分布式应用程序需要不同形式的协调。配置是最基本的协调形式之一。在其最简单的形式中,配置只是系统进程的操作参数列表,而更复杂的系统则具有动态配置参数。组成员关系和领导者选举在分布式系统中也很常见:通常进程需要知道其他哪些进程是活跃的以及这些进程负责什么。锁是一种强大的协调原语,用于实现对关键资源的互斥访问。 一种协调的方法是为每种不同的协调需求开发特定的服务。例如,Amazon Simple Queue Service 1 专门针对队列设计。其他服务则专门为领导者选举 2 和配置 3 开发。实现更强大原语的服务可以用来实现功能较弱的原语。例如,Chubby 4 是一个具有强同步保证的锁定服务。然后可以使用锁来实现领导者选举、组成员关系等。 在设计我们的协调服务时,我们没有选择在服务器端实现特定的原语,而是选择公开一个API,使应用程序开发人员能够实现自己的原语。这种选择导致了协调内核(coordination kernel)的实现,使得无需更改服务核心即可启用新的原语。这种方法允许根据应用的需求适应多种协调形式,而不是限制开发者使用固定的一组原语。 在设计Zookeeper的API时,我们避开了锁等阻塞原语。协调服务中的阻塞原语可能会导致诸如慢速或故障客户端对更快客户端性能产生负面影响等问题。如果请求处理依赖于其他客户端的响应和故障检测,服务本身的实现也会变得更加复杂。因此,我们的系统Zookeeper实现了一个操作简单无等待(wait-free)数据对象的API,这些数据对象以类似于文件系统的方式进行层次化组织。实际上,Zookeeper的API与其他文件系统的API相似,仅从API签名来看,Zookeeper似乎就是没有锁方法、打开和关闭功能的Chubby。然而,实现无等待数据对象使Zookeeper与基于锁等阻塞原语的系统有显著区别。 尽管无等待特性对性能和容错至关重要,但对于协调来说,这还不够。我们还需要为操作提供顺序保证。特别是,我们发现保证所有操作的先进先出(FIFO)客户机顺序和写操作的线性一致性,能够实现服务的高效实施,并且足以实现对我们应用程序感兴趣的协调原语。实际上,我们可以使用我们的API为任意数量的进程实现共识,根据Herlihy的层次结构,Zookeeper实现了通用对象5。 Zookeeper服务由一组使用复制技术以实现高可用性和性能的服务器组成。其高性能使得包含大量进程的应用程序能够使用这种协调内核来管理所有方面的协调。我们能够使用一种简单的流水线架构实现Zookeeper,这种架构允许我们在仍有低延迟的情况下处理数百或数千个未完成的请求。这样的流水线自然支持单个客户端操作按FIFO顺序执行。保证FIFO客户机顺序使客户机可以异步提交操作。通过异步操作,客户机能够同时拥有多个未完成的操作。例如,当一个新的客户机成为领导者并且需要相应地操作和更新元数据时,这一特性是理想的。如果没有多未完成操作的可能性,初始化时间可能会达到数秒而不是亚秒级。 为了保证更新操作满足线性一致性,我们实现了一个基于领导者的原子广播协议6,称为Zab7。然而,一个典型的Zookeeper应用程序工作负载主要由读操作主导,因此需要扩展读吞吐量。在Zookeeper中,服务器本地处理读操作,并且不使用Zab对它们进行全局排序。 客户端侧缓存数据是提高读性能的重要技术。例如,对于一个进程来说,缓存当前领导者的标识符比每次需要知道领导者信息时都查询Zookeeper要高效得多。Zookeeper使用观察机制使客户端能够在不直接管理客户端缓存的情况下缓存数据。通过这种机制,客户端可以监视给定数据对象的更新,并在发生更新时收到通知。Chubby直接管理客户端缓存,它阻止更新以使所有缓存了正在更改数据的客户端的缓存失效。在这种设计下,如果这些客户端中有任何一个缓慢或出现故障,更新将会被延迟。Chubby使用租约防止故障客户端无限期地阻塞系统。然而,租约仅限制了缓慢或故障客户端的影响,而Zookeeper的观察机制则完全避免了这一问题。 在本文中,我们讨论 ZooKeeper 的设计和实现。借助 ZooKeeper,我们能够实现应用程序所需的所有协调原语,即使只有写入是可线性化的。为了验证我们的方法,我们展示了如何使用 ZooKeeper 实现一些协调原语。 总之,本文的主要贡献如下: 协调内核:我们提出了一种具有宽松一致性保证的无等待协调服务,用于分布式系统中。特别是,我们描述了协调内核的设计与实现,该内核已在许多关键应用中用于实现各种协调技术。 协调方案:我们展示了如何使用Zookeeper构建更高级别的协调原语,甚至是阻塞和强一致性的原语,这些原语在分布式应用中经常被使用。 协调经验:我们分享了一些使用Zookeeper的方式,并评估了其性能。通过这些实际应用和性能评估,我们可以更好地理解Zookeeper在真实环境中的表现及其对分布式应用的支持能力。 2 ZooKeeper 服务 客户通过使用Zookeeper客户端库的客户端API向Zookeeper提交请求。除了通过客户端API暴露Zookeeper服务接口外,客户端库还管理客户端与Zookeeper服务器之间的网络连接。在本节中,我们首先提供Zookeeper服务的高层视图。然后讨论客户端用于与Zookeeper交互的API。 术语。在本文中,我们使用客户端(client)表示ZooKeeper服务的用户,服务器(server)表示提供ZooKeeper服务的进程,znode表示ZooKeeper数据中的内存数据节点,这些节点在一个分层命名空间中组织,称为数据树(data tree)。我们还使用术语更新和写入来指代任何修改数据树状态的操作。客户端在连接到ZooKeeper并获取会话句柄通过其发出请求时建立会话(session)。 2.1 服务总览 ZooKeeper为其客户端提供了一组按照分层命名空间组织的数据节点(znodes)的抽象。这个层次结构中的znodes是客户端通过ZooKeeper API进行操作的数据对象。分层命名空间常见于文件系统中,这是一种理想的组织数据对象的方式,因为用户对此抽象非常熟悉,并且它能够更好地组织应用程序元数据。为了引用一个特定的znode,我们使用标准的UNIX文件系统路径表示法。例如,我们使用/A/B/C来表示znode C的路径,其中C的父节点是B,而B的父节点是A。所有znodes都存储数据,并且除了临时znodes外,所有znodes都可以有子节点。 客户端可以创建两种类型的 znode: 常规znodes(regular):客户端通过显式地创建和删除来操作常规znodes。 临时znodes(ephemeral):客户端创建此类znodes,它们要么由客户端显式删除,要么在创建它们的会话终止(无论是故意终止还是由于故障)时由系统自动移除。 此外,当创建一个新的znode时,客户端可以设置一个顺序(sequential)标志。设置了顺序标志的节点会在其名称后附加一个单调递增计数器的值。如果$n$是新的znode,$p$是父znode,则$n$的序列值永远不会小于在$p$下创建的任何其他顺序znode名称中的值。 ZooKeeper实现了监视(watches)机制,允许客户端在不进行轮询的情况下及时接收变化的通知。当客户端发出带有监视标志的读操作时,操作会正常完成,但服务器承诺在返回的信息发生变化时通知客户端。监视是一次性触发器,与一个会话相关联;一旦触发或会话关闭,它们就会被注销。监视仅指示发生了变化,但不会提供具体的变化内容。例如,如果客户端在“/foo”被更改两次之前发出getData(''/foo'', true),客户端将收到一个监视事件,告知“/foo”的数据已更改。会话事件,如连接丢失事件,也会发送到监视回调中,以便客户端知道监视事件可能会延迟。...