文章目录
摘要
在本文中,我们描述了ZooKeeper,一种用于协调分布式应用的服务。由于ZooKeeper是关键基础设施的一部分,因此ZooKeeper旨在提供一个简单而高性能的内核,以在客户端构建更复杂的协调原语。它在复制的集中式服务中综合了组消息传递、共享寄存器和分布式锁服务的原理。ZooKeeper暴露的接口具有共享寄存器的无等待特性,它具有类似于分布式文件系统的缓存失效的事件驱动机制,以提供简单而强大的协调服务。
ZooKeeper接口可实现高性能服务。除了无等待特性之外,ZooKeeper还为每个客户端提供了保证请求的FIFO执行及所有更改ZooKeeper状态的请求的线性一致性。这些设计决策可以实现高性能处理流水线,而本地服务器可以满足读取请求。对于目标工作负载,我们展示了2:1到100:1的读写比,ZooKeeper每秒可以处理成千上万个事务。这种性能使ZooKeeper可以被客户端应用广泛使用。
1. 简介
大型分布式应用需要不同形式的协调。配置是最基本的协调形式之一。在最简单的形式中,配置只是系统进程的操作参数列表,而更复杂的系统具有动态配置参数。组成员身份认证和领导者选举在分布式系统中也很常见:通常,进程需要知道哪些其他进程仍在运行以及这些进程负责什么。锁构成了强大的协调原语,实现了对关键资源的互斥访问。
一种协调方法是为每种不同的协调需求开发服务。例如,Amazon Simple Queue Service
[3]特别专注于排队。针对领导者选举[25]和配置[27]也开发了其他服务。实现功能更强大的原语的服务可用于实现功能更弱的原语。例如,Chubby[6]是具有强同步保证的锁服务。然后可以使用锁来执行领导者选举、组成员身份认证等。
在设计协调服务时,我们不再在服务器端实现特定的原语,而是选择暴露一个API,使应用开发人员可以实现自己的原语。这种选择导致实现了一个协调内核,该协调内核无需更改服务内核即可启用新原语。这种方法使得适配于应用需求的多种形式的协调成为可能,而不是将开发人员限制在一组固定的原语上。
在设计ZooKeeper的API时,我们不再使用阻塞原语,例如锁。阻塞协调服务的原语可能会导致客户端变慢或出现故障,从而对更快的客户端的性能产生负面影响。如果处理请求依赖于其他客户端的响应和故障检测,则服务本身的实现将变得更加复杂。因此,我们的Zookeeper系统实现了一个API,该API可以像文件系统一样操作按层次结构组织的简单的无等待数据对象。实际上,ZooKeeper API类似于任何其他文件系统,并且仅查看API签名,ZooKeeper似乎是没有锁方法、打开和关闭操作的Chubby。但是,实现无等待数据对象使ZooKeeper与基于锁之类的阻塞原语的系统明显不同。
尽管无等待特性对于性能和容错性很重要,但还不足以进行协调。我们还必须为操作提供顺序保证。特别是,我们发现保证所有操作的FIFO客户端顺序和线性化写入都使服务的高效实现成为可能,并且足以实现应用感兴趣的协调原语。实际上,我们可以使用我们的API对任何数量的进程实现一致性,并且根据Herlihy的层次结构,ZooKeeper实现了通用对象[14]。
ZooKeeper服务由使用复制以实现高可用和高性能的服务器组成。它的高性能使包含大量进程的应用可以使用这种协调内核来管理协调的各个方面。我们能够使用简单的流水线体系结构实现ZooKeeper,该体系结构使我们能够处理数百或数千个请求,同时仍实现低延迟。这样的流水线自然可以使来自单个客户端的操作按FIFO顺序执行。保证FIFO客户端顺序使客户端可以异步提交操作。使用异步操作,客户端一次可以执行多个未完成的操作。例如,当新客户端成为领导者并且必须操纵元数据并相应地对其进行更新时,此功能是满足要求的。由于不可能进行多个未完成的操作,因此初始化时间可能是几秒左右,而不是亚秒级。
为了保证更新操作满足线性化要求,我们实现了基于领导者的原子广播协议[23],称为Zab[24]。但是,ZooKeeper应用的典型工作负载由读取操作主导,因此需要扩展读取吞吐量。在ZooKeeper中,服务器在本地处理读取操作,我们不使用Zab对其进行完全排序。
在客户端缓存数据是提高读取性能的一项重要技术。例如,对于一个进程来说,缓存当前领导者的标识而不是在每次需要了解该领导者时探查ZooKeeper是有用的。ZooKeeper使用监听机制使客户端可以缓存数据,而无需直接管理客户端缓存。使用此机制,客户端可以监听对给定数据对象的更新,并在更新时接收通知。Chubby直接管理客户端缓存。它阻塞更新以使所有缓存更改数据的客户端的缓存无效。在这种设计下,如果这些客户端中的任何一个缓慢或出现故障,更新都会延迟。Chubby使用租约来防止有故障的客户端无限期地阻塞系统。但是,租约只能限制慢速或故障客户端的影响,而ZooKeeper监听完全可以避免此问题。
在本文中,我们讨论了ZooKeeper的设计和实现。使用ZooKeeper(尽管只有写入是可线性化的),我们也可以实现应用所需的所有协调原语。为了验证我们的方法,我们展示了如何使用ZooKeeper实现一些协调原语。
总而言之,本文主要贡献是:
- 协调内核:我们提出了一种无等待的协调服务,该服务具有宽松的一致性保证,可用于分布式系统。特别地,我们描述了协调内核的设计和实现,我们已经在许多关键应用中使用了该协调内核来实现各种协调技术。
- 协调方案:我们展示了ZooKeeper如何可用于构建通常在分布式应用中使用的高级协调原语,甚至是阻塞和强一致性原语。
- 协调经验:我们分享一些使用ZooKeeper的方式并评估其性能。
2. ZooKeeper服务
客户端使用ZooKeeper客户端库通过客户端API向ZooKeeper提交请求。除了通过客户端API暴露ZooKeeper服务接口之外,客户端库还管理客户端和ZooKeeper服务器之间的网络连接。
在本节中,我们首先提供ZooKeeper服务的高层级视图。然后,我们讨论客户端用来与ZooKeeper交互的API。
术语。在本文中,我们使用客户端来表示ZooKeeper服务的用户,使用服务器来表示提供ZooKeeper服务的进程,使用znode来表示ZooKeeper数据中的内存数据节点,该数据节点以分层命名空间(被称为数据树)组织。我们还使用术语“更新和写入”来指代任何修改数据树状态的操作。客户端在连接到ZooKeeper并获得会话句柄以发出请求时建立会话。
2.1 服务概述
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实行监听,以使客户端能够及时收到更改通知,而无需轮询。当客户端发出带有监听标志的读取操作时,该操作将正常完成,除了服务器承诺在返回的信息已更改时会通知客户端。监听是与会话相关的一次性触发器;一旦已触发或会话关闭,它们将取消注册。监听表明发生了更改,但未提供更改。例如,如果客户端在“/foo”被再次更改之前发布了一个getData('/foo',true)
消息,则客户端将收到一个监听事件,告知客户端“/foo”的数据已更改。会话事件(例如连接丢失事件)也将发送到监听回调,因此客户端知道监听事件可能会延迟。
2.1.1 数据模型
ZooKeeper的数据模型本质上是一个具有简化API且只能整体读取和写入数据的文件系统,或者是具有分层键的键/值表。层次命名空间对于为不同应用的命名空间分配子树以及设置对这些子树的访问权限很有用。我们还将在客户端利用目录的概念来构建更高级别的原语,如我们将在2.4节中看到的那样。
与文件系统中的文件不同,znode不适用于常规数据存储。相反,znode映射到客户端应用的抽象,通常对应于用于协调目的的元数据。为了说明,在图1中,我们有两个子树,一个子树用于应用1(/app1),另一个子树用于应用2(/app2)。应用1的子树实现了一个简单的组成员身份认证协议:每个客户端进程pi在/app1下创建一个znode p_i,只要该进程正在运行,该节点便会持续存在。
尽管znode不是设计用于常规数据存储,但是ZooKeeper确实允许客户端存储一些可用于分布式计算中的元数据或配置的信息。例如,在基于领导者的应用中,这对于刚刚开始了解哪个其他服务器当前是领导者的应用服务器很有用。为了实现此目标,我们可以让当前的领导者在znode空间中的已知位置写入此信息。Znode还具有带有时间戳和版本计数器的相关元数据,这使客户端可以跟踪对znode的更改并根据znode的版本执行条件更新。
2.1.2 会话
客户端连接到ZooKeeper并启动会话。会话具有关联的超时时间。如果ZooKeeper在超时时间内没有收到来自会话的任何消息,则认为该客户端有故障。当客户端明确关闭会话句柄或ZooKeeper检测到客户端故障时,会话结束。在会话中,客户端观察到一系列状态变化,这些状态变化反映了其操作的执行。会话使客户端可以在全体ZooKeeper中从一台服务器透明地移动到另一台服务器,因此可以在ZooKeeper服务器之间持久存在。
2.2 客户端API
我们在下面提供ZooKeeper API的相关子集,并讨论每个请求的语义。
- create(path, data, flags):创建路径名称为path的znode,在其中存储data[],并返回新znode的名称。flags使客户端可以选择znode的类型:常规的、临时的,并设置顺序标志;
- delete(path, version):如果该znode处于预期版本,则删除该znode路径;
- exists(path, watch):如果路径名称为path的znode存在,则返回true,否则返回false。watch标志使客户端可以在znode上设置监听器;
- getData(path, watch):返回与znode关联的数据和元数据,例如版本信息。watch标志的工作方式与
exist()
相同,不同之处在于,如果znode不存在,ZooKeeper不会设置监听器; - setData(path, data, version):如果版本号是znode的当前版本,则将data[]写入znode路径;
- getChildren(path, watch):返回znode的子节点的名称集合;
- sync(path):等待操作开始时所有未完成的更新传播到客户端连接到的服务器(path当前被忽略了)。
所有方法都可以通过API获得同步版本和异步版本。当应用需要执行单个ZooKeeper操作并且没有要执行的并发任务时,它会使用同步API,因此它会进行必要的ZooKeeper调用并进行阻塞。但是,异步API使应用可以同时执行多个出色的ZooKeeper操作和其他任务。ZooKeeper客户端保证按顺序调用每个操作的相应回调。
请注意,ZooKeeper不使用句柄访问znode。相反,每个请求都包含正在操作的znode的完整路径。这种选择不仅简化了API(没有open()
或close()
方法),而且还消除了服务器需要维护的额外状态。
每个更新方法均接收期望的版本号参数,从而可以实现条件更新。如果znode的实际版本号与期望版本号不匹配,则更新将以意外的版本号错误而失败。如果版本号为-1,则不执行版本检查。
2.3 ZooKeeper保证
ZooKeeper具有两个基本的排序保证:
- 线性写入:所有更新ZooKeeper状态的请求都是可线性化的,并且遵循优先级;
- FIFO客户端排序:来自给定客户端的所有请求均按客户端发送的顺序执行。
请注意,我们对线性化的定义与Herlihy[15]最初提出的定义不同,我们称其为A-线性化(异步线性化)。在Herlihy最初对线性化的定义中,客户端一次只能执行一项未完成的操作(客户端是一个线程)。在我们的系统中,我们允许一个客户端执行多个未完成的操作,因此,我们可以选择保证不为同一客户端的未完成操作指定特定的顺序或保证FIFO顺序。我们选择后者作为我们的特性。重要的是可以看到,所有可线性化对象的结果也适用于A-线性化对象,因为满足A-线性化能力的系统也满足线性化能力。因为只有更新请求才是A-线性化的,所以ZooKeeper会在每个副本本地处理读取请求。这允许服务在服务器添加到系统中时线性扩展。
若要查看这两个保证如何相互作用,请考虑以下情形。包含多个进程的系统选举领导者控制工作者进程。当新的领导者负责系统时,必须更改大量配置参数,并在完成后通知其他进程。然后,我们有两个重要要求:
- 当新领导者开始进行更改时,我们不希望其他进程开始使用正在更改的配置。
- 如果新领导者在完全更新配置之前死掉,则我们不希望进程使用此部分配置。
注意到分布式锁(例如Chubby提供的锁)将有助于满足第一个要求,但不足以满足第二个要求。使用ZooKeeper,新的领导者可以将路径指定为就绪znode;其他进程将仅在该znode存在时使用配置。新领导者通过删除就绪znode、更新各种配置znode并再创建就绪znode来进行配置更改。所有这些更改都可以流水线化并异步发布,以快速更新配置状态。尽管更改操作的延时约为2ms,但是如果请求连续发出,则新领导者更新5000个不同znode的将花费10s。通过异步发出请求,请求将花费不到一秒钟的时间。由于顺序保证,如果进程看到就绪znode,则它一定看到新的领导者所做的所有配置更改。如果新领导者在创建就绪znode之前死掉,则其他进程会知道该配置尚未完成,因此将不使用它。
上面的方案仍然存在一个问题:如果在新的领导者开始进行更改之前,发现某个进程已经准备就绪,然后在进行更改的同时开始读取配置,将会发生什么情况。通知的排序保证解决了此问题:如果客户端正在监听更改,客户端将在看到系统的新状态之前看到通知事件(在进行更改后)。因此,如果读取就绪znode的进程请求获得对该znode的更改的通知,它将在看到任何新配置之前看到一条通知(通知客户端该更改)。
当客户端除了ZooKeeper之外还拥有自己的通信渠道时,可能会出现另一个问题。例如,考虑两个客户端A和B,它们在ZooKeeper中具有共享的配置,并通过共享的通信通道进行通信。如果A更改了ZooKeeper中的共享配置,并通过共享的通信通道将更改告知B,则B希望在重新读取配置时看到更改。如果B的ZooKeeper副本稍微落后于A,则可能看不到新配置。使用以上保证,B可以通过在重新读取配置之前发出写入操作来确保它看到了最新信息。为了更有效地处理这种情况,ZooKeeper提供了sync请求:在其后的读取,构成一个慢速读取。sync使服务器在处理读取之前应用所有未完成的写入请求,且不会产生全量写入的开销。这个原语在概念上与ISIS[5]的flush原语相似。
ZooKeeper还具有以下两个活跃性和持久性的保证:如果大多数ZooKeeper服务器处于活跃状态并且可以进行通信,则该服务可用;如果ZooKeeper服务成功响应更改请求,则该更改将在任何数量的故障中持久存在,只要最终能够达到法定服务器数量即可。
2.4 原语示例
在本节中,我们将展示如何使用ZooKeeper API来实现更强大的原语。ZooKeeper服务对这些更强大的原语一无所知,因为它们是完全使用ZooKeeper客户端API在客户端上实现的。一些常见的原语(例如组成员身份认证和配置管理)也是无需等待的。对于其他原语,例如会合点,客户端需要等待事件。即使ZooKeeper无需等待,我们也可以使用ZooKeeper实现有效的阻塞原语。ZooKeeper的顺序保证允许对系统状态进行有效的推断,而监听则可以实现高效的等待。
2.4.1 配置管理
ZooKeeper可用于在分布式应用中实现动态配置。以其最简单的形式,配置存储在znode zc中。进程以zc的完整路径名启动。启动进程通过将监听标志设置为true来读取zc以获取其配置。如果zc中的配置曾经更新过,则会通知进程并读取新配置,从而再次将监听标志设置为true。
请注意,在此方案中,就像在大多数使用监听的方案中一样,监听用于确保进程具有最新信息。例如,如果向监听zc的进程通知了zc的更改,并且在可以发出对zc的读取之前,存在对zc的另外三个更改,则该进程不会再收到三个通知事件。这不会影响进程的行为,因为这三个事件只是将已知的信息通知进程:它对zc拥有的信息是过时的。
2.4.2 会合点
有时,在分布式系统中,并非总是先验地知道最终的系统配置是什么样的。例如,客户端可能要启动一个主进程和多个工作进程,但是启动进程是由调度程序完成的,因此客户端不提前知道它可以为工作进程连接到主进程提供如地址和端口等先验信息。我们使用ZooKeepe中的会合点znode zr处理此场景,zr是由客户端创建的节点。客户端将zr的完整路径名作为主进程和工作进程的启动参数传递。当主进程启动时,它会在zr中填充有关其正在使用的地址和端口的信息。当工作进程开始时,他们将监听设置为true来读取zr。如果尚未写入zr完成,则工作进程将在更新zr时等待通知。如果zr是临时节点,则主进程和工作进程可以监听zr被删除,并在客户端结束时自行清理。
2.4.3 组成员身份认证
我们利用临时节点来实现组成员身份认证。具体来说,我们利用临时节点允许我们查看创建该节点的会话状态的事实。我们首先指定一个znode zg代表该组。当该组的某个进程成员启动时,它将在zg下创建一个临时子znode。如果每个进程都有唯一的名称或标识符,则将该名称用作子znode的名称;否则,该过程将使用SEQUENTIAL标志创建znode以获得唯一的名称分配。进程可以将进程信息放入子znode的数据中,例如该进程使用的地址和端口。
在zg下创建子znode后,该进程将正常启动。它不需要做任何其他事情。如果该进程失败或结束,则在zg下代表它的znode会被自动删除。
进程可以通过简单列出zg的子节点来获取组信息。如果某个进程想要监听组成员身份的更改,则该进程可以在接收到更改通知时将监听标志设置为true,并刷新组信息(始终将监视标志设置为true)。
2.4.4 简易锁
尽管ZooKeeper不是锁服务,但可以用来实现锁。使用ZooKeeper的应用通常使用根据其需求量身定制的同步原语,例如上面所示的那些。在这里,我们展示了如何使用ZooKeeper实现锁,以表明它可以实现各种各样的常规同步原语。
最简单的锁实现使用“锁文件”。该锁由znode表示。为了获取锁,客户端尝试使用EPHEMERAL标志创建指定的znode。如果创建成功,则客户端将持有该锁。另外,客户端可以读取设置了监听标志的znode,以便在当前领导者死亡时得到通知。客户端死亡或显式删除znode时会释放该锁。其他等待锁的客户端一旦观察到znode被删除,就会再次尝试获取锁。
尽管此简单的锁协议有效,但确实存在一些问题。首先,它具有羊群效应。如果有许多等待获取锁的客户端,则即使只有一个客户端可以获取该锁,当锁释放后他们都将争夺该锁。其次,它仅实现互斥锁。以下两个原语显示了如何同时解决这两个问题。
-
没有羊群效应的简易锁
我们定义一个锁znode l来实现这种锁。直观地,我们排序所有请求锁的客户端,每个客户端都按照请求到达的顺序获得锁。因此,希望获得该锁的客户端执行以下操作:
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行中使用SEQUENTIAL标志决定了客户端获取该锁尝试的顺序(相对于所有其他尝试)。如果客户端的znode在第3行的序列号最小,则客户端将持有该锁。否则,客户端将等待该znode的删除(在该客户端的znode之前持有锁或将获取到锁)。通过仅监听客户端znode之前的znode,我们仅在锁释放或锁请求放弃时才唤醒一个进程,从而避免了羊群效应。一旦客户端监听的znode消失,客户端必须检查它现在是否持有该锁。(先前的锁请求可能已被放弃,并且具有较低序号的znode仍在等待或持有该锁。)
释放锁就像删除代表锁请求的znode n一样简单。通过在创建时使用EPHEMERAL标志,崩溃的进程将自动清除所有锁请求或释放它们可能拥有的任何锁。
总之,此锁定方案具有以下优点:
- 删除一个znode只会导致一个客户端唤醒,因为每个znode都恰好被另一个客户端监视,因此我们没有羊群效应;
- 没有轮询或超时;
- 由于我们实现锁的方式,因此通过浏览ZooKeeper数据可以看到锁争用、中断锁和调试锁问题的数量。
-
读/写锁
为了实现读/写锁,我们略微更改了锁过程,并分开了读锁和写锁过程。释放锁过程与全局锁情况相同。
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行略有不同,因为只有较早的写锁znode会阻止客户端获得读锁。当有多个客户端在等待读锁时,当序列号较低的“write-” znode被删除时等待读锁的客户端会收到通知,我们似乎会有“羊群效应”。实际上,这是一种期望的行为,所有那些读客户端都应被释放,因为它们现在可以持有锁。
-
双重屏障
双重屏障使客户端能够同步计算的开始和结束。当由屏障阈值定义的足够多的进程加入该屏障时,进程将开始计算并在完成后马上离开屏障。我们在ZooKeeper中用znode表示一个屏障,称为b。每个进程p都会在进入时通过创建b的子节点的znode来向b注册,并在准备离开时通过删除该子节点来取消注册。当b的子znode数量超过屏障阈值时,进程可以进入屏障。当所有进程都删除了其子节点时,进程可能会离开屏障。我们使用监听来有效地等待进入和离开的条件得到满足。要进入,进程会监听是否存在b的就绪子节点,该子节点将由导致子节点数超过屏障阈值的进程创建。要离开,进程会监听特定的子节点消失,并且仅在删除znode之后检查离开条件。
3. ZooKeeper应用
现在,我们描述一些使用ZooKeeper的应用,并简要说明它们如何使用它。我们以粗体显示每个示例的原型。
3.1 The Fetching Service
爬虫是搜索引擎的重要组成部分,而Yahoo! 爬取数十亿个Web文档。The Fetching Service
(FS)是正在线上的Yahoo!爬虫的一部分。实质上,它具有一个控制页面提取过程的主进程。主进程为提取程序提供配置,并且提取程序回写通知其状态和运行状况。在FS中使用ZooKeeper的主要优点是可以从主进程的故障中恢复,即使出现故障也可以保证可用性,并且可以将客户端与服务器分离,从而允许他们仅通过从ZooKeeper读取状态即可将请求定向到正常的服务器。因此,FS使用ZooKeeper主要来管理配置元数据,尽管它也使用ZooKeeper来选举主进程(领导者选举)。
图2显示了三天中FS使用的ZooKeeper服务器的读写流量。为了生成此图,我们计算该时间段每秒内的操作数,每个点对应于该秒内的操作数。我们观察到,与写入流量相比,读取流量要高得多。在速率高于每秒1000次操作的时间段内,读:写比率在10:1和100:1之间变化。此工作负载中的读取操作为getData()
、getChildren()
和exist()
(按使用次数递增的顺序)。
3.2 Katta
Katta[17]是使用ZooKeeper进行协调的分布式检索器,它是非Yahoo!的应用示例。Katta使用分片划分检索工作。主服务器将分片分配给从服务器并跟踪进度。从服务器可能会发生故障,因此主服务器必须根据从服务器的加入和退出来分配负载。主服务器也可能发生故障,因此在发生故障时,其他服务器必须准备好接管。Katta使用ZooKeeper跟踪从服务器和主服务器(组成员身份认证)的状态,并处理主服务器故障恢复(领导者选举)。Katta还使用ZooKeeper来跟踪并将分片的分配传播给从服务器(配置管理)。
3.3 Yahoo! Message Broker
Yahoo! Message Broker
(YMB)是一个分布式的发布-订阅系统。该系统管理着数千个主题,客户端可以向其发布消息或从中接收消息。主题分布在一组服务器之间,以提供可扩展性。每个主题使用主备方案进行复制,该方案可确保将消息复制到两台计算机上,以确保可靠的消息传递。组成YMB的服务器使用无共享分布式架构,这使得协调对于正确操作至关重要。YMB使用ZooKeeper来管理主题(配置元数据)的分布,处理系统中机器的故障(故障检测和组成员身份认证)以及控制系统操作。
图3显示了YMB的znode数据布局的一部分。每个代理域都有一个称为节点的znode,该节点对组成YMB服务的每个活跃服务器都有一个临时znode。每个YMB服务器在具有负载和状态信息的节点下创建一个临时znode,并通过ZooKeeper提供组成员身份认证和状态信息。组成该服务并允许对YMB进行集中控制的所有服务器都将监听节点诸如关闭和禁止迁移之类操作。主题目录为YMB管理的每个主题都有一个子znode。这些主题的znode具有子znode,这些子znode表明每个主题的主服务器和备份服务器以及该主题的订阅者。主服务器和备份服务器znode不仅允许服务器发现负责主题的服务器,而且还管理领导者选举和服务器崩溃。
4. ZooKeeper的实现
ZooKeeper通过在组成服务的每台服务器上覆制ZooKeeper数据来提供高可用性。我们假设服务器因崩溃而失败,并且此类故障服务器稍后可能会恢复。图4显示了ZooKeeper服务的高级组件。收到请求后,服务器会为执行做准备(请求进程)。如果这样的请求需要服务器之间的协调(写请求),则它们使用一致性协议(原子广播的实现),最后服务器将更改提交到ZooKeeper数据库中,该更改已在整个服务器集群中完全复制。对于读取请求,服务器仅读取本地数据库的状态并生成对该请求的响应。
复制的数据库是包含整个数据树的内存数据库。默认情况下,数据树中的每个znode最多存储1MB数据,但是此最大值是一个配置参数,在特定情况下可以更改。为了实现可恢复性,我们将日志有效地更新到磁盘上,并且在其应用到内存数据库之前将其强制写入磁盘介质中。实际上,正如Chubby[8]一样,我们保留已提交操作的重播日志(在我们情况中是预写日志),并生成内存数据库的定期快照。
每个ZooKeeper服务器都为客户端提供服务。客户端仅连接到一台服务器以提交其请求。如前所述,读取请求是从每个服务器数据库的本地副本提供服务的。更改服务状态的请求(写请求)由一致性协议处理。
作为一致性协议的一部分,写请求被转发到称为领导者的单个服务器。其余的ZooKeeper服务器(称为跟随者)从领导者那里接收包含状态更改的消息提议,并就状态更改达成一致。
4.1 请求处理器
由于消息传递层是原子的,因此我们保证本地副本不会偏离,尽管在某些时间点某些服务器可能比其他服务器应用了更多的事务。与客户端发送的请求不同,事务是幂等的。领导者收到写请求后,它将计算写操作应用时系统的状态,并将其转换为捕获此新状态的事务。因为可能存在尚未应用到数据库的未完成事务,所以必须计算将来的状态。例如,如果客户端执行条件setData,并且请求中的版本号与正在更新的znode的将来版本号匹配,则该服务将生成一个setDataTXN,其中包含新数据、新版本号和更新的时间戳。如果发生错误,例如版本号不匹配或要更新的znode不存在,则会生成errorTXN。
4.2 原子广播
所有更新ZooKeeper状态的请求都转发给领导者。领导者执行该请求,并通过Zab[24](一种原子广播协议)将更改广播给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快照称为模糊快照。相反,我们对数据树进行深度优先扫描,以原子方式读取每个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的值分别为f3和g2,版本分别为3和2。但是,模糊快照可能已记录/foo和/goo的值分别为f3和g1,版本为3和1,这不是ZooKeeper数据树的有效状态。如果服务器崩溃并使用此快照恢复,并且Zab重新发送状态更改,则结果状态与服务崩溃前的状态一致。
4.4 客户端-服务器交互
服务器处理写请求时,还会发出并清除与该更新对应的任何监听有关的通知。服务器按顺序处理写入,并且不会同时处理其他写入或读取。这确保了通知的严格继承。请注意,服务器在本地处理通知。仅客户端连接到的服务器跟踪并触发给该客户端的通知。
读取请求在每个服务器的本地处理。每个读取请求都被处理并用zxid标记,该zxid对应于服务器看到的最后一个事务。该zxid定义了相对于写入请求的读取请求的偏序。通过本地处理读取,我们获得了出色的读取性能,因为它只是本地服务器上的内存操作,并且没有要运行的磁盘活动或一致性协议。这种设计选择对于我们以读取为主的工作负载下实现出色性能的目标至关重要。
使用快速读取的一个缺点是不能保证读取操作的优先顺序。也就是说,即使已提交对同一znode的更近的更新,读取操作也可能返回旧的值。并非我们所有的应用都需要优先顺序,但是对于确实需要优先顺序的应用,我们已经实现了同步。该原语异步执行,并在所有未完成更新写入其本地副本后由领导者排序。为了保证给定的读取操作返回最新的更新值,客户端将先调用sync,然后再执行读取操作。客户端操作的FIFO顺序保证与同步的全局保证一起使读取操作的结果能够反映发出同步之前发生的所有更改。在我们的实现中,我们不需要原子广播同步,因为我们使用基于领导者的算法,我们只需将同步操作放在领导者和执行同步调用的服务器之间的请求队列的末尾。为了使它起作用,跟随者必须确保领导者仍然是领导者。如果有已提交的待处理事务,则服务器不会怀疑该领导者。如果待处理队列为空,则领导者需要发出一个空事务来提交并对该事务后的同步进行排序。它具有很好的特性,即当领导者处于负载状态时,不会产成额外的广播流量。在我们的实现中,设置超时时间是为了使领导者在跟随者放弃他们之前意识到他们不是领导者,因此我们不会发布空事务。
ZooKeeper服务器按FIFO顺序处理来自客户端的请求。响应中包括对应的zxid。甚至在无活动间隙期间的心跳消息也包括了客户端连接到的服务器看到的最后一个zxid。如果客户端连接到新服务器,则该新服务器通过对照其最后一个zxid与客户端的最后一个zxid来检查其ZooKeeper数据的视图至少与客户端的视图一样新。如果客户端的视图比服务器的视图新,则服务器在追赶上之前不会与客户端重新建立会话。保证客户端能够找到另一台具有最新系统视图的服务器,因为客户端只能看到已经复制到大多数ZooKeeper服务器的更改。此行为对于保证持久性很重要。
为了检测客户端会话失败,ZooKeeper使用超时。如果会话超时期间没有其他服务器从客户端会话收到任何消息,则领导者将确定其存在故障。如果客户端足够频繁地发送请求,则无需发送任何其他消息。否则,客户端会在活动不足时发送心跳消息。如果客户端无法与服务器通信以发送请求或心跳,则它将连接到其他ZooKeeper服务器以重新建立其会话。为了防止会话超时,ZooKeeper客户端库在会话闲置了s/3ms后发送心跳信号,如果在2s/3ms内未收到服务器的消息,则切换到新服务器,其中s是会话超时时间(以毫秒为单位)。
5. 评估
我们在50台服务器的集群上进行所有评估。每个服务器都有一个Xeon双核2.1GHz处理器,4GB RAM,千兆以太网和两个SATA硬盘驱动器。我们将以下讨论分为两部分:吞吐量和请求延迟。
5.1 吞吐量
为了评估我们的系统,我们以系统饱和时的吞吐量以及各种注入故障的吞吐量变化为基准。我们改变了组成ZooKeeper服务的服务器数量,但始终保持客户端数量不变。为了模拟大量的客户端,我们使用了35台机器来模拟250个并发客户端。
我们有ZooKeeper服务器的Java实现,以及客户端的Java和C的实现。对于这些实验,我们使用配置为登录到一个专用磁盘并在另一个磁盘上生成快照的Java服务器。我们的基准客户端使用异步Java客户端API,每个客户端至少有100个未完成的请求。每个请求都包含对1K数据的读取或写入。我们没有展示其他操作的基准,因为所有修改状态的操作的性能大致相同,并且非状态修改操作(不包括同步)的性能大致相同。(由于请求必须发送到领导者,但不会广播,因此同步的性能近似于轻量级写操作。)客户端每300ms发送一次已完成操作的计数,我们每6s采样一次。为了防止内存溢出,服务器会限制系统中并发请求的数量。ZooKeeper使用请求限制来防止服务器过载。对于这些实验,我们将ZooKeeper服务器配置为最多可处理2000个请求。
在图5中,我们展示了吞吐量,因为我们改变了读写请求的比率,每条曲线对应于提供给ZooKeeper服务的不同的服务器数量。表1显示了极限读取负载时的数量。读取吞吐量高于写入吞吐量,因为读取不使用原子广播。该图还显示服务器的数量也对广播协议的性能产生负面影响。从这些图中,我们观察到系统中的服务器数量不仅影响服务可以处理的故障数量,而且还影响服务可以处理的工作负载。请注意,三个服务器的曲线与其他服务器的曲线在约60%处相交。这种情况并不排除三服务器配置,并且由于启用了本地读取的并行性,所有配置都会发生这种情况。但是,这对于图中的其他配置而言看不到,因为我们为可读性设置了最大y轴吞吐量。
写请求比读请求花费更长的时间有两个原因。首先,写请求必须经过原子广播,这需要一些额外的处理并增加请求的延迟。较长时间处理写请求的另一个原因是,服务器必须确保在将确认发送回领导者之前,将事务记录到非易失性存储中。原则上,此要求过高,但由于ZooKeeper构成了应用的正确根基,因此对于我们的生产系统,我们以可靠性为代价来交换性能。我们使用更多的服务器来容忍更多的错误。通过将ZooKeeper数据划分为多个ZooKeeper集合,我们提高了写入吞吐量。Gray等人[12]先前已经观察到复制和分区之间的这种性能折衷。
ZooKeeper可以通过在组成服务的服务器之间分配负载来实现如此高的吞吐量。由于我们宽松的一致性保证,我们可以分配负载。相反,Chubby的客户端会将所有请求定向到领导者。图6显示了如果我们不利用这种宽松而强迫客户端仅连接到领导者,会发生什么。正如预期的那样,对于读取为主的工作负载,吞吐量要低得多,但是即使对于写入为主的工作负载,吞吐量也要低。为客户端提供服务会导致额外的CPU和网络负载,从而影响领导者协调提议广播的能力,进而对总体写入性能产生不利影响。
原子广播协议完成了系统的大部分工作,因此,ZooKeeper的性能比其他任何组件都受到更多的限制。图7显示了原子广播组件的吞吐量。为了测试其性能,我们通过直接在领导者处生成事务来模拟客户端,因此没有客户端连接或客户端请求和回复。在最大吞吐量下,原子广播组件成为CPU瓶颈的。从理论上讲,图7的性能将与100%写操作的ZooKeeper的性能相匹配。但是,ZooKeeper客户端通信、ACL检查以及对事务转换的请求均需要CPU。对CPU的争用将ZooKeeper吞吐量降低到实质上低于隔离状态下的原子广播组件。因为ZooKeeper是关键的生产组件,所以到目前为止,我们对ZooKeeper的开发重点一直是正确性和鲁棒性。有很多机会可以显著提高性能,如通过消除多余的副本、同一对象的多个序列化、更有效的内部数据结构等方面。
为了显示随着时间的推移系统在注入故障时的行为,我们运行了由5台计算机组成的ZooKeeper服务。我们使用与以前相同的饱和度基准,但是这次我们将写入百分比保持在恒定的30%,这是我们预期工作负载的保守比率。我们定期杀死了一些服务器进程。图8显示了系统吞吐量随时间变化的情况。图中标记的事件如下:
- 跟随者的失败和恢复;
- 另一个跟随者的失败和恢复;
- 领导者的失败;
- 前两个标记中有两个跟随者(a,b)失败,并到第三标记(c)恢复;
- 领导者失败。
- 领导者的恢复。
该图有一些重要的观察结果。首先,如果跟随者失败并迅速恢复,则ZooKeeper能够在失败的情况下维持高吞吐量。单个跟随者的故障不会阻止服务器作出仲裁,而只会由于共享服务器在故障之前正在处理的读取请求来稍微降低吞吐量。其次,我们的领导者选举算法能够足够快地恢复,以防止吞吐量大幅下降。根据我们的观察,ZooKeeper只需不到200ms即可选出新的领导者。因此,尽管服务器停止了几分之一秒的请求服务,但由于我们的采样周期(秒级),我们没有观察到吞吐量为零。第三,即使跟随者需要更多时间来恢复,ZooKeeper也可以在开始处理请求后再次提高吞吐量。我们没有在事件1、2和4之后恢复到满吞吐量级别的原因之一是,客户端仅在其与跟随者的连接断开时才切换跟随者。因此,在事件4之后,直到领导者在事件3和事件5失败之前,客户端不会重新分配自己。实际上,随着客户端来来往往,这种失衡会随着时间的流逝逐渐消失。
5.2 请求延迟
为了评估请求延迟,我们创建了一个以Chubby基准[6]为模型的基准测试。我们创建一个工作进程,该工作进程仅发送一个创建、等待它完成、发送新节点的异步删除,然后开始下一个创建。我们会相应地更改工作程序的数量,并且对于每次运行,我们每个工作程序创建50000个节点。我们将完成的创建请求数除以所有工作程序完成所花费的总时间来计算吞吐量。
表2显示了我们基准测试的结果。创建请求包含1K的数据,而不是Chubby基准中的5个字节,以更好地与我们的预期使用相吻合。即使有这些较大的请求,ZooKeeper的吞吐量也比Chubby公布的吞吐量高出3倍以上。单个ZooKeeper工作者基准测试的吞吐量表明,三台服务器的平均请求延迟为1.2ms,而九台服务器的平均请求延迟为1.4ms。
5.3 屏障的性能
在本实验中,我们依次执行许多屏障来评估使用ZooKeeper实现的原语的性能。对于给定数量的屏障b,每个客户端首先进入所有b屏障,然后依次离开所有b屏障。当我们使用第2.4节中的双重屏障算法时,客户端首先要等待所有其他客户端执行enter()
过程,然后再移至下一个调用(与Leave()
类似)。
我们在表3中报告了我们的实验结果。在此实验中,我们分别有50、100和200个客户连续输入b个障碍,b属于集合{200; 400; 800; 1600}
。尽管一个应用可以有成千上万的ZooKeeper客户端,但是由于每个客户端通常根据应用的具体情况进行分组,因此通常每个协调操作中都只会涉及一个较小的子集。
该实验的两个有趣观察结果是,处理所有屏障的时间随屏障数量的增加而近似线性增长,这表明并发访问数据树的相同部分不会产生任何额外的延迟,并且延迟随着客户端数量成比例地增加。这是不使ZooKeeper服务饱和的结果。实际上我们观察到,即使客户端步调一致,在所有情况下,屏障操作(进入和离开)的吞吐量在每秒1950至3100次操作之间。在ZooKeeper操作中,这对应于每秒10700至17000次操作之间的吞吐量值。在我们的实现中,读写比率为4:1(读操作占80%),与ZooKeeper可以达到的原始吞吐量(根据图5超过40000)相比,我们的基准代码使用的吞吐量要低得多。这是由于客户端在等待其他客户端。
6. 相关工作
ZooKeeper的目标是提供减轻分布式应用中协调过程问题的服务。为了实现此目标,其设计使用了以前的协调服务、容错系统、分布式算法和文件系统的思想。
我们不是首先提出用于协调分布式应用的系统的人。一些早期的系统提出了一种分布式锁服务,用于事务应用[13],并用于在计算机集群中共享信息[19]。最近,Chubby提出了一种用于管理分布式应用的咨询锁的系统[6]。Chubby与ZooKeeper有几个相同的目标。它具有类似于文件系统的接口,并且使用一致性协议来确保副本的一致性。但是,ZooKeeper不是锁服务。客户端可以使用它来实现锁,但是其API中没有锁操作。与Chubby不同,ZooKeeper允许客户端连接到任何ZooKeeper服务器,而不仅仅是领导者。ZooKeeper客户端的一致性模型比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等人讨论了一种在不修改当前服务器代码库的情况下使ZooKeeper完全具有拜占庭容错能力的方法[9]。迄今为止,我们还没有观察到使用完全拜占庭容错协议可以避免的生产错误[29]。
Boxwood[21]是使用分布式锁服务器的系统。Boxwood为应用提供了更高级别的抽象,并且依赖于基于Paxos的分布式锁服务。与Boxwood一样,ZooKeeper是用于构建分布式系统的组件。但是,ZooKeeper具有高性能要求,并且在客户端应用中得到了更广泛的使用。ZooKeeper暴露了应用用来实现高级原语的低级原语。
ZooKeeper类似于小型文件系统,但仅提供文件系统操作的一小部分,并增加了大多数文件系统中不存在的功能,例如排序保证和有条件的写入。ZooKeeper监听在本质上与AFS[16]的高速缓存回调相似。
Sinfonia[2]引入了小事务,这是用于构建可扩展分布式系统的新范例。Sinfonia旨在存储应用数据,而ZooKeeper存储应用元数据。ZooKeeper保持状态完全复制并存储在内存中,以实现高性能和在要求的延迟。我们对文件系统的使用(如操作和排序)使功能类似于小事务。znode是添加监听的便捷抽象,Sinfonia中缺少此功能。Dynamo[11]允许客户端在分布式键值存储中获取及写入相对少量(少于1M)的数据。与ZooKeeper不同,Dynamo中的键值空间不是分层的。Dynamo还没有为写入提供强大的持久性和一致性保证,而是解决了读取冲突。
DepSpace[4]使用元组空间来提供拜占庭容错服务。像ZooKeeper一样,DepSpace使用简单的服务器接口在客户端实现强大的同步原语。尽管DepSpace的性能远低于ZooKeeper,但它提供了更强的容错能力和保密保证。
7. 结论
ZooKeeper通过将无等待对象暴露给客户端,从而采用无等待方法来解决分布式系统中的协调过程问题。我们发现ZooKeeper对于Yahoo!内部和外部的多个应用很有用。ZooKeeper通过使用带有监听的快速读取功能来实现以读取为主的工作负载的每秒数十万次操作的吞吐量值,这两者均由本地副本提供服务。尽管我们对读取和监听的一致性保证似乎很弱,但是我们已经通过用例表明,这种结合使我们能够在客户端实现高效且复杂的协调协议,即使读取没有优先顺序并且数据对象的实现也是无等待的。事实证明,无等待特性对于高性能至关重要。
尽管我们仅描述了少数几个应用,但还有许多其他使用ZooKeeper的应用。我们相信,成功的原因在于其简单的接口以及可以通过此接口实现的强大抽象。此外,由于ZooKeeper的高吞吐量,应用可以广泛使用它,而不仅仅是粗粒度锁。
致谢
我们要感谢Andrew Kornev和Runping Qi对ZooKeeper的贡献;Zeke Huang和Mark Marchukov提供了宝贵的反馈;Brian Cooper和Laurence Ramontianu为ZooKeeper的早期贡献;Brian Bershad和Geoff Voelker在展示中给出了重要评论。
参考文献
- M. Abd-El-Malek, G. R. Ganger, G. R. Goodson, M. K. Reiter, and J. J. Wylie. Fault-scalable byzantine fault-tolerant services. In SOSP ’05: Proceedings of the twentieth ACM symposium on Operating systems principles, pages 59–74, New York, NY, USA, 2005. ACM.
- M. Aguilera, A. Merchant, M. Shah, A. Veitch, and C. Karamanolis. Sinfonia: A new paradigm for building scalable distributed systems. In SOSP ’07: Proceedings of the 21st ACM symposium on Operating systems principles, New York, NY, 2007.
- Amazon. Amazon simple queue service. http://aws.amazon.com/sqs/, 2008.
- A. N. Bessani, E. P. Alchieri, M. Correia, and J. da Silva Fraga. Depspace: A byzantine fault-tolerant coordination service. In Proceedings of the 3rd ACM SIGOPS/EuroSys European Systems Conference - EuroSys 2008, Apr. 2008.
- K. P. Birman. Replication and fault-tolerance in the ISIS system. In SOSP ’85: Proceedings of the 10th ACM symposium on Operating systems principles, New York, USA, 1985. ACM Press.
- M. Burrows. The Chubby lock service for loosely-coupled distributed systems. In Proceedings of the 7th ACM/USENIX Symposium on Operating Systems Design and Implementation (OSDI), 2006.
- M. Castro and B. Liskov. Practical byzantine fault tolerance and proactive recovery. ACM Transactions on Computer Systems, 20(4), 2002.
- T. Chandra, R. Griesemer, and J. Redstone. Paxos made live: An engineering perspective. In Proceedings of the 26th annual ACM symposium on Principles of distributed computing (PODC), Aug. 2007.
- A. Clement, M. Kapritsos, S. Lee, Y.Wang, L. Alvisi, M. Dahlin, and T. Riche. UpRight cluster services. In Proceedings of the 22 nd ACM Symposium on Operating Systems Principles (SOSP), Oct. 2009.
- J. Cowling, D. Myers, B. Liskov, R. Rodrigues, and L. Shira. Hq replication: A hybrid quorum protocol for byzantine fault tolerance. In SOSP ’07: Proceedings of the 21st ACM symposium on Operating systems principles, New York, NY, USA, 2007.
- G. DeCandia, D. Hastorun, M. Jampani, G. Kakulapati, A. Lakshman, A. Pilchin, S. Sivasubramanian, P. Vosshall, and W. Vogels. Dynamo: Amazons highly available key-value store. In SOSP ’07: Proceedings of the 21st ACM symposium on Operating systems principles, New York, NY, USA, 2007. ACM Press.
- J. Gray, P. Helland, P. O’Neil, and D. Shasha. The dangers of replication and a solution. In Proceedings of SIGMOD ’96, pages 173–182, New York, NY, USA, 1996. ACM.
- A. Hastings. Distributed lock management in a transaction processing environment. In Proceedings of IEEE 9th Symposium on Reliable Distributed Systems, Oct. 1990.
- M. Herlihy. Wait-free synchronization. ACM Transactions on Programming Languages and Systems, 13(1), 1991.
- M. Herlihy and J. Wing. Linearizability: A correctness condition for concurrent objects. ACM Transactions on Programming Languages and Systems, 12(3), July 1990.
- J. H. Howard, M. L. Kazar, S. G. Menees, D. A. Nichols, M. Satyanarayanan, R. N. Sidebotham, and M. J. West. Scale and performance in a distributed file system. ACM Trans. Comput. Syst., 6(1), 1988.
- Katta. Katta - distribute lucene indexes in a grid. http://katta.wiki.sourceforge.net/, 2008.
- R. Kotla, L. Alvisi, M. Dahlin, A. Clement, and E. Wong. Zyzzyva: speculative byzantine fault tolerance. SIGOPS Oper. Syst. Rev., 41(6):45–58, 2007.
- N. P. Kronenberg, H. M. Levy, and W. D. Strecker. Vaxclusters (extended abstract): a closely-coupled distributed system. SIGOPS Oper. Syst. Rev., 19(5), 1985.
- L. Lamport. The part-time parliament. ACM Transactions on Computer Systems, 16(2), May 1998.
- J. MacCormick, N. Murphy, M. Najork, C. A. Thekkath, and L. Zhou. Boxwood: Abstractions as the foundation for storage infrastructure. In Proceedings of the 6th ACM/USENIX Symposium on Operating Systems Design and Implementation (OSDI), 2004.
- L. Moser, P. Melliar-Smith, D. Agarwal, R. Budhia, C. Lingley-Papadopoulos, and T. Archambault. The totem system. In Proceedings of the 25th International Symposium on Fault-Tolerant Computing, June 1995.
- S. Mullender, editor. Distributed Systems, 2nd edition. ACM Press, New York, NY, USA, 1993.
- B. Reed and F. P. Junqueira. A simple totally ordered broadcast protocol. In LADIS ’08: Proceedings of the 2nd Workshop on Large-Scale Distributed Systems and Middleware, pages 1–6, New York, NY, USA, 2008. ACM.
- N. Schiper and S. Toueg. A robust and lightweight stable leader election service for dynamic systems. In DSN, 2008.
- F. B. Schneider. Implementing fault-tolerant services using the state machine approach: A tutorial. ACM Computing Surveys, 22(4), 1990.
- A. Sherman, P. A. Lisiecki, A. Berkheimer, and J. Wein. ACMS: The Akamai configuration management system. In NSDI, 2005.
- A. Singh, P. Fonseca, P. Kuznetsov, R. Rodrigues, and P. Maniatis. Zeno: eventually consistent byzantine-fault tolerance. In NSDI’09: Proceedings of the 6th USENIX symposium on Networked systems design and implementation, pages 169–184, Berkeley, CA, USA, 2009. USENIX Association.
- Y. J. Song, F. Junqueira, and B. Reed. BFT for the skeptics. http://www.net.t-labs.tu-berlin.de/˜petr/BFTW3/abstracts/talk-abstract.pdf.
- R. van Renesse and K. Birman. Horus, a flexible group communication systems. Communications of the ACM, 39(16), Apr. 1996.
- R. van Renesse, K. Birman, M. Hayden, A. Vaysburd, and D. Karr. Building adaptive systems using ensemble. Software - Practice and Experience, 28(5), July 1998.