`
msmummy
  • 浏览: 2816 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

Chubby翻译

 
阅读更多

 

两年前的毕设翻译,存档。


The Chubby Lock Service for Loosely-Coupled Distributed Systems(原文),毕设的翻译。2.10节以前由阿 灿(nEver)翻译,2.10节后由我(msmummy)翻译。比较乱,存个档先。

 

摘要 

我们描述我们关于Chubby锁服务的实验。Chubby锁服务试图为一个松耦合的分布式系统同时提供粗粒度锁和可靠的存储(尽管容量不大)。Chubby提供了一个很类似于带劝告性锁的分布式文件系统的接口,但设计的重点在于有效性和可靠性,这两点是和高性能相对的。很多这项服务的应用已经被使用了将近一年,他们中的一些同时处理数万计的客户端。这篇论文描述了最初的设计和期望的应用,和实际的应用作了比较,并且解释了最初的设计是如何不得不修改以适应于这些不同。 

 

1      简介 

这篇论文描述了一种被称为Chubby的锁服务。它试图应用于一个由大量小机器通过高速网络连接而组成的松耦合分布式系统。举个例子,一个Chubby应用可能服务于数万计通过1Gbit/s 以太网连接的4处理器机器。大多数Chubby单元只限于一个单独的数据中心或机房中,尽管我们运行的一个Chubby元的副本可能分布在几千公里外的地方。 

这一锁服务的意图是让它的客户端能同步他们的活动,同时能对于它们环境中基本信息达成一致。首要的目标包括可靠性,对于大数量客户端的可用性,以及易懂的语义;吞吐量和存储的容量被次要考虑。Chubby的客户端接口类似于一个拥有完整文件读写功能的简单文件系统,同时额外地带有劝告性锁,以及多种事件的通知,比如文件修改。 

我们期望Chubby能够帮助开发者处理他们系统的粗粒度同步,特别的,能处理在一组其他方面等价的服务器中选举出一个领导者的问题。举个例子,Google文件系统使用Chubby lock来选举出一个GFS master server,而Bigtable 通过多种方式来使用Chubby:选举master,允许master发现它控制的服务,以及允许客户端发现master。另外,GFSBigtable都将Chubby作为以一个众所周知和可用的位置来存储少量的元数据;实际上它们都将Chubby作为它们的分布式数据结构的基础部分。一些服务使用锁来给多个服务器划分工作。

Chuuby被部署之前,Google的大多数分布式系统使用ad hoc方法来进行primary election(当工作能够无损复制时),或者需要操作的干预(当错误不可避免的时候)。对于前一种情况,Chubby能够提供稍微地减少计算的花费。对于后一种情况,它能让系统不再需要对错误进行人工干预,从而极大地提高系统的有效性。

对于分布式文件系统熟悉的读者,能认识到选举问题就是分布式一致性问题的一个实例,同时能意识到我们需要一个使用异步通讯的解决方法;异步通讯这一术语描述了属于大部分现实网络的行为,比如以太网和互联网。这些网络允许包的丢失,延迟和重排序(从业者应该当心基于对环境进行了稳定性假设的模型的协议)。Paxos协议解决了异步通讯。OkiLiskov使用了这一协议,其他的则使用了一个等价的。对于目前为止我们所有的可工作的用于处理异步通信的协议,核心部分都由Paxos组成。Paxos不依靠定时假定来确保安全性,但是必须依靠时钟来确保系统运作;这种方法克服了Fischer et al的不可能结果。

构建Chubby是通过工程上的手段来满足以上提到的要求;它不是一项研究。我们没有提出新的算法或者技术。这篇论文的目的是来描述我们做了什么和我们为什么这么做,而不是提倡它。在接下来的部分,我们将描述Chubby的设计和实现,以及它是怎么通过实验来进行改进。我们将描述Chuuby是如何通过一些非期望中的方式被使用,已经一些被证明是错误的特征。我们省略了在别的文献中可以查到的细节,比如一个RPC系统的一致性协议的细节。

2      设计

2.1     基本原理

有人可能说我们构建了一个具体化了Paxos的库,而不是一个能提供集中式的锁服务的库,甚至不是一个能提供高度可信的服务的库。一个客户端Paxos库应该是不依靠任何其他服务器,(除了名字服务器),同时应该能为程序员提供一个标准的框架,假定他们的服务能够作为状态机而得以应用。实际上,我们提供一个这样的客户端,而这一客户端是独立于Chubby的。

然而,一个锁服务相对于一个客户端库是有一些优势的。首先,有时我们的开发者没有考虑到高可用性。通常他们的系统的最初原型是低负载以及宽松的可用性保证;代码总是没有被设计成能够使用一致性协议。当这个服务逐渐成熟,拥有了更多的用户,可用性变得越来越重要;于是副本和选举被加入到一个已有的设计中。虽然这个能够通过一个提供分布式一致性的库来实现,但是对于修改已有的程序结构和通讯模式的工作,锁服务能够让其更简单。比如,为了选举一个master,并使其能写到一个已有的文件服务器上,我们只需要增加两个状态和一个RPC参数到现有系统中:一个节点可以通过获得一个锁从而成为master,然后通过write RPC传递一个额外的整数(锁获得计数),最后增加一个if状态到文件服务器中,if状态为:如果这个获得计数比当前的值小,则拒绝这个write(用以防止延迟包)。我们发现这个技术比尝试使一个已有系统适应于一致性协议要容易,特别是当在一个转变时期,兼容性必须被保障的时候。

其次,我们的许多选举或是划分数据的服务,需要一个机制来广播结果。这就意味着我们必须允许客户端存储和获取小规模的数据,也就是说,能够对小文件进行读写。这可以通过一个名字服务器来实现,但是我们的实验表明锁服务本身就能很好的完成这个任务。这不仅因为锁服务能减少一个客户端所需要的服务器数量,还因为协议的一致性特征一同被分享了。Chubby作为一个名字服务器的成功主要归于它使用了一致性的用户缓存,而不是基于时间的缓存。我们尤其发现,开发者非常高兴于不用非得去选择一个缓存超时,例如DNS time-to-live valueDNS time-to-live value能引起高DNS负载,或者是重时间的客户端出错。

再次,我们的程序员对于一个基于锁的接口更为熟悉。Paxos的复制状态机和独占锁相关的临界区都能让程序员感觉自己是在进行顺序编程。但是,很多程序员之前都接触过锁,而且认为自己知道怎么去使用它。带有讽刺意味的是,这样的程序员往往经常出错,特别是当他们在一个分布式系统中使用锁时;在一个异步通信的系统中,单个机器的锁错误所造成的影响,往往被他们忽视。然而,这种对于锁的表面上的熟悉消除了程序员在使用一个新的可靠的机制的障碍。

最后,分布式一致算法使用法定人数来做出决定,所以它们通过使用副本来获取高可用性。比如,Chubby通常在一个元中使用5个副本,要让该元启动的话,要求至少其中的3个运作。与之相反的,如果一个客户端系统使用一个锁服务,就算只有一个客户在运作,也能获得一个锁并且安全的运行。从而,一个锁服务减少了一个一个可靠的客户端系统正常运作所需要的服务器数目。从一种宽松的意义说,可把锁服务看成是一种提供一个一般性的选民的方式,这种方式允许一个客户端系统始终能做出正确的决定,即使它的所属成员中只有较少一部分能运行。有人可能想到通过另外一种方式来解决这个问题:通过提供一种“一致性”服务,使用一定数量的服务器来提供Paxos协议中的“acceptors”。和锁服务类似的,一个一致性服务允许客户端始终能正常安全地工作,即使只有一个运行中的客户端进程。一个类似的技术被应用于减少Byzantine错误容忍所需的状态机的数目。然而,采取一个一致性服务的方式并没有专门地被用来提供锁,这一个方法解决不了任何一个之前提出的问题。

这些讨论得出了两个主要的决定:

l  我们选择一个锁服务,而不是一个库或是一致性服务。

l  我们选择为小文件服务从而允许被选中的领导者能广播他们自己和他们的参数,而不是构建和维护一个次级服务

另外一些决定源自我们期望的应用和我们的实验环境:

l  一个服务通过一个Chubby文件来广播它的领导者,它可能有上千个客户端。因此,我们必须让这上千个的目录都能访问到这个文件,这样比提供更多的服务器要更好。

l  客户端和副本可能希望能知道服务器的领导者何时会改变。这就说明一个事件改变机制会很有助于避免投票。

l  即使客户端不需要定期的投票文件,大多数还是会这么做。这是一个支持很多开发者的结论。因此,对这些文件进行缓存是很必要的。

l  我们的开发者总是迷惑于非直觉就能理解的缓存语义,所以我们倾向于使用一致性的缓存

l  为了避免经济损失和违法行为,我们提供安全机制,包括有使用权控制

我们的一个选择可能会让一些读者感到惊讶。我们并不期望锁的使用是细粒度的。因为细粒度的锁可能只被持有一小段时间(几秒甚至更少)。作为替代,我们使用粗粒度的锁。比如,一个应用可能使用一个锁来选举领导者,然后这个领导者就可以控制这个应用数据的使用权一段时间,几小时或者一天。这两种不同类型的使用表明对于一个锁服务器会有不同的要求。

粗粒度的锁不会给锁服务器太多的负载。锁获取速率通常只和客户端应用的事务发生率有很微弱的关系。粗粒度的锁的获取频率很低,所以临时的锁服务器失效对客户端影响很小。另一方面,锁在用户间的传递可以能需要开销很大的回复过程,所以我们不希望因为一次锁的出错而导致锁丢失。因此,粗粒度锁能在锁服务器失效的情况下存活下来,是一个很有好处的事情,而且基本不需要关心这样实现的开销。这样的锁允许许多客户端都能被一定数目的锁服务器充分地服务到,而仅仅只带来一点点可用性的降低。

细粒度的锁会导向多个不同的结论。甚至是锁服务器的轻微失效都会导致很多客户端的停止。性能和能够随意地增加新的服务器的能力很重要,因为锁服务的交换率随着客户端的总交换率的增长而增长。选择不维护锁服务的失效很有利于减少锁的开销,而且时常发生的丢失锁的时间惩罚并不是很严格,因为锁只被短时间持有。。(客户端必须在网络分区之间为丢失锁做好准备,这样因为锁服务器的失效而引起的锁丢失就不会带来新的恢复路径)。

Chubby只提供粗粒度锁。幸运的是,用户能够很方便地将它们自己的细粒度锁应用到它们的应用中。一个应用可能将它的锁分成组,然后使用Chubby的粗粒度锁来将这些组分配给特定的应用锁服务器。维护这些细粒度锁只需要很少的状态;服务器只需要维护一个非易失的,单调增的获取数,而这个数会很少被更新。客户端能够在解锁的时候发现锁的丢失。如果使用了一个简单的,定长的租约,协议可以变得简单而有效率。这一计划最重要的好处在于我们的客户端开发者会负责提供给服务器一定的资源,以保证其能承担它们的负载,同时这还能减轻客户端自己保持一致性的复杂度。

2.2     系统结构

Chubby有两个重要的组件:一个服务器,和一个客户端应用使用的库,这两个组件通过RPC来通讯;见图1。客户端库控制Chubby客户端和服务器间的所有通讯。一个额外的第三方组件—一个代理服务器,将在3.1节进行讨论。

一个Chubby元由一定数量的服务器(通常5个)组成,这些服务器又称为副本,以减少相关错误发生的可能性。副本们使用一个分布式的一致性协议来选举一个master;这个master必须获得大部分副本的投票,另外保证这些副本在几秒的间隔时间内不换选举另外的一个master,这段间隔时间被称为master租期。副本周期性地更新这个master租期,并让master继续赢得大多数的投票。

副本维护一个简单数据库的拷贝,但是只有master能够对这个数据库进行读写操作。所有的其他副本,只能从master拷贝更新,这个更新使用一致性协议发送。

客户端通过给副本发送master定位请求来找到master,所有副本都列在DNS中。当收到这个请求时,非master的副本用master的身份来回复。一旦客户端找到了master,客户端开始把所有请求发送给master,直到客户端收到了停止的回复,或者master发现自己已经不再是一个master。写请求通过一个一致性协议发送给各个副本;只有大多数的副本都接收到这个请求时,写操作才能进行。读请求只需要master进行处理;当master租期没有到期时,这样是安全的,他因为没有其他的可能存在的master。如果一个master失效,其他副本就会在他们的master租期到期时,运行选举协议;一般会在几秒内就选举出来一个新的master。例如,两次最近的选举花费了6s4s,但是有时可能高达30s(4.1)

如果一个副本失效,并且在几小时内都没有恢复,一个简单的替换系统就会在一个空闲池里选择一个新机器了,然后在它上面启动一个锁服务。之后,这个系统会更新DNS表,用新机器的IP地址来代替失效副本的IP地址,当前的master周期性的查看DNS,并且最终注意到这个变化,然后它会更新数据库中的元成员列表;这个列表通过“正常复制协议”来保持在各个成员间的一致性。同时,新的副本从一个备份组合中获得最近的一个数据库拷贝,这个备份组合保存在文件服务器上,并且通过活动的副本更新。一旦新的副本发送一个当前master在等待获得的请求,这个副本就被允许参与到新master的投票中。

2.3     文件,目录和句柄

Chubby提供一个和UNIX相似的文件系统结构,但是更简单。它由一个精确的文件和目录树组成,其中名称组件有斜线分开。一个典型的名字为:

/ls/foo/wombat//pouch

前缀ls是所有Chubby名字共有的,代表锁服务(lock service)。第二个组件(foo)是Chubby元的名字;它通过DNS查找来确定一个或者更多的Chubby服务器。有一种特殊的元被称为local,表明客户端的本地Chubby元必须被使用;这种元通常在同一个建筑中,而且最有可能被访问到。名字的剩下部分:/wombat/pouch,由Chubby元自身来解释。再次和UNIXl类似的,每个路径包括了一系列的子文件和子目录,每个文件又由一系列未解释的字节组成。

因为Chuuby的名字结构类似与一个文件系统,不仅是通过它自己的API,通过其他文件系统的接口也能使用它的应用,比如GFS。这个特点使得我们不需要再去写基本的浏览和名字空间操作工具,而且也不用再专门地去教会Chubby的使用者。

这个设计和UNIX不同的部分是为了利于分布。为了让不同的Chubby master是来服务不同目录中的文件,我们不提供将文件从一个文件夹移动到另一个文件夹中的操作,我们不维护目录修改时间,同时我们还避开了基于路径的权限语义(即,文件的访问权限由其自身来维护,而不是基于这个文件所处的目录)。为了更容易的对文件的元数据进行缓存,系统不显示文件的最后访问时间。

名字空间只包括文件和目录,被共同称为节点。每一个这样的节点在它所在的元中只有一个名字,没有符号链接或者是硬链接。

节点可以使不变的或者是暂时的。任何节点都可能明确地被删除,但是暂时性的节点在没有被任何用户打开的时候就会被删除(对于目录,则是清空)。暂时性的文件被用作是临时文件,而且告诉其他文件一个客户端还在运行。任何一个节点都可以作为也给建议性的读写锁。这些锁匠在2.4节中更详细的描述。

每一个节点都有很多不同的元数据,包括访问控制列表的三个名字(ACLs,用来控制对这个节点ACL名字的读,写和修改。除非是被拒绝,一个节点在创建的时候集成它父目录的ACL名字。ACLs是在一个ACL路径中的文件,这个路径在元的本地名字空间中是公开的。这些ACL文件由简单的主要名字列表组成;读者可能想起Plan 9s groups[21]。一次,一个文件F的写ACL名是foo,而ACL目录下有一个文件foo而且这个文件有一个入口bar,那么用户bar就会被允许对F进行写操作。用户通过一个RPC系统中的机制来认证。因为ChubbyACLs是简单的文件,其他的一些希望使用类似的访问控制机制的服务能自动能访问它们。

每个节点的元数据包括四个单调增的64位数字,从而允许用户很容易地发现它们的变化。

l  一个实例数字;比任何一个之前的拥有通用名字的节点的实例数字都要大。

l  一个内容产生数字(只有文件拥有);当文件进行写操作时,该数字增加。

l  一个锁产生数字;当这个节点的锁从空闲变成获取是该数字增加。

l  一个ACL产生数字;当该节点的ACL名字被写时,该数字增加。

Chubby还显示一个64位的文件内容检验和。这样用户能够分辨出文件的不同。

Chubby打开节点来获得一个类似于UNIX文件描述符的句柄,句柄包括:

l  检验位,防止客户单穿件或者猜测句柄,所以完整的访问控制检验只在句柄创建时进行。(相比较于UNIXUNIX在打开时检查它的权限位,而不是在每次读写操作时,因为文件描述符不能伪造)。

l  一个序列号,通过它,一个maste可以知道一个句柄是由它还是由前一个master产生。

l  在打开时提供模式信息,当一个旧的句柄交给一个刚重启的master时,mstaer就可以重新设置它的状态。

2.4     锁和序列

每一个Chubby文件和目录能够作为一个读写锁;一个客户端句柄能以独有模式来获得这个所,也可以是很多客户端句柄以分享模式来获得这个锁。像大多数程序员所知道到互斥锁一样,这些锁是建议性的。也就是说,它们只和其他的意图来获得相同锁的进行竞争:获得一个锁F,即不需要访问文件F,也不阻止其他客户端这么做。我们没有使用强制锁,强制锁锁上的文件,对于那些没有获得锁的客户端,是不能访问的:

Chubby锁通常保护其他服务使用的资源,而不仅仅是和这个锁相关的文件。如果用一种有意义的方式来强制执行一个强制锁,我们就必须对这些服务做做更多的修改。

当用户想访问锁文件来进行调试或者管理时,我们不希望强制她们关闭他们的应用。在一个复杂的系统中,使用那些已经应用在大多数的个人电脑上的方法会显得很难。在个人电脑上,管理程序能简单地通过建议用户关掉他的应用或是重启来打破强制锁。

我们的开发者通过常有的方式来进行错误检查,通过警报信息比如“锁X已经被持有”,这样他们从强制锁中获益很少。恶意的进程在锁没有被持有时有很多机会能够修改数据,所以我们发现强制锁所提供的额外守护没有多少意义。

Chubby中,通过任一模式来获得一个锁都需要一个写允许,这样,一个无特权的读者不能阻止一个写者的工作。

分布式系统中的锁很复杂,因为交流通常是不可靠的,而且进程可能独立的挂掉。比如,一个持有锁L的进程可能发出请求R,然后挂掉。另一个进程可能来获取L然后在R到达它的目的地之前进行同样的动作。如果R在之后到达了,它可能在没有L的保护之下进行操作,然后导致了不一致的数据。乱序地收到信息的问题已经被很好地研究过了;方法包括有虚拟时间和虚拟同步,都是通过确保所有有的信息按照一个所有参与者看来都一致的次序发送来避免这个问题。

要在一个现存的复杂系统中将序列号加入到所有交互中花费很高。因此,Chubby采用的方法是:序列号只加入到那些使用了锁的交互中。任意时候,一个锁持有者可以请求得到一个序列号-一个不透明的描述锁获取后状态的字节字符串。它包括锁的名字,它被获取的模式(独占还是共享),以及锁产生数。如果客户端希望操作被锁保护的话,它就将这个序列号传递给服务器(比如文件服务器)。接受的服务器则需要测试这个序列号是否还是有效的以及是否有这正确的模式。如果否的话,它就应该拒绝这个请求。一个序列号的有效性能够通过检查服务器的Chubby缓存确定,或者,如果服务器不想维护一个和Chubby的会话,可以通过检查服务器之前观察到的最近序列号。这个序列号机制只需要额外地一个影响信息的字符串,而且能很容易地解释给开发者。

尽管我们发现序列号很容易使用,然而重要的协议发展得很慢。因此Chubby提供了一个有缺点但是更容易的机制来减少延迟或是乱序到达服务器的不支持序列号的请求所带来的风险。如果一个客户端通过一个正常方式释放掉一个锁,这个锁立刻就能被其他的客户端获取。然而,如果一个锁因为他的持有者挂掉了或者连接不上了而空闲了,锁服务器会在一段时间阻止其他的客户端去获得这个锁,这段时间成为lock-delay。客户端可能给lock-delay指定一个范围,通常是一分钟。这个限制确保一个错误的客户端不会过长时间的占有一个锁。尽管有缺陷,lock-delay保护了未经修改的服务器和客户端不受信息延迟还重启引起的问题的影响。

2.5     事件

Chubby客户端在创建一个句柄时,可能发生一系列的事件。这些事件通过Chuby库中的up-call操作异步的发送给这个客户端。事件包括有:

l  文件内容修改-通常用来通过文件控制广播一个服务器的位置。

l  增加,删除或者修改子节点-用于镜像。

l  Chubby master挂掉了-警告客户端其他的事件可能已经丢失,需要重新检查数据。

l  一个句柄(和它的锁)无效-这通常是一个通讯问题。

l  锁获取-可以用来确定一个primary何时被选举出来。

l  另一个客户端造成的锁请求冲突-允许对锁进行缓存。

在相应的行动进行了之后,事件被发送。因此,如果一个客户端被告知文件内容已经被修改,他在随后读这个文件时就需要保证能看到新数据。

上面提到的最后两个事件很少发生,而且通过事后处理完全可以忽略。例如,在primary选举后,客户端通常需要和新的primary通讯,而不是仅仅知道有一个primary存在就可以了,因为,他们等待一个文件修改事件,表明新的primary已经把它的地址写入到一个文件中。理论上冲突锁事件允许客户端缓存其他服务器的数据,使用Chubby锁来维护缓存的一致性。一个冲突锁请求的通知将会告知一个客户端停止使用和这个锁相关的数据;客户端将会停止未结束的操作,刷新修改到一个home位置,丢弃缓存数据,并且释放。到目前为止,还没有人采取过这种应用。

2.6     API

客户端将一个Chubby句柄视为一个指向一个不透明结构的指针,这个结构可以支持多种操作。句柄只能通过open()创建,通过close()清除。

Open()打开一个命名了的文件或目录来生成一个句柄,类似于一个UNIX文件描述符。只有这个调用能得到一个节点名字,其他的只能对句柄操作。

通过一个已存的目录句柄来计算得到名字;库提供了一个”/”目录的句柄,并且这个句柄永远有效。目录句柄避免了在一个多进程拥有多层抽象物的程序中使用一个全局性的当前目录的困难。

客户端表明了多个选项:

l  句柄如何使用(读,写和上锁;修改ACL);只有当客户端有着适当的权限时,句柄才会创建。

l  需要发送的事件(2.5节)

l  锁延迟(2.4节)

l  需要创建一个文件或者目录。

 

 

 

 

 

[TO-DO]

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2.10 数据库实现

Chubby的第一个版本采用多副本的Berkeley Db作为数据库。Berkeley Db提供了B树结构,可以映射字节串的关键字到任意的字节串的值。我们安装了一项关键字比较的功能,可以优先按照路径名中的组件编号进行排序;这使得节点既可以按照路径名得到关键字,同时在排序中兄弟节点又可以相邻。由于Chubby中不使用基于全路经的许可,所以一次数据库查询就可以获得单个文件的许可权限。

Berkeley Db使用分布式一致性协议在一系列的机器上复制数据库日志。一旦添加了主服务器的租约功能,就非常符合Chubby的设计,使得实现起来非常直观。

虽然Berkeley DbB树代码已经较为成熟且被广泛使用,它的副本代码却是刚刚添加的,并且没有很多的用户。软件维护者必须的首要任务是维护和改善他们产品最常用的功能。虽然Berkeley Db的维护者解决了我们的问题,我们仍然觉得使用他们的副本代码使我们承受了过多的风险。所以,我们写了一个简单的数据库,采用预写日志和快照机制,与Birrell等的设计类似。如同之前,数据库日志使用分布式一致性协议分发在所有副本上。Chubby使用了Berkeley Db很少的特性,所以这次的重写在整体上极大的简化了系统;例如,我们需要原子性操作,但我们不需要通用的事务。

2.11 备份

每隔几个小时,每个Chubby节点的主节点就把它数据库的快照备份到不同建筑的GFS文件服务器。放在不同建筑内既可以避免建筑损坏带来数据丢失,而且避免了备份的循环依赖;在同建筑内的GFS节点可能会依赖Chubby节点来选举它的主节点。

备份不仅可以用于灾难恢复,并且可以用于新添加节点的初始化工作,避免增加在服务中的节点的负载。

 

2.12 镜像

Chubby允许一系列文件从一个节点镜像到另一个节点。镜像的速度很快,因为文件尺寸较小,而且事件的机制允许在一个文件添加、修改、删除后立即通知镜像模块。假设没有网络问题,文件修改会在1s内镜像到世界范围的所有服务器。如果一个镜像不能访问,那么在连接修复前它保持不变。更新的文件会通过他们的识别码进行识别。

镜像经常用于拷贝配置文件到全世界不同的计算集群。一个特殊的节点,名为global,包含一个子树/ls/global/master,它被镜像到所有其他Chubby节点的/ls/cell/slave子树上。全球节点很特殊是因为它的五个子节点分布在世界各地,这样方便全球各组织机构的访问。

global节点镜像的文件当中还包括Chubby自己的访问控制列表,Chubby和其他系统的宣传文件,方便用户接入数据存储的指针,如Bigtable节点,以及许多其他系统的配置文件。

 

3      扩展机制

Chubby的客户端是独立的进程,所以Chubby必须要处理超出平常预期数量的客户端访问。我们曾经遇到超过90,000个客户端同时和一个Chubby主节点通信,该数目远远超过实际物理机器的数量。因为每个节点只有一个主服务器,而它所在的物理机器与客户端不同,这样客户端可能会使主服务器超负荷。所以,系统扩展的最有效机制就是减少与主服务器的通信数量。假设主服务器没有严重的性能错误,对主服务器端请求处理的修建不能产生很大影响。我们采取如下措施:

创建任意数量的Chubby节点;客户端几乎总是可以使用最近的节点(由DNS得到),避免对远程机器的依赖。我们的典型部署是一个Chubby节点对应一个数千台机器的数据中心。

当负载过高时,主服务器可以把租约时间由默认的12s增加到60s左右,减少了维持活动的RPC数量。(目前为止维持活动的开销比较大,而且负载过高的服务器较容易出现该问题;客户端对其它调用的延迟变化非常不敏感。)

Chubby客户端缓存文件数据,元数据,文件丢失,以及打开的句柄来减少对服务器的调用。

我们使用协议转换服务器把Chubby协议转换为比较不复杂的协议,例如DNS等。具体稍后讨论。

这里我们描述两种相似的机制,代理和分区,我们期望这两种机制可以让Chubby的规模进一步扩展。我们在具体实践中没有使用它们,但是已经设计完成,并有可能近期投入到使用。目前我们尚不需要考虑把系统扩展五倍以上:首先,需求的机器有限;其次,因为我们在Chubby的客户端和服务器端使用的机器相似,硬件升级使得每台机器的客户端数量增加,同时也使每个服务端的容量增加。

 

3.1     代理

Chubby协议可以被信任的进程代理(两侧使用相同的协议),把其他客户端的通信转发给Chubby元。代理可以代替Chubby服务器处理保持活动和读取请求,从而减轻系统负载;代理不能减少写流量,这些流量经过代理的缓存。但即使通过非常强势的缓存,写通信在Chubby系统中也只占有不超过百分之一的负载(见4.1节),所以通过代理可以极大增加客户端连接的数量。如果一个代理服务器处理Nproxy个客户端,维持活动的流量开销就相应减少Nproxy个,有可能是上万个甚至更多。代理的缓存可以减少读操作的通信,最显著的是读共享的平均流量——约是原先的十分之一(见4.1节)。但是因为目前在Chubby的负载中,读操作占10%以下,相比之下,维持活动开销的显著减少是目前更重要的效果。

代理机制加入了额外的RPC,包含写和第一次读。也许有人认为代理机制的加入使得Chubby元相比以前至少有两倍的更多机会暂时失效,因为每个通过代理连接的客户端依赖的是两台可能产生故障机器:它连接的代理和Chubby的主服务器。

 

3.2     分区

[TO-DO]

4      使用,意外和设计错误

4.1     使用和行为

下面表格中的数据可以作为一个Chubby元的快照;RPC频率按照10分钟统计。得到的数据是GoogleChubby元的典型情况。


 

上表中显示了如下事实:

l  许多文件用于名字服务。

l  配置文件、访问控制和元数据文件(与文件系统的超级区块相似)十分常见。

l  平均10个客户端(230k/24k  10)共用一个缓存的文件。

l  消极缓存非常重要

l  较少的客户端保持锁,而且共享锁很少;这个锁服务用于主服务器选举和在多副本上分配数据的情况是一致的。

l  RPC流量主要消耗是保持活动连接;有一部分的读操作(缓存未命中的情况下);有很少量的写和获取锁的操作。

现在我们简要介绍组件失效的典型原因。如果假设(最理想情况)一个Chubby元中有一个主服务器正常工作就算该Chubby元是工作的,我们对我们的Chubby元进行了数周的统计,统计数据达到700-日,得到61次工作异常。我们排除了由于数据中心维护需要关闭导致的故障。其他原因包括:网络拥堵、维护、负载、操作员失误、软件和硬件。大多数失效在15s以内,有52次在30s以内;Chubby元低于30s的失效对我们大多数的应用都不会有太大影响。引起其余九次失效的原因有网络维护(4)、疑似网络连接问题(2)、软件故障(2)和负载过大(1)。

在一部分元-年级的操作中,我们有六次丢失数据,因为数据库软件错误(4)和操作人员失误(2);没有因为硬件错误引起故障。具有讽刺意义的是,几次操作上的失误包括一些避免软件错误的升级。我们两次纠正了在没有主服务器的副本中由软件引起的错误。

Chubby的数据存放在内存(RAM)中,所以大部分操作的成本较低。在我们的产品中平均请求延时大约都在毫秒级的范围内,与负载无关,但在过载的情况下,请求延时急速增长而且会话会被丢弃。过载通常发生在有许多活动会话(>90,000)时,但也有其他原因可以导致过载:当客户端同时发出数百万的读操作时(在4.3节中描述),和客户端的库由于错误无法读取缓存,导致每秒钟产生上万次请求。因为大多数RPC是需要保持活动连接的,服务端可以通过增加会话租约时间的方法在较多活动客户端连接的情况下保持一个较低的请求延时开销(见第3章)。当大量写操作请求到达时,成组提交会减少每个请求的有效工作量,但这种情况比较少见。

在客户端测量的RPC读延迟收到RPC系统和网络的限制;对本地元的调用小于1ms,跨元调用250ms。写操作(需要锁操作)由于数据库的日志更新会有额外5-10ms的延时,但是如果一个近期失效的客户端缓存了该文件,则延时可能高达10s。但即便这种写操作的变数对平均请求延时的影响也是很小的,因为写操作是很少见的。

在会话没有被丢弃的情况下客户端对延时的变化非常不敏感。有一次,我们在open()中人工加入了延迟,又来限制非法的客户端(见4.5节);仅当延迟超过10s和被反复申请时开发人员才注意到。我们发现扩展Chubby的关键不在于服务端的性能;减少与服务端的通信会有更重大的影响。在服务端读/写代码没有做很大调整;我们检查后没有发现重大的错误,之后把精力放在了可能更有效果的扩展机制上。另一方面,如果有性能上的错误影响到了本地Chubby的缓存,而导致客户端每秒产生数千次的读操作,开发人员会立即注意到。

4.2     Java客户端

Google的基础架构几乎全部用C++实现,但是越来越多的系统开始用Java语言编写。对于具有复杂客户端协议和庞大客户端库的Chubby来说,这个趋势产生了一些不可预料的问题。

Java鼓励的是整个程序的可移植性,产生的额外代价是某种程度上不方便与其它语言链接。Java调用非原生库的常用机制是JNI,但它经常被认为笨重而且速度较慢。我们的Java程序员非常不喜欢JNI,为了避免使用JNI,他们宁愿把这个庞大的库完全用Java重写。

ChubbyC++客户端库有7,000行代码(可与服务端相比),而且客户端协议比较精致。维护Java的库需要很大的耐心和代价,因为一个没有缓存的实现会极大增加服务端的代价。所以我们的Java用户使用协议转换服务器的副本,来导出一个与Chubby客户端API很相似的简单的RPC协议。即便已经有经验,我们还是可能会在编写、运行和维护这个额外服务时付出一定代价。

4.3     作为名字服务的使用

虽然Chubby是作为一个锁服务设计的,但是我们发现它最经常被用作名字服务。

通常的互联网域名系统DNS的缓存是基于事件的。DNS入口有一个“存活时间”(TTL, time-to-live),如果DNS数据在该时间内没有更新的话则被抛弃。通常做法是选定一个合适大小的TTL,但是如果系统要求经常性的替换失效的服务器,那么只能设定一个很低的TTL,很容易导致DNS服务器过载。

例如,我们的开发人员经常执行包含数千进程的任务,而且每个进程都会与其它进程相通信,从而产生数千的平方量级的DNS查询。我们希望设定TTL60s;这样既可以允许我们在较短的时间内及时替换失效的组件,又不会被认定是过短的替换时间。在这种情况下,设一个很小的任务只包含三千个客户端,维持DNS缓存需要满足15万每秒的名字查询。(作为比较,一个双CPU 2.6GHz Xeon DNS服务器每秒大约可以处理5万次查询。)更大规模的任务会产生更坏的问题,况且可能许多任务可能会一起执行。在使用Chubby之前,DNS负载的变化对Google来说是一个比较严峻的问题。

作为对比,Chubby的缓存模块使用显式的失效机制,所以在没有变化的情况下,一个常定数量的保持活动连接就可以在客户端无限期地维持任意数量的缓存接入。一个双CPU 2.5GHz Xeon Chubby主服务器被验证可以处理9万个客户端同时直接与它通信(无代理)。这些客户端包括上述的包含大量通信模式的大规模任务。正因为无需检查每个名字就可以提供名字更新的能力非常具有吸引力,所以Chubby已经在大多数公司内被部署为提供名字服务。

虽然Chubby的缓存机制允许一个元组保持大量的客户端连接,峰值负载仍然是一个问题。当我们首次部署基于Chubby的名字服务,运行一个3千进程的任务(意味着产生9百万的请求)也会使Chubby的主服务器过载。为解决这个问题,我们把名字接入分组进行批处理使单个请求可以返回,而且把一个任务中的大量的(典型数目是100)相关进程中的名字映射进行缓存。

Chubby提供的缓存语义要比一个名字服务所需要的细致的多;名字服务的解决方案只需要按照时间的提醒,而不是完全的一致性。因此,也许可以使用一种专门为名字查询设计的简单的协议转换服务器来降低Chubby元组的负载。如果我们之前预见到Chubby可以用作名字服务,也许可以提早实现一个全能的代理,来避免后来实现的这个简单但是额外的服务。

还存在一个更进一步的协议转换服务器:Chubby DNS服务器。这样可以让DNS客户端利用存储在Chubby中的名字数据。这样的服务器比较重要,不仅可以简化DNS名字到Chubby名字的转换,而且可以使现有的不易转换的应用程序直接使用Chubby的名字服务,如浏览器。

4.4     故障切换的问题

主服务故障切换的初始设计要求主服务器在创建时向数据库写入新的会话。在Berkeley DB版本的锁服务器中。创建会话的系统开销在许多进程同时开始时会产生问题。为避免过载,主服务器作了相应修改,不在会话创建的时候存储到数据库,而是在它的第一次修改,锁获取或者打开临时文件时。另外,对于每个保持活动连接,活动的会话会有一定概率被记录在数据库中。这样,对只读会话的写操作可以及时的散播开。

虽然避免过载是必要的,但这项优化措施会产生意外,可能会导致新生的只读会话没有被记录在数据库中,一旦故障发生则会被丢弃。虽然这样的会话没有保持锁,但也是不安全的;如果所有被记录的会话在被丢弃的会话租约过期之前被重新载入到新的主服务器中,被丢弃的会话有可能会读取一段时间的损坏数据。这在实践中很少见;在一个大规模的系统中几乎总是会有一些会话没有被成功载入,这样就迫使新的主服务器无论如何要等待,一直到最大的租约时间过期。尽管如此,我们还修改了故障切换的设计,既可以避免这种影响,也可以避免现有机制给代理增加复杂程度。

在新的设计中,我们在数据库中不对任何会话进行记录,而是重新构建它们,就像现在主服务器重新构建句柄一样(见2.9节,第8段)。现在一个新的主服务器必须要等待一个最坏情况下的租约时间,因为它不确定是否所有的会话都被成功载入(见2.9节,第6段)。再一次的,这在实践中效果不明显,因为基本上不可能所有会话都被载入。

一旦会话可以脱离存储的状态被重新创建,代理服务器就可以管理主服务器不知道的会话。只对代理服务器可见的额外操作使得他们可以改变与锁关联的会话。这就允许当一个代理失效时,另一个代理接管它的客户端。未来主服务器所需的唯一改变就是主服务器要确保不能放弃与代理会话相关的锁或者临时文件句柄,除非另一个代理来声明拥有它们。

4.5     非法客户端

[TO-DO]

4.6     经验教训

[TO-DO]

5      与相关工作比较

Chubby是构建在成熟的思想理念上的。Chubby的缓存设计来自于分布式文件系统中的工作。它的会话和缓存令牌与Echo系统中的设计很相似;会话减少了租约的开销,如同V系统。提供一种通用型锁服务的思想来自于VMS,尽管该系统最初使用一种专用。高速内部网络来进行低延时的内部通信。如同缓存的模型,ChubbyAPI基于一个文件系统模型,意味着一个文件系统类型的名字空间很方便,而且不仅是对文件所言。

Chubby与一般的分布式文件系统相比,如EchoAFS,在性能和容量方面的期望不同:客户端不会读、写或存储大量的数据,而且也不期望有很大的吞吐量甚至是较低的延时,除非有缓存的情况下。它们所期望的是一致性、可用性、可靠性,这些特性在性能要求不高的情况下是容易被实现的。因为Chubby的数据库比较小,所以我们可以在线存储它的很多拷贝(通常是5个副本和一些备份),我们每天做几次完全备份,而且每隔几个小时,我们就会通过数据库状态的校验码,对几个副本进行比对。弱化的性能和存储容量需求使得我们可以用一个Chuby主服务器处理上万的客户端连接。通过引入这样一个中央节点供许多客户端共享信息和协同工作,我们解决了我们的系统开发人员面临的一系列问题。

在之前架构描述中涉及到大量的文件系统和锁服务的叙述,这避免了现在做大量无用的比较,所以我们现在只选择一个进行细节上描述:我们选择与Boxwood的锁服务比较,因为它是最近设计的,也是设计在松耦合环境中工作,但是它的设计与Chubby有许多不同之处,有些很有趣,有些是次要的。

Chubby实现了锁服务、可靠的小文件存储、单一服务中的会话/租约机制。相比之下,Boxwood将之分为了三部分,分别是:一个锁服务,一个Paxos服务(可靠的状态存储池),和一个错误检测服务。Boxwood同时使用它们三个,其它系统可以选择地使用各个独立的模块。我们认为这点设计上的不同来自于预期目标用户的不同。Chubby旨在面向多样的用户群和不同的应用程序组合;它的用户既包括编写新的分布式系统的专家,也包括写管理脚本的初学者。在我们的环境中,一个具有相似API的大型共享服务是很有吸引力的。相比之下,Boxwood提供一个工具集,服务对象是(至少在我们看来)小众的更专业的开发人员,他们的项目也许需要共享代码,但是不需要一起使用。

在许多情况下,Chubby要比Boxwood提供更高层次的接口。例如,Chubby结合了锁服务与文件的命名空间,而Boxwood的锁名字只是简单的字节序列。Chubby客户端默认缓存文件状态;BoxwoodPaxos服务的客户端可以通过锁服务实现缓存,但是大概只能使用Boxwood自身提供的缓存。

两个系统的默认参数有明显的不同,用以满足不同的需求。Boxwood系统的每个错误检查模块与客户端每隔200ms通信一次,超时时间为1s;Chubby的租约时间是12s,每7s交换一次保持活动连接。Boxwood的子模块通常使用23个副本实现可用性,我们通常每个元组5个副本。但是,仅仅这些细节上的不同不能说明设计上的迥异,而是说明了系统参数要如何设置才能要满足更多的客户端数量,以及不同项目共享带来的不确定性。

另一个值得注意的不同是Chubby引入了宽限期的概念,这是Boxwood所缺乏的。(回忆该宽限期可以允许客户端安全的渡过Chubby主服务器长时间的失效而不丢弃会话或锁。Boxwood的“宽限期”概念等同与Chubby中的“会话租约”,是一个不同的概念。)同样的,这点不同也是对系统扩展性和失效可能性的预期不同而导致的。虽然主服务器的故障切换比较少见,对于客户端来讲丢弃一个Chubby锁的代价也是很昂贵的。

最后,两个系统的锁服务是基于不同目的设计的。Chubby锁服务的量级更高,而且需要适合的排序来保护外部的资源,而Boxwood的锁更轻量级,主要面向系统内部服务。

6      总结

Chubby是一个分布式锁服务,被设计用作Google分布式系统中的各活动的松耦合的同步工作;随后它更广泛的用途是用作名字服务器和配置文件的存储池。

Chubby的设计基于一系列互相吻合很好的广为人知的思想理念:分布式一致性用在许多副本中来进行容错,客户端的一致性缓存用来降低服务端负载同时保持简单的语义,按照时间机制的更新,和一个与文件系统相似的接口。我们使用缓存、协议转换服务器和简单的负载适应机制,使之扩展到每个Chubby实例连接数万客户端进程。我们希望通过代理和分区机制使之进一步扩展。

Chubby已经成为Google内部主要的的名字服务;对例如MapReduce这样的系统它已经成为主要的交互机制;存储系统GFSBigtable使用Chubby来从一系列冗余副本中选举主服务器;而且它作为标准存储池存储要求高可用性的文件,如访问控制列表。

7      致谢

 

分享到:
评论
1 楼 zorksylar 2012-09-27  
mark!

相关推荐

Global site tag (gtag.js) - Google Analytics