翻译自《The Chubby lock service for loosely-coupled distributed systems》By Mike Burrows, Google Inc. 原文发表于2006年,最初的设计和实现在2003年。现在的Chubby相比于当年的最初设计发生了变化,变成了consistency and high availability service
我们描述了我们在Chubby锁服务方面的经验,该服务旨在为松散耦合的分布式系统提供粗粒度的锁和可靠的(尽管是低容量的)存储。Chubby提供了一个类似于分布式文件系统的接口,并带有建议锁,但设计的重点是可用性和可靠性,而不是高性能。该服务的许多实例已经使用了一年多,其中有几个实例都能同时处理几万个客户。本文描述了最初的设计和预期的使用情况,与实际使用情况进行了比较,并解释了如何修改设计以适应不同的情况。
本文描述了一个名为Chubby的锁服务。它的目的是在一个松散耦合的分布式系统中使用,该系统由中等数量的小型机器组成,通过高速网络连接。例如,一个Chubby实例(也称为Chubby单元)可以为一万台由1Gbit/s以太网连接的4处理器机器服务。大多数Chubby单元被限制在一个数据中心或机房内,尽管我们至少运行一个Chubby单元,其副本相隔数千公里。
锁服务的目的是让其客户同步他们的活动,并就其环境的基本信息达成一致。主要目标包括可靠性、对中等规模客户的可用性,以及易于理解的语义;吞吐量和存储容量被认为是次要的。Chubby的客户端界面类似于一个简单的文件系统,它执行整个文件的读写,并带有咨询锁和各种事件的通知,如文件修改。
我们期望Chubby能够帮助开发者处理他们系统中的粗粒度同步问题,特别是处理从一组各方面对等的服务器中leader的选举问题。例如,谷歌文件系统[7]使用Chubby锁来指定一个GFS主服务器,而Bigtable[3]在以下几个方面使用Chubby:选举一个主服务器,让主服务器发现它所控制的服务器,并允许客户端找到主服务器。此外,GFS和Bigtable都使用Chubby作为一个众所周知的可用位置来存储少量的元数据;实际上,它们使用Chubby作为其分布式数据结构的根。一些服务使用锁在几个服务器之间划分工作(粗粒度级别)。
在Chubby部署之前,谷歌的大多数分布式系统(当工作可以被无害重复时)都使用专门自定制的方法进行主备选举,或者(当正确性至关重要时)依赖操作者干预。在前一种情况下,Chubby允许节省少量的计算工作。在后一种情况下,它在系统的可用性方面取得了重大改进,不再需要人为的故障干预。
熟悉分布式计算的读者会认识到,在对等体中选举一个主站是分布式共识问题的一个实例,并认识到我们需要一个使用异步通信的解决方案;这个术语描述了绝大多数真实网络的行为,如Ethernet以太网或Internet互联网,它们允许数据包丢失、延迟和重新排序。(实践者通常应该提防基于模型的协议,这些模型对环境做了更强的假设)。异步共识是由Paxos协议[12, 13]解决的。Oki和Liskov使用了相同的协议(见他们关于viewstamped复制的论文[19, §4]),其他人注意到了这种等价性[14, §6]。事实上,到目前为止,我们遇到的所有异步共识的工作协议都以Paxos为核心。Paxos在没有时间假设的情况下保持了安全性,但必须引入时钟以确保有效性;这克服了Fischer等人[5, §1]的不可能结果。
构建Chubby是为了满足上述需求而进行的工程努力;这不是research。我们没有宣扬有新的算法或技术。本文的目的是描述我们所做的事情和原因,而不是鼓吹它。在接下来的章节中,我们描述了Chubby的设计和实现,以及它是如何根据经验而改变的。我们描述了Chubby被使用的意外方式,以及那些被证明是错误的功能。我们省略了文献中其他地方涉及的细节,例如共识协议或RPC系统的细节。
有人可能会说,我们应该建立一个体现Paxos的库,而不是一个访问集中式锁服务的库,即使是一个高度可靠的锁。一个客户端Paxos库将不依赖于其他服务器(除了name服务),并为程序员提供一个标准的框架,假设他们的服务可以被实现为状态机。事实上,我们提供了这样一个独立于Chubby的客户端库。
尽管如此,锁服务比客户端库有一些优势。首先,我们的开发者有时并不像人们希望的那样为高可用性做计划。通常,他们的系统开始时是原型,只有很少的负载和松散的可用性保证;无一例外的是,代码没有为使用共识协议而进行专门的结构设计。随着服务的成熟和客户的增加,可用性变得更加重要;复制和主备选举被添加到现有的设计中。虽然这可以通过一个提供分布式共识的库来完成,但一个锁服务器可以更容易地维护现有的程序结构和通信模式。例如,要选出一个主站,然后写入现有的文件服务器,只需要在现有的系统中增加两个语句和一个RPC参数。一个服务器将获得一个锁以成为主服务器,在写RPC时传递一个额外的整数(锁的获得数),并在文件服务器上添加一个if语句,如果获得数低于当前值,则拒绝写入(以防止数据包延迟)。我们发现这种技术比让现有的服务器参与共识协议更容易,尤其是在过渡期内必须保持兼容性的情况下。
第二,许多需要选举主服务器的服务或在其组件之间划分数据的服务需要一种机制来公布结果。这表明我们应该允许客户存储和获取少量的数据,即读写小文件。这可以通过名称服务来完成,但我们的经验是,锁服务本身非常适合这项任务,因为这减少了客户所依赖的服务器的数量,也因为协议的一致性特征是共享的。Chubby作为一个名称服务器的成功在很大程度上归功于它使用了一致的客户端缓存,而不是基于时间的缓存。特别是,我们发现,开发人员非常高兴不必选择缓冲超时,如DNS的生存时间值,如果选择不好,会导致DNS的高负载,或更长的客户端故障切换时间。
第三,基于锁的接口对我们的程序员来说更熟悉。Paxos的复制状态机和与独占锁相关的关键部分都可以为程序员提供顺序编程的错觉。然而,许多程序员曾经接触过锁,并且认为他们知道如何使用它们。具有讽刺意味的是,这样的程序员通常是错误的,特别是当他们在分布式系统中使用锁的时候;很少有人考虑独立机器故障对具有异步通信的系统中的锁的影响。尽管如此,在说服程序员使用分布式决策的可靠机制方面,锁的显著熟悉性克服了一个障碍。
最后,分布式共识算法使用投票人数(quorums)来做决定,所以它们使用几个副本来实现高可用性。例如,Chubby本身在每个单元中通常有五个副本,其中三个必须在运行,单元才会正常。相比之下,如果一个客户系统使用锁服务,即使是一个客户也可以获得一个锁,并安全地取得进展。因此,锁服务减少了一个可靠的客户系统取得进展所需的服务器数量。在一个宽松的意义上,人们可以把锁服务看作是提供一个通用选举人的方式,它允许客户端系统在自己的成员少于多数时正确地做出决定。人们可以想象用另一种方式来解决最后这个问题:通过提供 "共识服务",使用一些服务器来提供Paxos协议中的 "接受者"。像锁服务一样,共识服务将允许客户安全地取得进展,即使只有一个活跃的客户进程;类似的技术已经被用来减少拜占庭容错所需的状态机数量[24]。然而,假设共识服务不是专门用来提供锁的(这使它沦为一个锁服务),这种方法就不能解决上述的其他问题。
这些论点表明了两个关键的设计决定。
- 我们选择了一个锁服务,而不是一个用于共识的库或服务,以及
- 我们选择了提供小文件,以允许当选的主服务器广播他们自己和他们的参数,而不是建立和维护另一个服务。
一些决定来自于我们的预期使用和我们的环境。
- 一个通过Chubby文件广播主服务器信息的服务可能有成千上万的客户。因此,我们必须允许成千上万的客户观察这个文件,最好是不需要很多服务器。
- 客户端和服务器副本可能希望知道服务的主服务器何时改变。这表明,事件通知机制对于避免轮询是有用的。
- 即使客户端不需要定期轮询文件,许多人也会这样做;这是支持许多开发者的结果。因此,文件的缓存是可取的。
- 我们的开发者被非直观的缓存语义所迷惑,所以我们更喜欢一致的缓存。
- 为了避免经济损失和牢狱之灾,我们提供安全机制,包括访问控制。
一个可能会让一些读者感到惊讶的选择是,我们并不期望锁的使用是细粒度的,在这种情况下,它们可能只保持很短的时间(几秒钟或更短);相反,我们期望粗粒度的使用。例如,一个应用程序可能会使用锁来选举一个主程序,然后在相当长的时间内(可能是几个小时或几天)处理对该数据的所有访问。这两种使用方式表明了对锁服务器的不同要求。
粗粒度的锁对锁服务器的负载要小得多。特别是,锁的获取率通常与客户应用的交易率只有微弱的关系。粗粒度锁很少被获取,所以暂时的锁服务器不可用对客户的影响较小。另一方面,一个锁从客户端转移到另一个客户端可能需要代价极高的恢复程序,所以我们不希望锁服务器的故障切换导致锁丢失。因此,对于粗粒度的锁来说,最好能够在锁服务器发生故障的情况下生存,且不需要担心对这样做的开销,而且这样的锁允许许多客户在相对低可用性的条件下使用由数量不多的锁服务器提供的服务。
细粒度的锁导致了不同的结论。即使是锁服务器的短暂不可用,也可能导致许多客户停滞。性能和随意添加新服务器的能力是非常值得关注的,因为锁服务器的交易率会随着客户的综合交易率而增长。通过在锁服务器故障时不维护锁来减少锁的开销是很有利的,而且由于锁被保持的时间很短,所以每隔一段时间丢掉锁的时间惩罚并不严重。客户端必须准备好在网络分区期间丢失锁,所以在锁服务器故障时丢失锁不会带来新的恢复路径)。
Chubby打算只提供粗粒度的锁。幸运的是,客户可以直接为他们的应用实现自己的细粒度锁。一个应用程序可以将其锁分成若干组,并使用Chubby的粗粒度锁将这些锁组分配给特定的应用程序锁服务器。维护这些细粒度锁所需的状态很少;服务器只需要保持一个非易失性的、单调增长的获取计数器,该计数器很少更新。客户端可以在解锁时得知丢失的锁,如果使用一个简单的固定长度的租约,该协议可以变的简单而高效。这个方案最重要的好处是,我们的客户端开发者负责提供支持其负载所需的服务器,但却免除了自己实施共识的复杂性。