http://blog.xiping.me/2010/12/google-chubby-in-chinese.html?#failoverhtml
本文翻译自Google的[The Chubby lock service for loosely-coupled distributed systems]。翻译此文旨在传播更多信息。翻译水平有限,时间少且文章太长了,质量通常。完整版本请阅读原文。若是转载请完整转载,并包含本申明。node
=========================================================================
咱们描述了咱们在Chubby锁服务方面的经历。Chubby的目标是为松散耦合的分布式系统,提供粗粒度的锁定、可靠的存储(尽管容量不大)。Chubby提供了一个很像带有意向锁的分布式文件系统的接口(interface),不过它的设计重点是可用性和可靠性,而不是高性能。许多Chubby服务实例已经使用了超过一年,其中的几个实例每一个都同时处理着数万台客户端。本文描述了最初的设计和预期使用,将之与实际的使用相比较,并解释了设计是怎样修改以适应这些差异的。
本文介绍了一种锁服务: Chubby。它被计划用在由适度规模的大量小型计算机经过高速网络互联构成的的松散耦合的分布式系统中。例如,一个Chubby实例(也叫Chubby单元(Cell))可能服务于经过1Gbit/s网络互联的一万台4核计算机。大部分Chubby单元限于一个数据中心或者机房,不过咱们运行着至少一个各个副本(replica)之间相隔数千千米的Chubby单元。
锁服务的目的是容许它的客户应用们(clients)同步它们的活动,并对它们所处环境的基本信息取得一致。主要的目标包括在适度大规模的客户应用群时的可靠性、可用性,以及易于理解的语义;吞吐量和存储容量被认为是次要的。Chubby的客户端接口很像这样一个简单的文件系统:能执行整文件的读和写,另外还有意向锁和和多种诸如文件改动等事件的通知。
咱们预期Chubby帮助开发者处理他们的系统中的粗粒度同步,特别是处理从一组各方面至关的服务器中选举领导者。例如Google文件系统(Google File System)[7]使用一个Chubby锁来选择GFS Master 服务器,Bigtable[3]以数种方式使用Chbbuy:选择领导者;使得Master能够发现它控制的服务器;使得客户应用(client)能够找到Master。此外,GFS和Bigtable都用Chubby做为众所周知的、可访问的存储小部分元数据(meta-data)的位置;实际上它们用Chubby做为它们的分布式数据结构的根。一些服务使用锁在多个服务器间(在粗粒度级别上)拆分工做。
在Chubby发布以前,Google的大部分分布式系统使用必需的、未提早规划的(ad hoc)方法作主从选举(primary election)(在工做可能被重复但无害时),或者须要人工干预(在正确性相当重要时)。对于前一种状况,Chubby能够节省一些计算能力。对于后一种状况,它使得系统在失败时再也不须要人工干预,显著改进了可用性。
熟悉分布式计算的读者会意识到在多个相同体(peer)间primay选举是分布式协同(distributed consensus)问题的一个特例,同时意识到咱们须要一种异步(asynchronous)通讯的解决方案。异步(asynchronous)这个术语描述了绝大多数真实网络(real networks)如以太网或因特网的行为:它们允许数据包丢失、延时和重排序。(专家们通常应该了解(真实网络的)协议集创建在对环境作了很强假设的模型之上。) 异步一致性由Paxos协议[12, 13]解决了。一样的协议被Oki和Liskov(见于他们有关Viewstamped replication的论文[19, $4])使用,其余人使用了等价的协议[14, $6]。实际上,迄今为止咱们遇到的全部可用的异步协同协议的核心都有Paxos。Paxos不须要计时假设来维持安全性,但必须引入时钟来确保活跃度(liveness)。这克服了Fisher等人的不可能性结果(impossibility result of Fisher et al.)[5, $1]。
构建Chubby是一种知足上文提到的各类需求的工程上的工做,不是学术研究。咱们声明没有提出新的算法和技术。本文的目的在于描述咱们作了什么以及为何这么作,而不是提出这些。在接下来的章节中,咱们描述Chubby的设计和实现,以及在实际过程当中它是怎么改变的。咱们描述了预料以外的Chubby的使用方式,以及被证实是错误的特性。咱们忽略了在其余文献中已经包括的细节,例如一致性协议或者RPC系统。
有人可能会争论说咱们应该构建一个包含Paxos的库,而不是一个访问中心化锁服务的库,即便是一个高可靠的锁服务。客户端Paxos库将不依赖于其余服务器(除了名字服务(name service)之外),而且假定他们的服务能够实现为状态机,将为开发者提供标准化的框架(and would provide a standard framework for programmers, assuming their services can be implemented as state machines)。事实上,咱们提供了一个这样的与Chubby无关的客户端库。
然而,一个锁服务具备一些客户端库不具备的优势。第一,有时开发者并无如人们但愿的那样,为高可用性作好规划。一般他们的系统从只有很小负载和宽松的可用性保证的原型开始,代码老是没有特别地构造为使用一致性协议。当服务成熟,获得了用户,可用性变得更重要了,复制(replaction)和主从选举(primary election)随后被加入到已有的设计中。 尽管这能经过提供分布式协同的库来搞定,但锁服务更易于保持已经存在的程序结构和通讯模式。例如,选择一个master,而后将选举结果写入一个已经存在的文件服务器中,只须要加两条语句和一个RPC参数到已经存在的系统中:一条语句请求一个锁以成为master,另外传递一个整数(锁请求计数)给写RPC,再加入一条if语句给文件服务器拒绝写入若是请求计数小于当前的值(用于防止延时的包)。咱们发现这种技术比将已有的服务器加入一致性协议更容易,尤为是在迁移期间(transition period)必须维持兼容性时。
第二,咱们的许多服务在选举parimary,或在它们的各个组件间划分数据时,须要一种公布结果的机制。这意味着咱们应该容许客户端存储和取得小量的数据–也就是读写小文件。这能经过名字服务来完成,可是咱们的经验是锁服务自身至关适合作这件事,既由于这样减小了客户端要依赖的服务器数,也由于协议的一致性特性是相同(shared)的。Chubby的做为一个名字服务器的成功,应很大程度上应归功于一致的客户端缓存,而不是基于时间的缓存。特别地,咱们发现开发者至关地赏识不须要选择一个像DNS生存时间值(time-to-live)同样的缓存超时值,这个值若是选择不当会致使很高的DNS负载或者较长的客户端故障修复时间。
第三,基于锁的接口更为程序员所熟悉。Paxos的复制状态机(replicated state machine)和与排他锁关联的临界区都能为程序员提供顺序编程的幻觉。但是,许多程序员已经用过锁了,而且认为他们知道怎么使用它们。颇为讽刺的是,这样的程序员常常是错的,尤为是当他们在分布式系统里使用锁时。不多人考虑单个机器的失败对一个异步通讯的系统中的锁的影响。无论怎样,对锁的表面上的熟悉性,打败了试图说服程序员们为分布式决策使用(其余)可靠机制的努力。
最后,分布式协同算法使用quorums作决策,因而它们使用多个副原本达到高可用性。例如,Chubby自己一般在每一个单元中有五个副本,Chubby单元要存活(to be up)的话必须保证其中三个副本在正常运行。相反,若是一个客户端系统使用一个锁服务,即便只有一个客户端能成功取到锁也能继续安全地运行。所以,锁服务能减小可靠的客户端系统运行所须要服务器数目。在更宽泛的意义上,人们可以将锁服务视做一种经过提供通用的全体选民(electorate)的方式,容许一个客户系统在少于其多数的成员存活(up)时正确地决策。人们能够设想用不一样的方式解决最后的这个问题:经过提供一个“协同服务”(consensus service), 使用一些服务器提供Paxos协议中的”acceptors”。像锁服务同样,“协同服务”也将容许客户端(clients)在即便只有一个活跃客户进程的状况下继续安全地运行。相似的技术曾经被用于减小拜占庭故障兼容问题(Byzantine fault tolerance)所需的状态机(state machines)数目[24].。然而,假如协同服务不专门地提供锁(也就是将其删减为一个锁服务),这种途径不能解决任意一个上文提到的其它问题。
上面这些讨论建议了咱们的两个关键的设计决策:
咱们选择一个锁服务,而不是一个库或者一致性服务,以及
咱们选择提供小文件,以使得被选出来的primaries能够公布它们自身以及它们的参数,而不是建立和维护另一个服务。
某些设计决策则来自于咱们预期的用途和咱们的环境:
一个经过Chubby文件来公布其Primary的服务,可能拥有数千的客户端。所以,咱们必须容许数千的客户端监视这个文件,而且最好不须要太多服务器。
客户端和有多个副本(replica)的服务的各个副本要知道何时服务的primary发生了变化。这意味着一种事件通知机制将颇有用,以免轮询。
即便客户端不须要间歇性地轮询文件,不少客户端仍是会这样作;这是支持许多开发者的结果。所以,缓存这些文件是很可取的。
咱们的开发者对不直观的缓存语义感到困扰,因此咱们倾向于一致的缓存(consistent caching)。
为了不金钱上的损失与牢狱之灾(jail time),咱们提供了包括访问控制在内的安全机制。
一个可能让一些读者吃惊的选择是,咱们不但愿锁被细粒度地使用,这种状况下这些锁可能只被持有一段很短的时间(数秒或更短);实际上(instead),咱们但愿粗粒度地使用。例如,一个应用程序可能使用锁来选举一个primary,而后这个primary在一段至关长的时间多是数小时或数天里,处理全部对数据的访问。这两种使用方式意味着对锁服务器的不一样的需求。
粗粒度的锁在锁服务器引入的负载要小得多。特别是锁的获取频率一般只会与客户端应用系统的事务频率只有很微弱的关联。粗粒度的锁不常被请求,因此临时性的锁服务器不可用给客户端的形成的延时会更少。在另外一方面,一个锁从客户端到另外一个客户端可能须要高昂的恢复处理,全部人们不但愿锁服务器的故障恢复形成锁的丢失。所以,粗粒度的锁可以经历锁服务器的失败而继续有效是颇有用的,这里不太在乎这样作的开销,而且这样的锁使得许多客户端可由少数可用性稍低的锁服务器服务得足够好(and such locks allow many clients to be adequately served by a modest number of lock servers with somewhat lower availability)。
细粒度的锁会有不一样的结论。即便锁服务的短暂的不可用也可能致使许多客户端挂起。由于锁服务上的事务频率将随着全部客户端的事务频率之和一块儿增加,性能和随意增长新服务器的能力十分重要。不须要在锁服务器失败之间维持锁能够减小锁定的开销,这是优点;频繁地丢弃锁也不是一个很严重的问题,由于锁只被持有一段很短的时间。(客户端必须准备好在网络分离期间丢失锁,所以锁服务器故障恢复形成的锁的丢失不会引入新的恢复路径。(Clients must be prepared to lose locks during network partitions, so the loss of locks on lock server fail-over introduces no new recovery paths.))
Chubby被计划为只提供粗粒度的锁定。幸运的是,对于客户端而言,实现根据其自身的应用系统定制的细粒度锁是很简单的。一个应用程序可能将它的锁划分红组,并使用Chubby的粗粒度锁将这些锁分组分配给应用程序特定的锁服务器。维护这些细粒度的锁只须要不多的状态,这些服务器只须要持有一个不常变的、单调递增的请求计数器,这个请求计数器不会常常被更新。客户端可以在解锁时发现丢失了的锁,而且若是使用了一个简单定长的租期(lease),其协议将会简单高效。这种模式的最重要的收益是咱们的客户端开发者对他们的负载所需的服务器的供应负责,也将他们从本身实现协同的复杂性中解放出来。
Chubby有两个主要的经过RPC通讯的组件:一个服务器和一个客户端应用连接的库,如图一所示。全部Chubby客户端和服务器之间的通讯由客户端库居间达成。还有一个可选的第三个组件,代理服务器,将在第3.1节讨论。
一个Chubby单元由一组称做副本集(replicas)的服务器(典型的是五个)组成,按下降关联失败的可能性来放置(例如分别放在不一样的机架)。这些副本使用分布式一致的协议来选举一个Master,Master必须从副本集获得多数投票,并获得副本集在一个持续数秒的被称为master租期(lease)的时间段内再也不选举另外一个不一样的Master的承诺。只要Master继续赢得大多数投票,这个master租期就会周期性地被副本集刷新。
副本集维护着一个简单数据库的备份集,可是只有master发起对数据库的读写。其余的全部副本简单地从master复制使用一致性协议传送的更新。
客户端们经过发送master位置请求给列在DNS中的副本集来查找master。非master的副本返回master的标识来回应这些请求。一旦一个客户端定位到master,它将全部请求指引到该master,直到该master中止回应或者指明本身再也不是master。写请求被经过一致性协议传播给全部副本,这些请求在写入达到Chubby单元中的多数副本时被答谢确认。杜请求有master独自知足,这样是安全的–只要master租期没有过时,由于没有别的master可能存在。若是一个master失败了,其余的副本在他们的master租期到期时运行选举协议,典型地,一个新的master将在几秒以内选举出来。例如,最近两次的选举花费了6秒和4秒,可是咱们也见太高达30秒的。
若是一个副本失败了而且在几个小时内没有恢复,一个简单的替换系统从一个空闲池选择一台新的机器,并在其上启动锁服务器的二进制文件(binary)。而后它将更形DNS表,将失败了的副本的IP替换为新启动的启动的IP。当前的master周期性地轮询DNS并最终注意到这个变化,而后它在该Chubby单元的数据库中更新单元成员列表,这个列表在全部成员之间经过常规的复制协议保持一致。与此同时,新的副本取得存储在文件服务器上的数据库备份和活跃副本的更新的组合。一旦新副本处理了一个当前master在等待提交的请求,新的副本就被许可在新master的选举中投票。
Chubby开放出一个相似于UNIX的文件系统[22]的接口,但比Unix的文件系统更简单。它由一个严格的文件和目录树组成,名字的各部分使用反斜杠划分。一个典型的名字以下:
/ls/foo/wombat/pouch
其中的ls前缀与全部的Chubby名字相同,表明着锁服务(lock service)。第二个部分(foo)是Chubby单元的名字,经过DNS查询被解析到一个或多个Chubby服务器。一个特殊的单元名字local,代表应该使用客户端的本地Chubby单元,这个Chubby单元一般在同一栋楼里,所以这个单元最有可能能访问到。名字的剩余部分,/wombat/pouch,由Chubby单元内部解析。一样跟UNIX同样,每一个目录包含一个全部子文件和子目录的列表,而每一个文件包含一串不解析的字节。
由于Chubby的命名结构组成了一个文件系统,咱们既能够经过它的专门API将它开放给应用系统,也能够经过咱们的其余文件系统例如GFS使用的接口。这样显著地减小了编写基本浏览和名字空间操做的工具的工做(effort),也减小了培训那些偶然用Chubby的用户的需求。
这种设计使得chubby接口不一样于UNIX文件系统,它使得分布更容易(The design differs from UNIX in a ways that easy distribution)。为容许不一样目录下的文件由不一样的Chubby Master来服务,咱们没有放出(expose)那些将文件从一个目录移动到另外一个目录的操做,咱们不维护目录修改时间,也避开路径相关的权限语义(也就是文件的访问由其自己的权限控制,而不禁它上层路径上的目录控制)。 为使缓存文件元数据更容易,系统不公开最后访问时间。
其名字空间包含文件和目录,统一叫作节点(nodes)。每一个这样的节点在其所在的Chubby单元内只有一个名字,没有符号连接和硬连接。
节点多是永久的或者瞬时(ephemeral)的。任意节点均可以被显示地(explicitly)删除,可是瞬时节点也会在没有客户端打开它时被删除(另外,对目录而言,在它们为空时被删除)。瞬时文件被用做临时文件,也被用做一个客户端是否存活的指示器(给其余客户端)。任意节点都能做为一个意向性(advisory)的读写锁;这些锁将在2.4节更详细地描述。
每一个节点有多种元数据,包括访问控制列表(ACLs)的三个名字,分别用于控制读、写和修改其ACL。除非被覆盖,节点在建立时继承父目录的ACL名字。ACLs自己是位于一个ACL目录中的文件, 这个ACL目录是Chubby单元的一个为人熟知的本地名字空间。这些ACL文件的内容由简单的访问名字(principals)列表组成;这里读者可能会想起Plan 9的 groups[21]。这样,若是文件F的写ACL名字是foo,而且ACL目录中包含了一个名foo的文件,foo文件中包含bar这个条目,那么用户bar就能够写文件F。用户由内嵌在RPC系统里的机制鉴权。由于Chubby的ACLs是日常的文件,它们自动地就能够由其余想使用相似的访问控制机制的服务访问。
每一个节点的元数据包括四个单调递增的64位编号。这些编号容许客户端很容易地检测变化:
实例编号:大于任意先前的同名节点的实例编号。
内容的世代编号(只针对文件):这个编号在文件内容被写入时增长。
锁的世代编号:这个编号在节点的锁由自由(free)转换到持有(held)时增长。
ACL的世代编号:这个编号在节点的ACL名字被写入时增长。
Chubby也放出了一个64位的文件内容的校验码,因此客户端能够分辨文件是否有变化。
客户端经过打开节点获得相似于UNIX文件描述符的句柄(handles)。句柄包括:
校验位:阻止客户端自行建立或猜想句柄,因此完整的访问控制检查只须要在句柄建立时执行(对比UNIX,UNIX在打开时检查权限位但在每次读写时不检查,由于文件描述符不能伪造)。
一个序列号:这个序列号容许master分辨一个句柄是由它或前面的master生成。
模式信息:在句柄打开时设定的是否容许新master在碰见一个由前面的master建立的旧句柄时重建该句柄的状态。
每一个Chubby文件和目录都能做为一个读写锁:要么一个客户端句柄以排他(写者)模式持有这个锁,要么任意数目的客户端句柄以共享(读者)模式持有这个锁。像大部分程序员熟知的互斥器(mutexes),锁是意向性的(advisory)。就是只与对同一个锁的加锁请求冲突:持有锁F既不是访问文件F的必要条件,也不会阻止其余客户端访问文件F。咱们舍弃强制锁—强制锁使得其余没有持有锁的客户端不能访问被锁定的对象:
Chubby锁常常保护由其余服务实现的资源,而不只仅是与锁关联的文件。以一种有意义的方式执行强制锁定可能须要咱们对这些服务作更普遍的修改。
咱们不想强迫用户在他们为了调试和管理而访问这些锁定的文件时关闭应用程序。在一个复杂的系统中,这种在我的电脑上经常使用的方法是很难用的。我的电脑上的管理员见能够经过指示用户关闭应用或者重启来停止强制锁定。
咱们的开发者用常规的方式来执行错误检测,例如“lock X is held”,因此他们从强制检查中受益不多。有Bug或者恶意的进程有不少机会在没有持有锁时破坏数据,因此咱们咱们发现强制锁定提供的额外保护没有实际价值。
在Chubby中,请求任意模式的锁都须要写权限,于是一个无权限的读者不能阻止一个写者的操做。
在分布式系统中,锁定是很复杂的,由于通讯常常发生变化(communication is typically uncertain),而且进程可能各自独立地失败(fail independently)。像这样,一个持有锁 L 的进程可能发起请求 R ,但接着就失败了。另外一个进程可能请求 L 并在 R 到达目的地以前执行了某些操做。若是 R 来晚了,它可能在没有锁 L 的保护下操做,也有可能在不一致的数据上操做。接收消息顺序紊乱的问题已经被研究得很完全:解决方案包括虚拟时间(virtual time)[11]和虚拟同步(virtual synchrony)[1],它经过确保与每一个参与者的观察一致的顺序处理消息来避免这个问题。
在一个已有的复杂系统中给全部的交互(interaction)引入顺序编号(sequence number)的成本很高。做为替代,Chubby经过只在使用锁的交互(interaction)中引入序号提供了一种方法。任什么时候候,锁持有者可能请求一个序号,一个不透明(opaque)的描述了锁刚得到时的状态的字节串(byte-string)。它包括锁的名字,被请求的锁定模式(独占仍是共享),以及锁的世代编号。客户端在其但愿操做将被锁保护时传递这个序号给服务器(例如文件服务器)。接受服务器被预期将测试这个序号是否仍然有效而且具备适当的锁定模式,若是它将拒绝该请求。序号的有效性可与服务器的Chubby缓存核对,或者若是服务器不想维护一个到Chubby的Session的话,可与服务器最近观察到的序号比对。这种序号机制只须要给受影响的消息增长一条字符串,而且很容易地解释给咱们的开发者。
虽然咱们发现序号使用简单,但重要的协议也在缓慢演化。所以Chubby提供了一种不完美可是更容易的机制来下降延迟的或者重排序的到不支持序号的服务器的风险。若是一个客户端以一般的方式释放了一个锁,像人们期待的那样,这个锁当即可供其余的客户端索取。然而,若是一个锁是由于持有者中止工做或者不可访问,锁服务器将阻止其余的客户端在一个被称做锁延时(lock-delay)的小于某个上界的时间段内索取该锁。客户端可能指定任意的低于某个上界(当前是一分钟)的锁延时,这个限制防止有毛病的客户端形成一个锁(和像这样的某些资源)在一个随意长的时间里不可用。 尽管不完美,锁延时保护未修改过的服务器和客户端免受消息延时和重启致使的平常问题的影响。
在建立句柄时,Chubby客户端可能会订阅一系列事件。这些事件经过Chubby库的向上调用(up-call)异步地传递给客户端。这些事件包括:
文件内容被修改–经常使用于监视经过文件公布的某个服务的位置信息(location)。
子节点的增长、删除和修改 — 用于实现镜像(mirroring)(第2.12节)。(除了容许新文件可被发现外,返回子节点上的事件使得能够监控临时文件(ephemeral files)而不影响这些临时文件的引用计算(reference counts))
Chubby master故障恢复 — 警告客户端其余的事件可能已经丢失了,因此必须从新检查数据。
一个句柄(和它的锁)失效了 — 这常常意味着某个通信问题。
锁被请求了(lock required) — 可用于判断何时primary选出来了。
来自另外一个客户端的相冲突的锁请求 — 容许锁的缓存。
事件在相应的动做发生以后被递送。这样,若是一个客户端被告知文件内容发生了变化,必定能保证(it’s guaranteed) 接下来它读这个文件会看到新的数据(或者比该事件还要更近的数据)。
上面提到的最后两种事件极少使用,过后思考它们能够略去(and with hindsight could have been omitted)。例如在primary选举(primary election)后,客户端一般须要与新的primary联系,而不是简单地知道一个新的primary存在了;于是,它们会等待primary将地址写入文件的文件修改事件。锁冲突事件在理论上容许客户端缓存其余服务器持有的数据,使用Chubby锁来维护缓存的一致性。一个冲突的锁请求将会告诉客户端结束使用与锁相关的数据;它将结束等待进行的操做,将修改刷新到原来的位置(home location),丢弃缓存数据,而后释放锁。到目前为止,没有人采纳这种用法。
客户端将Chubby句柄看做一个指向一个支持多种操做的不透明结构的指针。句柄之经过open()建立,以及经过close()销毁。
Open() 打开一个带名字的文件或目录产生一个句柄,相似于一个UNIX文件描述符。只有这个调用须要一个节点名,全部其余的调用都在句柄上操做。
这个节点名是相对于某个已知的目录句柄的,客户端库提供了一个一直有效的“/”目录句柄。Directory handles avoid the difficulties of using a program-widecurrent directory in a multi-threaded program that contains many layers of abstraction. (目录句柄避开了在包含不少层抽象的多线程程序内使用程序范围内的当前目录的困难[18]。)
在调用Open()时,客户端指定了多个选项:
句柄将被如何使用(读;写和锁定;改变ACL);句柄只在客户端拥有合适的权限时建立。
须要传递的事件(查看第2.5节)。
锁延时(第2.4节)。
是否应该(或者必须)建立一个新的文件或目录。若是要建立一个文件,调用者可能要提供初始内容和初始的ACL名字。其返回值代表这个文件其实是否已经建立。
Close() 关闭一个打开的句柄。之后再也不容许使用这个句柄。这个调用永远不会失败。一个相近的调用Poison()引发该句柄上未完成的和接下来的操做失败,但不关闭它,这就容许一个客户端取消由其余线程建立的Chubby调用而不须要担忧被它们访问的内存的释放。
在句柄上进行的主要调用包括:
GetContentsAndStat() 返回文件的内容和元数据。一个文件的内容被原子地、完整地读取。咱们避开部分读取和写入以阻碍大文件。一个相关的调用GetStat()只返回元数据,而ReadDir()返回一个目录下的名字和元数据(the names and meta-data for the children of a directory)。
SetContents()将内容写到文件。可选择地,客户端可能提供一个内容世代编号以容许客户端模拟在文件上的比较并交换:只有在世代编号是当前值时内容才被改变。文件的内容老是完整地、原子地写入。一个相关的调用SetACL()在节点关联的ACL名字上执行一个相似的操做。
Delete() 删除一节节点,若是它没有孩子的话。
Accquire(), TryAccquire(), Release() 得到和释放锁。
GetSequencer() 返回一个描述这个句柄持有的锁的序号(sequencer)(见第2.4节)。
SetSequencer() 将一个序号与句柄关联。在这个句柄上的随后的操做将失败若是这个序号再也不有效的话。
CheckSequencer() 检查一个序号是否有效。(见第2.4节)
若是节点在该句柄建立以后被删除,其上的调用将会失败,即便这个文件随后又从新建立了也是如此。也就是一个句柄关联到一个文件的实例,而不是文件名。Chubby可能在任意的调用上使用访问控制,可是老是检查Open() 调用。
除了调用自身须要的参数以外, 上面的全部调用都带有一个操做(operation)参数。这个操做参数持有可能与任何调用相关的数据和控制信息。特别地,经过操做的参数客户端可能:
提供一个回调(callback)以使调用以异步方式执行。
等待调用的结束,和/或
取得展开的错误和诊断信息。
客户端能够利用这个API执行primary选举:全部潜在的primaries打开锁文件,并尝试得到锁。其中一个成功得到锁,并成为primary,而其余的则做为副本。这个Primary将它的标识用SetContents()写入到锁文件,在对文件修改事件的响应中,它被客户端和副本们发现,它们用GetContentsAndStat()读取锁文件。理想状况下,Primary经过GetSequencer()取得一个序号,而后将序号传递给与它通讯的其余服务器:这些服务器应该用CheckSequencer()确认它仍然是primary。一个锁延时可能与不能检查序号(第2.4节)的服务一块儿使用。
为了减小读传输量,Chubby客户端将文件数据和节点元数据(包括文件缺失信息(file absence)) 缓存在内存中的一个一致的(consistent)、write-through的缓存中。这个缓存由一个如后文描述的租期机制(lease mechanism)维护,并经过由master发送的过时信号(invalidations)来维护一致性。Master保留着每一个客户端可能正在缓存的数据的列表。这个协议保证客户端要么看到一致的Chubby状态,要么看到错误。
当文件数据或者元数据将被修改时,修改操做被阻塞,同时master发送过时信号给全部可能缓存了这些数据的客户端。这种机制创建在下一节要详细讨论的KeepAlive RPC之上。在接收到过时信号后,客户端清除过时的状态信息,并经过发起它的下一个KeepAlive调用应答服务器。修改操做只在服务器知道每一个客户端都将这些缓存失效之后再继续,要么由于客户端答谢了过时信号,要么由于客户端让它的缓存租期(cache lease)过时。
只有一次过时来回(round)是必需的,由于master在缓存过时信号没有答谢期间将这个节点视为不可缓存的(uncachable)。这种方式让读操做老是被无延时地获得处理;这是颇有用的,由于读操做的数量要大大超过了写操做。另外一种选择多是在过时操做期间阻塞对该节点的访问;这将使得过分急切的客户端在过时操做期间接二连三地以无缓存的访问轰炸master的可能性大大下降,其代价是偶然的延迟。 若是这成为问题,人们可能会想采用一种混合方案,在检测到过载时切换处理策略(tactics)。
缓存协议很简单:它在修改时将缓存的数据失效,而永不去更新这些数据。有可能去更新缓存而不是让缓存失效也会同样简单,可是只更新的协议可能会无理由地低效;某个访问文件的客户端可能无限期地收到更新,从而致使次数无限制的、彻底没必要要的更新。
尽管提供严格一致性的开销不小,咱们拒绝了更弱的模型,由于咱们以为程序员们将发现它们很难用。相似地,像虚拟同步(virtual synchrony)这种要求客户端在全部的消息中交换序号的机制,在一个有多种已经存在的通讯协议的环境中也被认为是不合适的。
除了缓存数据和元数据,Chubby客户端还缓存打开的句柄。于是,若是一个客户端打开一个前面已经打开的文件,只有第一次Open()调用时会引发一个给Master的RPC。这种缓存被限制在较低层次(in a minor ways),因此它不会影响客户端观察到的语义:临时文件上的句柄在被应用程序关闭后,不能再保留在打开状态;而允许锁定的句柄则可被重用,可是不能由多个应用程序句柄并发使用。最后的这个限制是由于客户端可能利用Close()或者Poison()的边际效应:取消正在进行的向Master请求的Accquire()调用。
Chubby的协议容许客户端缓存锁 — 也就是,怀着这个锁会被同一个客户端从新使用的但愿,持有锁比实际须要更长的时间。若是另外一个客户端请求了一个冲突的锁,能够用一个事件告知锁持有者,这容许锁持有者只在别的地方须要这个锁时才释放锁(参考§2.5)。
一个Chubby会话是Chubby单元和Chubby客户端之间的一种关系,它存在于么某个时间间隔内,并由称作KeepAlive的间歇性地握手来维持。除非一个Chubby客户端通知master,不然,只要它的会话依然有效,该客户端的句柄、锁和缓存的数据都仍然有效。(然而,会话维持的协议可能要求客户端答谢一个缓存过时信号以维持它的会话,请看下文)
一个客户端在第一次联系一个Chubby单元的master时请求一个新的会话。它要么在它结束时明确地结束会话,或者在会话陷入空转时(在一段时间内没有任何打开的句柄和调用)结束会话。
每一个会话都有关联了一个租期(lease) — 一个延伸向将来的时间间隔,在这个时间间隔内master保证不单方面停止会话。间隔的结束被称做租期到期时间(lease timeout)。Master能够自由地向将来延长租期到期时间,可是不能将它往回移动。
在三种情形下,Master延长租期到期时间:会话建立时、master故障恢复(见下文)发生时和它应答来自客户端的KeepAlive RPC时。在接收KeepAlive时,master一般阻塞这个RPC请求(不容许其返回),直到客户端的上前一个租期接近过时。而后,Master容许RPC返回给客户端,并告知客户端新的租期过时时间。Master可能任意长度地延伸超时时间。默认的延伸是12秒,但一个过载的master可能使用更高的值,以下降它必须处理的KeepAlive调用数目。客户端在收到上一个回复之后当即发起一个新的KeepAlive,这样客户端保证几乎老是有一个KeepAlive调用阻塞在master上。
除了延伸客户端的租期,KeepAlive的回复还被用于将事件和缓存过时传回给客户端。Master容许一个KeepAlive在有事件或者缓存过时须要递送时提早返回。在KeepAlive的回复上搭载事件,保证了客户端不该答缓存过时则不能维持会话,并使得全部的RPC都是从客户端流向master。这样既简化了客户端,也使得协议能够经过只容许单向发起链接的防火墙。
客户端维持着一个本地租期超时,这个本地超时值是Master的租期的较小的近似值。它跟master的租期过时不同,是由于客户端必须在两方面作保守的假设。一是KeepAlive花在传输上的时间,一是master的时钟超前的度。为了维护一致性,咱们要求服务器的时钟频率相对于客户端的时钟频率,不会快于某个常数量(constant factor)。
若是一个客户端的本地租期过时了,它就变得不能肯定master是否终结了它的会话。因而这个客户端清空并禁用本身的缓存,咱们称它的会话处于危险(jeopardy)中。客户端在等待一个以后的被称做宽限期的间隔,默认是45秒。若是这个客户端和master在宽限期结束以前设法完成了一个成功的KeepAlive,客户端就再开启它的缓存。不然,客户端假定会话已通过期。这样作了,因此Chubby的API调用不会在Chubby单元变成不可访问时无限期阻塞,若是在通信从新创建以前宽限期结束了,调用返回一个错误。
Chubby库可以经过一个”危险”(jeopardy)事件在宽限期开始时通知应用程序。当知道会话在通信问题后幸存下来,一个”安全”(safe)事件告知应用程序继续工做;而若是会话时间过去了,一个”过时”(expire)事件被发送给应用程序。这些通知容许应用程序在对它的会话状态不肯定时停下来,而且在问题被证实是瞬时的时不须要重启进行恢复。在启动开销很高的服务中,这在避免服务不可用方面多是很重要的。
若是一个客户端持有某个节点上的句柄 H,而且任意一个H上的操做都由于关联的会话过时了而失败,那么全部接下来的H上的操做(除了Close()和Poison()) 将以一样的方式失败。客户端能够用这去保证网络和服务器不可用时会致使随后的一串操做丢失了,而不是一个随机的操做子序列丢失,这样就容许复杂的修改能够以其最后一次写标记为已提交。
当一个master失效了或者失去了master身份时,它丢弃内存中有关会话、句柄和锁的信息。有权力的(authoritative)会话租期计时器开始在master上运行。这样,直到一个新的master被选举出来,租期计时器才中止;这是合法的由于它等价于延长客户端的租期。若是一个master选举很快发生,客户端能够在它们本地(近似的)租期计时器过时以前与新的master联系。若是选举用了很长时间,客户端刷空它们的缓存并等待一个宽限期(grace period),同时尝试寻找新的master。以这种方式,宽限期(grace period)使得会话在跨越超出正常租期的故障恢复期间被维持。
图2展现了一个漫长的故障恢复事件中的事件序列,在这个恢复事件中客户端必须使用它的宽限期来保留它的会话。时间从左到右延伸,可是时间是不能够往回缩的。客户端会话租期显示为粗箭头,它既被新的、老的Master看到(M1-3,上方),也被客户端都能看到(C1-3,下方)。倾斜向上的箭头标示KeepAlive请求,倾斜向上的箭头标示这些KeepAlive请求的应答。原来的Master有一个给客户端的租期M1,而客户端有一个(对租期的)保守的估计C1。在经过KeepAlive应答2通知客户端以前,原Master允诺了一个新的租期M2;客户端可以延长它的视线为租期C2。原Master在应答下一个KeepAlive以前死掉了,过了一段时间后另外一个master被选出。最终客户端的租期估计值(C2)过时了。而后客户端清空它的缓存,并为宽限期启动一个计时器。
在这段时间里,客户端不能肯定它的租期在master上是否已通过期了。它没有销毁它的会话,可是它阻塞全部应用程序对它的API的调用,以防止应用程序观测到不一致的数据。在宽限期开始时,Chubby的库发送一个危险事件给应用程序,容许它停下来直到会话状态可以肯定。最终一个新的master选举完成了。Master最初使用对它的前任可能持有的给客户端的租期的保守估算值M3。新Master的来自客户端的第一个KeepAlive请求(4)被拒绝了,由于它的master代数不正确(下文有详细描述)。重试请求(6)成功了,可是一般不去延长master租期,由于M3是保守的。而后它的回复(7)容许客户端延再一次长它的租期(C3),还能够选择通知应用程序其会话再也不处于危险状态。由于宽限期足够长,能够覆盖从租期C2的结束到租期C3的开始之间的间隔,客户端除了观测到延迟不会看到别的。但假如宽限期比这个间隔短,客户端将丢弃会话,并将失败报告给应用程序。
一旦客户端与新的master联系上,客户端库和master协做提供给应用程序一种没有故障发生过的假象。为达到此目的,新的master必须从新构造一个前面的master拥有的内存状态的保守估算(conservative approximation)。它经过读取存储在硬盘上的数据(经过常规的数据库复制协议复制)来完成一部分,经过从客户端获取状态完成一部分,经过保守的假设(conservative assumptions)完成一部分。数据库记录了每一个会话、被持有的锁和临时文件。
一个新选出来的master继续处理:
1. 它先选择一个新的代编号(epoch number),这个编号在客户端每次请求时都要求出示。Master拒绝来自使用较早的代编号的客户端的请求,并提供新的代编号给它们。这保证了新的master将不会响应一个很早的发给前一个master的包,即便此前的这个Master与如今的master在同一台机器上。
2. 新的Master可能会响应对master的位置的请求(master-location requests),可是不会当即开始处理传入的会话相关的操做。
3. 它为记录在数据库中的会话和锁构建内存数据结构。会话租期被延长至上一个master可能当时正使用的最大期限。
4. Master如今容许客户端进行KeepAlive,但不能执行其余会话相关的操做。
5. It emits a fail-over event to each session; this causes clients to flush their caches (because they may have missed invalidations), and to warn applications that other events may have been lost.
5. 它发出一个故障切换事件给每一个会话;这引发客户端刷新它们的缓存(由于这些缓存可能错过了过时信号(invalidations)),并警告应用程序别的事件可能已经丢失。
6. Master一直等待,直到每一个会话应答了故障切换事件或者使其会话过时。
7. Master容许全部的操做继续处理。
8. 若是一个客户端使用一个在故障切换以前建立的句柄(可依据句柄中的序号来判断),master从新建立了这个句柄的内存印象(in-memory representation),并执行调用。若是一个这样的重建后的句柄被关闭后,master在内存中记录它,这样它就不能在这个master代(epoch)中再次建立;这保证了一个延迟的或者重复的网络包不能偶然地重建一个已经关闭的句柄。一个有问题的客户端能在将来的master代中重建一个已关闭的句柄,但假若该客户端已经有问题的话,则这样不会有什么危害。
9. 在通过某个间隔(如,一分钟)后,master删除没有已打开的文件句柄的临时文件。在故障切换后,客户端应该在这个间隔期间刷新它们在临时文件上的句柄。这种机制有一个不幸的效果是,若是该文件的最后一个客户端在故障切换期间丢失了会话的话,临时文件可能不会很快消失。
读者将不意外地得知,远远不像系统的其余部分那样被频繁使用的故障切换代码,是一些有趣的bug的丰富来源。
初版的Chubby使用带复制的Berkeley DB版本[20]做为它的数据库。Berkeley DB提供了映射字节串的键到任意的字节串值上B-树。咱们设置了一个按照路径名称中的节数排序的键比较函数,这样就容许节点用它们的路径名做为键,同时保证兄弟节点在排序顺序中相邻。因为Chubby不使用基于路径的权限,数据库中的一次查找就能够知足每次文件访问。
Berkeley DB使用一种分布式一致协议将它的数据库日志复制到一组服务器上。只要加上master租期,就吻合Chubby的设计,这使得实现很简单。相比于在Berkeley DB的B-树代码被普遍使用而且很成熟,其复制相关的代码是最近增长的,而且用户较少。维护者必须优先维护和改进他们的最受欢迎的产品特性。
在Berkeley DB的维护者解决咱们遇到的问题期间,咱们以为使用这些复制相关的代码将咱们暴露在比咱们愿负担的更多的风险中。结果,咱们写了一个简单的,相似于Birrell et al.[2]的设计的,利用了日志预写和快照的数据库。数据库日志仍是想之前同样利用一致性协议在多个副本之间分布。Chubby至少了Berkeley DB的不多的特性,这样从新可以使整个系统大幅简化;例如,咱们须要原子操做,可是咱们确实不须要通用的事务。
每隔几个小时,Chubby单元的mster将它的数据库快照写到另外一栋楼里的GFS文件服务器上 [7]。使用另外一栋楼(的文件服务器)既确保了备份会在楼宇损毁中保存下来,也确保备份不会在系统中引入循环依赖,同一栋楼中的GFS单元潜在地可能依赖于Chubby单元来选举它的master。 备份既提供了灾难恢复,也提供了一种初始化一个新建立的副本的数据库的途径,而不会将初始化的压力放在其余正在提供服务的副本上。
Chubby容许一组文件(a collection of files)从一个单元镜像到另一个单元。镜像是很快的,由于文件很小,而且事件机制(参见第2.5节)会在文件增长、删除或修改时当即通知镜像处理相关的代码。在没有网络问题的前提下,变化会一秒以内便在世界范围内的不少个镜像中反映出来。若是一个镜像不可到达,它保持不变直到链接恢复。而后经过比较校验码识别出更新了的文件。
镜像被最多见地用于将配置文件复制到各类各样的遍及全世界的计算集群。一个叫global的特殊的Chubby单元,含有一个子树 /ls/global/master,这个子树被镜像到每一个其余的Chubby单元的子树 /ls/cell/slave。这个Global单元很特别,由于它的五个副本分散在全球相隔很远的多个地区,因此几乎老是可以从大部分国家/地区访问到它。
在这些从global单元镜像的文件中,有Chubby本身的访问控制列表,多种含有Chubby单元和其余系统向咱们的监控服务广告它们的存在的文件,容许客户端定位巨大的数据集如BigTable单元的指针信息(的文件),以及许多其余系统的配置文件。
由于Chubby的客户端是独立的进程,因此Chubby必须处理比人们预想的更多的客户端;咱们曾经看见过90,000个客户端直接与一个Chubby服务器通信 — 这远大于涉及到的机器总数。由于在一个单元之中有一个master,并且它的机器跟那些客户端是同样的,因而客户端们能以巨大的优点(margin)将master战胜。所以,最有效的扩展技术减小与master的通讯经过一个重大的因数(Thus, the most effective scaling techniques reduce communication with the master by a significant factor)。假设master没有严重的性能缺陷,master上的请求处理中的较小改进只有很小的(have little)效果。咱们使用了下面几种方法:
咱们能够建立任意数量的Chubby单元;客户端几乎老是使用一个邻近的单元(经过DNS找出)以免对远程机器的依赖。咱们的有表明性的发布让一个有几千台机器的数据中心使用一个Chubby单元。
Master可能会在负载很重时将租期(lease times)从默认的12秒到延长到最大60秒左右,这样它就只须要处理更少的KeepAlive RPC调用。(KeepAlive是到目前为止的占统治地位的请求类型(参考第4.1节),而且未能及时处理它们是一个超负荷的服务器的表明性的失败模式;客户端对其余调用中的延迟变化至关地不敏感。)
Chubby客户端缓存文件数据,元数据,文件缺失(the absence of files)和打开的句柄,以减小它们在服务器上作的调用。
咱们使用转换Chubby协议为不那么复杂协议如DNS等的协议转换服务器。咱们后文描述其中的一些。
这里咱们描绘两种熟悉的机制,代理(proxies)和分区(partitioning),这两种机制将容许Chubby扩展更多。咱们如今尚未在产品环境中使用它们,可是它们已经被设计出来了,而且可能很快被使用。咱们尚未显示出考虑扩展到五倍之外的须要:第一,在一个数据中心想要放的或者依赖于单个实例服务的机器的数目是有限制的;第二,由于咱们为Chubby的客户端和服务器使用相似配置的机器,硬件上的提高增长了每台机器上的客户端的数量时,同时也增长了每台服务器的容量。
Chubby的协议能够由可信的进程代理(在两边都使用相同的协议),这个进程未来自其余客户端的请求传递给Chubby单元。一个代理能够经过处理KeepAlive和读请求减小服务器负载,它不能减小从代理的缓存中穿过的写入流量。但即便有积极的客户端缓存,写入流量仍远小于Chubby正常负载的百分之一(参见第4.1节),这样代理容许大幅提高客户端的数量。若是一个代理处理Nproxy个客户端,KeepAlive流量将减小Nproxy倍,而Nproxy多是一万甚至更大。A proxy cache can reduce read traffic by at most the mean amount of read-sharing–a factor of around 10 (参见第4.1节)。但由于读只占当前的Chubby负载的10%如下,KeepAlive流量上的节省仍然是到目前为止更重要的成效。
代理增长了一个额外的RPC用于写和第一次读。人们可能会预期代理会使得Chubby单元的临时不可用次数至少是之前的两倍,由于每一个代理后的客户端依赖于两台可能失效的机器:它的代理和Chubby的Master。
机敏的读者会注意到2.9节描述的故障切换,对于代理机制不太理想。咱们将的第4.4节讨论这个问题。
像第2.3节提到的,Chubby的接口被选择为Chubby单元的名字空间能在服务器之间划分。虽然咱们还不须要它,可是如今的代码(code)可以经过目录划分名字空间。 若是开启划分,一个Chubby单元将由 N 个分区 组成,每一个分区都有一组副本(replicas)和一个master。每一个在目录 D 中的节点 P(D/C) 将被保存在分区 P(D/C)= hash(D) mod N 上。注意 D 上的元数据可能存储在另外一个不一样的分区 P(D) = hash(D’) mod N 上, D’ 是 D 的父目录。
分区被计划为以很小的分区之间的通讯来启用很大的Chubby单元集(Chubby cells)。尽管Chubby没有硬连接(hard links),目录修改时间,和跨目录的重命名操做,一小部分操做仍然须要跨分区通讯:
ACL 自己是文件,因此一个分区可能会使用另外一个分区作权限校验。 而后, ACL 文件能够很快捷地缓存;只有Open() 和 Delete() 操做须要作 ACL 核查;而且大部分客户端读公开可访问的、不须要ACL的文件。
当一个目录被删除时,一个跨分区的调用可能被须要以确保这个目录是空的。
由于每一个分区独立地处理大部分调用,咱们预期这种通讯只会对性能和可用性形成不太大的影响。
除非分区数目 N 很大,咱们预期每一个客户端将与大部分分区联系。 所以,分区将任意一个分区上的读写通信量(traffic)下降为原来的N分之一,可是不会下降 KeepAlive 的通讯量。若是Chubby须要处理更多客户端的话,咱们的应对方案将会联合使用代理和分区。
下面的表给出了某个Chubby单元的快照的统计信息;其中的RPC频率是从一个10分钟的时间段里计算出来的。这些数字在Google的Chubby单元中是很常见的。
从表中能够看到下面几点:
许多文件被用于命名(naming),参见第$4.3节。
参数配置,访问控制和元数据文件(相似于文件系统的super-blocks)很广泛。
Negative Caching很突出。
平均来看,每一个缓存文件由230k/24k≈10个客户端使用。
较少的客户端持有锁,共享锁不多;这与锁定被用于primary选举和在多个副本之间划分数据的预期是相符合的。
RPC流量主要是会话 KeepAlive,有少许的读(缓存未命中引发),只有不多的写或锁请求。
如今咱们简要描述写咱们的Chubby单元不可用的缘由。若是咱们假定(乐观地)若是一个单元有一个master愿意服务则是”在线的(up)”,在咱们的Chubby的采样中,在数周内咱们记录下合计61次不可用,合计共有700单元-天(??)。咱们排除了维护引发的关闭数据中心时的不可用。全部其余的不可用的缘由包括:网络拥塞,维护,超负荷和运营人员引发的错误,软件,硬件。大部分不可用在15s之内或者更短,另外52次在30秒之内;咱们的大部分应用程序不会被Chubby的30秒内的不可用显著地影响到。剩下的就此不可用由这些缘由引发:网络维护(4),可疑的网络链接问题(2),软件错误(2),超负荷(1)。
在好几打Chubby单元年(cell-years)的运行中,咱们有六次丢失了数据,由数据库软件错误(4)和运营人员错误(2)引发;与硬件错误没有关系。具备讽刺意味的是,操做错误与为避免软件错误的升级有关。咱们有两次纠正了由非master的副本的软件引发的损坏。
Chubby的数据都在内存中,因此大部分操做的代价都很低。咱们生产服务器的中位请求延时始终保持在一毫秒之内(a small fraction of a millisecond),无论Chubby单元的负载如何,直到Chubby单元超出负荷,此时延迟大幅增长,会话掉线。超负荷一般发生在许多(> 90,000)的会话激活时,可是也能由异常条件引发:当客户端们同时地发起几百万读请求时(在4.3节有描述),或者当客户端库的一个错误禁用了某些读的缓存,致使每秒成千上万的请求。由于大部分RPC是KeepAlive,服务器能够经过延长会话租期(参考第三章)跟许多客户端维持一个急哦较低的中位请求延时。Group commit reduces the effective work done per request when bursts of writes arrive,可是这不多见。
客户端观测到的RPC读延迟受限于RPC系统和网络;对一个本地Chubby单元而言在1毫秒如下,但跨洲则须要250毫秒。RPC写(包括锁操做)被数据库的日志更新进一步地延迟了5-10毫秒,可是若是一个最近刚失败过的客户端要缓存该文件的话,最高能够被延迟几十秒。即便这种写延迟的变化程度对服务器上的中位请求延迟也只有很小的影响,由于写至关地罕见。
若是会话没有丢弃的话,Chubby是至关不受延迟变化的影响的。在一个时间点上(At one point),咱们在Open()中增长了人为的延时以抑制***性的客户端(参考第4.5节);开发人员只会在延时超过十秒而且反复地被延时时会注意到。咱们发现扩展Chubby的关键不是服务器的性能;下降到服务器端的通信能够有远远大得多的做用。没有重要的努力用在调优读/写相关的服务器代码路径上;咱们检查了没有极坏的缺陷(bug)存在,而后集中在更有效的扩展机制上。在另外一方面,若是一个性能缺陷影响到客户端会每秒读几千次的本地Chubby缓存,开发者确定会注意到。
Google的基础架构设施大部分是用C++写的,可是一些正在不断增长中的系统正在用Java编写 [8]。这种趋势呈现出Chubby的一个预料以外的问题,而Chubby有一个复杂的客户端协议和不那么简单的(non-trival)客户端库。
Java经过让它有些不厌其烦地与其余语言链接,以渐进采用的损耗,来鼓励整个程序的移植性。一般的用于访问非原生(non-native)的库的机制是JNI [15],但JNI被认为很慢而且很笨重。咱们的Java程序员们是如此不喜欢JNI以致于为了不使用JNI而倾向于将很大的库转换为Java,并维护它们。
Chubby的客户端库有7000行代码(跟服务器差很少),而且客户端协议很精细。去维持这样一个用Java写的库需当心和代价,同时一个没有缓存的实现将埋葬Chubby服务器。所以,咱们的Java用户跑着多份协议转换服务器,这些服务器暴露一个很相似于Chubby客户端API的简单的RPC协议。即便过后回想,怎么样去避免编写、运行并维护这些额外的服务器仍然是不那么明显的。
尽管Chubby被设计为一种锁服务,咱们发现它的最流行的应用是作为名字服务器。
在常规的因特网命名系统 — DNS中,缓存是基于时间的。DNS条目有一个生存时间(TTL),DNS数据在这个时间内没有获得刷新的话,就将被丢弃。一般很容易选择一个合适的TTL值,可是当但愿对失败的服务作快速替换时,这个TTL会很小,以致于使DNS服务器超载。
例如,很常见地,咱们的开发者运行有数千个进程的任务,且其中的每一个进程之间都要通讯,这引发了二次方级的DNS查询。咱们可能但愿使用一个60秒的TTL,这将容许行为不正常的客户端没有过长的延迟就被换掉,同时也在咱们的环境中不被认为是一个过长的替换时间。在这种状况下,为维持某一个小到3000个客户端的任务的DNS缓存,可能须要每秒15万次查找。(做为比照,一个2-CPU 2.6GHz Xeon DNS服务器么秒可能能处理5万请求)。更大的任务产生更恶劣的问题,而且多个任务可能同时执行。在引入Chubby之前,DNS负载的波动对Google来时曾经是的一个严重问题。
相反,Chubby的缓存使用显示地失效(invalidations),所以在没有修改时,一个恒定频率的会话KeepAlive请求能无限期地维护一个客户端上的任意数目的缓存条目。 一个2-CPU 2.6GHz Xeon的Chubby Master 曾被见到过处理9万个直接与它通信(没有Proxy)的客户端;这些客户端包括了具备上文描述过的那种通信模式的庞大任务。不须要轮询每一个名字的提供快速名字更新的能力是如此吸引人,以致于如今Google的大部分系统都由Chubby提供名字服务。
尽管Chubby的缓存容许一个单独的单元能承受大量的客户端,负载峰值仍多是一个问题。当咱们第一次发布基于Chubby的名字服务时,启动了一个3千进程的任务(其结果是产生了9百万个请求)可能将Chubby的Master压垮。为了解决这个问题,咱们选择将名字条目组成批次,于是一次查询将返回同一个任务内的大量的(一般是100个)相关进程的名字映射,并缓存起来。
Chubby提供的缓存语义要比名字服务须要的缓存语义更加精确;名字解析只要求按期的通知,而非彻底的一致性。于是,这里有一个时机,能够经过引入特别地为名字查找设计的简单协议转换服务器,来下降Chubby的负载。假如咱们预见到将Chubby用做名字服务的用法,咱们可能选择实现完整的代理,而不是提供这种简单但没有必要的额外的服务器。
有一种更进一步的协议转换服务器存在:Chubby DNS服务器。这中服务器将存储在Chubby里的命名数据提供给DNS客户端。这种服务器很重要,它既能减小了DNS名字到Chubby名字之间的转换,也能适应已经存在的不能轻易转换的应用程序,例如浏览器。
原来的master故障切换(§2.9)的设计要求master在新的会话建立时将新会话写入数据库。在Berkeley DB版本的锁服务器中,在许多进程同时启动时建立会话的开销成为问题。为了不超载,服务器被修改成不在会话建立时保存会话信息到数据库,而是在会话的第一次修改、锁请求或者打开临时文件时写入数据库中。此外,活动会话在每次KeepAlive时以某种几率被记录到数据库中。这样,只读的会话信息的写入在时间上分散开来。
尽管为了不超载,这种优化是必要的,但它有一种负面效应:年轻的只读的会话可能没有被记录在数据库中,于是在故障切换发生时可能被丢弃。虽然这种会话不持有锁,这也是不安全的;若是全部已记录的会话在被丢弃的会话的租期到期以前签入到新的master中,被丢弃的会话可能在一段时间内读到脏数据。这在实际中不多见,可是在大型系统中,几乎能够确定某些会话将签入失败,于是迫使新的master必须等待最大租期时间。尽管这样,咱们修改了故障切换设计,既避免这个效应,也避开当前这种模式带给代理的复杂性。
在新的设计下,咱们彻底避免在数据库中记录会话,而是以目前master重建句柄的方式重建它们(参见§2.9,8)。一个新的master如今必须等待一个完整的最坏状况下的租期过时,才能容许操做继续处理。由于它不能得知是否全部的会话都签入(check in)了(参见§2.9,6)。再者,这在实际中只有很小的影响,由于极可能不是全部的会话都会签入。
一旦会话能在没有在磁盘上的状态信息重建,代理服务器就能管理master不知情的会话。一个额外的只提供给代理的操做容许它们修改与锁有关联的会话。这容许在在代理失败时,一个代理可从另外一个代理取得一个客户端。Master上增长的惟一必要的修改是保证不放弃与代理会话关联的锁或者临时文件句柄,直到一个新的代理有机会得到它们。
Google的项目团队可自由地设置它们本身的Chubby单元,但这样作增强了他们的维护负担,并且消耗了额外的硬件资源。许多服务所以使用共享的Chubby单元,这使得隔离行为不正常的客户端变得很重要。Chubby是本是计划用于在单个公司内部运行的,所以针对它的恶意的服务拒绝袭击是很罕见的。然而,错误、误解和开发者不同的预期都致使了相似于袭击的效应。
咱们的某些解决方式是很粗暴的。例如,咱们检查项目团队计划使用Chubby的方式,并在检查令人满意以前拒绝其访问共享的Chubby名字空间。这种方式的一个问题是开发者常常不能预计他们的服务未来会被如何使用,以及使用量会怎么样增加。
咱们的检查的最重要的方面是判断任何的Chubby的资源(RPC频率,硬盘空间,文件数目)是否会随着该项目的用户数量或处理的数据量线性(或者更坏)地增加。任何线性增加必须被由一个修正参数来调低,这个修正参数能够调整为下降Chubby上负载到一个合理边界的值。尽管如此,咱们的早期检查仍然不完全足够。
一个相关的问题是大部分软件文档中缺乏性能建议。一个由某个团队编写的模块,可能在一年后被另外一个团队复用,并带来极糟糕的后果。有时很难向接口设计者解释他们必须修改他们的接口,不是由于它们很差,而是由于其余开发者可能较少知道RPC的代价。
下面咱们列出了咱们遇到过的一些问题。
缺乏可应付冲击的缓存(aggressive caching) 最初,咱们没有意识到缓存不存在的文件和重用打开的文件句柄的关键性的须要。 尽管在培训时进行了尝试,咱们的开发人员一般在一个文件不存在时,仍会编写无限重试的循环,或者经过重复地关闭/打开一个文件来轮询文件(实际上只需打开一次就能够)。
最初,咱们对付这些重试循环的方法是,在某个应用程序短期内作了许多Open()同一个文件的尝试时,引入指数级递增的延时。在某些情形下,这暴露了开发者接受了的漏洞,可是一般这须要咱们花更多的时间作培训。最后,让重复性的Open()调用廉价会更容易一些。
缺乏限额 Chubby从没有被计划用作存放大量数据的存储系统,所以没有存储限额。过后反思,这是很弱智的。Google的某个项目写了一个跟踪数据上传的模块,存储了某些元数据到Chubby中。这种上传不多发生,而且只限于一小群人,所以使用的空间是有上限的。然而,两个其余服务开始使用该模块做为追踪来自于多得多的用户群的上传的手段。不可避免地,他们的服务一直增加,直到对Chubby的使用到达极限:一个1.5M字节的文件在每次用户动做时被整个重写,被这些服务使用的空间超出了其余全部Chubby客户端加起来的空间需求。
咱们在文件大小上引入了一个限制(256KB),并推进这些服务迁移到更合适的存储系统上去。但要在这些由很繁忙的人们维护着的生产系统上要取得重大变化是很困难的,—-花了大约一年将这些数据迁移到其余地方。
发布/订阅 曾有数次将Chubby的事件机制做为Zephyr[6]式的发布/订阅系统的尝试。Chubby的超重量级的保证和它在维护缓存一致性时使用过时(invalidation)而不是更新,使得它除了无心义的订阅/发布示例以外,都很慢、效率很低。幸运地是,全部这样的应用都在从新设计应用程序的成本变得太大之前被叫停(caught)。
这里咱们列出教训和多种若是有机会的话咱们可能会作出的设计变动:
开发者极少考虑可用性。 咱们发现开发者极少考虑失败的可能性,并倾向于认为像Chubby这样的服务好像会用于可用。例如,开发者曾经构建了一个用了几百台机器的系统,这个系统在Chubby选举出一个新的Master时就发起一个持续几十分钟的恢复处理过程。这将一个单一故障的后果放大到受影响的机器数与时间的乘积的倍数。咱们偏好开发者为短期的Chubby不可用作好计划,以便这样的事件对他们的应用只有不多影响,甚至没有影响。这是粗粒度锁定的一个有争议的地方,在第2.1节讨论了这个问题。
开发者一样未能成功地意识到服务上线和服务对他们的应用可用的之间的区别。例如,全局Chubby单元(参见 $2.12)基本上老是在线的,由于不多会有两个物理距离遥远的数据中心同时下线。而后,对于某个客户端而言,它被观测到的可用性一般低于这个客户端的观测到的本地Chubby单元的可用性。首先,本地单元较少可能与客户端之间发生网络断开,另外,尽管本地Chubby单元可能会由于维护操做而下线,但一样的维护操做也会直接影响到客户端,所以Chubby的不可用就不会被客户端看到。
咱们的API选择一样能影响到开发者处理Chubby不可用的方式。例如,Chubby提供了一个时间容许客户端检测何时发生了master的故障切换。原本这是用于客户端检查可能的变化的,由于别的时间可能丢失了。不幸的是,许多开发选择在收到这个事件是关闭他们的程序,所以大幅下降了他们系统的可用性。咱们原本有可能经过给“文件改变(file-change)”事件以作得更好,甚或能够保证在故障切换期间不丢失事件。
目前咱们用了三种机制防止开发者对Chubby的可用性过度乐观,特别是对全局单元(Global cell) 的可用性过度乐观。首先,如前文提到的那样,咱们复查各个项目团队打算如如何使用Chubby,并建议它们不要使用可能将它们的可用性与Chubby的可用性绑定的太紧密。第二,如今咱们提供了执行某些高层次任务的库,所以开发者被自动地与Chubby中断隔离。第三,咱们利用每次Chubby中断的过后分析做为一种手段,不只清除Chubby和运维过程当中的缺陷(bugs),还下降应用程序对Chubby的可用性的敏感性 — 两方面都带来系统的总体上更好的可用性。
差劲的API选择致使了不可预料的后果 对于大部分而言,咱们的API演化得很好,可是有一个错误很突出。咱们的取消长期运行(long-running)的调用是Close()和Poison() RPC,它们也会丢弃服务器的分配给对应句柄的状态 。这阻止了能请求锁的句柄被 共享,例如,被多个线程共享。咱们可能会添加一个 Cancel() RPC以容许更多打开的句柄的共享。
RPC的使用影响了传输协议 KeepAlive被用于刷新客户端的会话租期,也被用于从master端传递事件和缓存过时到客户端。这个设计有自动的符合预期的效应:一个客户端不能在没有应答缓存过时的状况下刷新会话状态。
这彷佛很理想,除了这样会在传输协议的选择中引入紧张情形之外。TCP的拥塞时回退(back off)策略不关心更高层的超时,例如Chubby的租期,所以基于TCP的KeepAlive在发生高网络拥塞时致使许多丢失的会话。咱们被迫经过UDP而不是TCP发送KeepAlive RPC调用;UDP没有拥塞回避机制,所以咱们只有当高层的时间界限必须知足时才使用UDP协议。
咱们可能增长一个基于TCP的GetEvent() RPC来加强传输协议,这个RPC将用于在正常情形下传递事件和过时,它与KeepAlives的方式相同。KeepAlive回复仍将包含一个未应答事件列表,于是事件最终都将被应答。
Chubby是基于长期稳定的思想(well-established ideas)之上的。Chubby的缓存设计源自于分布式文件系统相关的成果[10]。它的会话和缓存标记(tokens)在行为上与Echo的相应部分相似[17];会话下降租期压力与V 系统(V System)相似。放出一个通常性的(general-purpose)锁服务的理念最先出如今VMS[23]中,尽管该系统最初使用了一个特殊目的的许可低延迟交互的高速联网系统。 像它的缓存模型,Chubby的API是创建在文件系统模型上的,其中包括相似于文件系统的名字空间要比单纯的文件要方便不少的思想。 [18, 21, 22] (原文:Like its caching model, Chubby’s API is based on a file-system model, including the idea that a filesystem-like name space is convenient for more than just files [18, 21, 22].)
Chubby区别于一个像Echo或者AFS[10]这样的文件系统,主要表如今在它的性能和存储意图上:客户端不读、写或存储大量数据,而且他们不期待很高的吞吐量,甚至若是数据不缓存的话也不预期低延时。它们却期待一致性,可用性和可靠性,可是这些属性在性能不那么重要是比较容易达成。由于Chubby的数据库很小,咱们能在线存储它的多个拷贝(一般是五个副本和少数备份)。咱们天天作不少次完整备份,并经过数据库状态的校验码(checksums),咱们每隔几个小时互相比较副本。对常规文件系统的性能和存储需求的弱化(weakening)容许咱们用一个Chubby master服务数千个客户端。咱们经过提供一个中心点,许多客户端在这个中心点共享信息和协同活动,解决了一类咱们的系统开发人员面对的问题。
各类文献(literature)描述了大量的文件系统和锁服务器,因此不可能作一个完全的比较。因而咱们选择了其中一个作详细比较:咱们选择与Boxwood的锁服务16比较,因它是最近设计的,而且设计为运行在松耦合环境下,另外它的设计在多个方面与Chubby不一样,某些颇有趣,某些是本质上的。(原文:some interesting and some incidental)
Chubby 实现了锁,一个可靠的小文件存储系统,以及在单个服务中的会话/租期机制。相反,Boxwood将这些划分为三部分:锁服务,Paxos服务(一个可靠的状态存储库),以及对应的失败检测服务。 Boxwood系统它自己使用了这三个组件,可是另外一个系统可能独立地使用这些构建块。咱们怀疑设计上的这个区别源自不一样的目标客户群体(target audience)。Chubby被计划用于多种多样的目标客户(audience) 和应用的混合体, 它的用户有建立新分布式系统的专家,也有编写管理脚本的新手。对于咱们的环境,一个提供熟知的API的大规模(large-scale)的共享服务彷佛更有吸引力。相反,Boxwood提供了一个工具包(至少咱们看到的是这样),这个工具包适合于一小部分经验丰富、老练的开发者用在那些可能共享代码但不须要一块儿使用的项目中。
In many cases, Chubby provides a higher-level interface than Boxwood. For example, Chubby combines the lock and file names spaces, while Boxwood’s lock names are simple byte sequences. Chubby clients cache file state by default; a client of Boxwood’s Paxos service could implement caching via the lock service, but would probably use the caching provided by Boxwood itself.
The two systems have markedly different default parameters, chosen for different expectations: Each Boxwood failure detector is contacted by each client every 200ms with a timeout of 1s; Chubby’s default lease time is 12s and KeepAlives are exchanged every 7s. Boxwood’s subcomponents use two or three replicas to achieve availability, while we typically use five replicas per cell. However, these choices alone do not suggest a deep design difference, but rather an indication of how parameters in such systems must be adjusted to accommodate more client machines, or the uncertainties of racks shared with other projects.
A more interesting difference is the introduction of Chubby’s grace period, which Boxwood lacks. (Recall
that the grace period allows clients to ride out long Chubby master outages without losing sessions or locks.
Boxwood’s “grace period” is the equivalent of Chubby’s “session lease”, a different concept.) Again, this difference is the result of differing expectations about scale and failure probability in the two systems. Although master fail-overs are rare, a lost Chubby lock is expensive for clients.
Finally, locks in the two systems are intended for different purposes. Chubby locks are heavier-weight, and
need sequencers to allow externals resources to be protected safely, while Boxwood locks are lighter-weight,
and intended primarily for use within Boxwood.
Chubby is a distributed lock service intended for coarsegrained synchronization of activities within Google’s
distributed systems; it has found wider use as a name service and repository for configuration information.
Its design is based on well-known ideas that have meshed well: distributed consensus among a few replicas
for fault tolerance, consistent client-side caching to reduce server load while retaining simple semantics, timely notification of updates, and a familiar file system interface. We use caching, protocol-conversion servers, and simple load adaptation to allow it scale to tens of thousands of client processes per Chubby instance. We expect to scale it further via proxies and partitioning.
Chubby has become Google’s primary internal name service; it is a common rendezvous mechanism for systems
such as MapReduce [4]; the storage systems GFS and Bigtable use Chubby to elect a primary from redundant
replicas; and it is a standard repository for files that require high availability, such as access control lists.
Many contributed to the Chubby system: Sharon Perl wrote the replication layer on Berkeley DB; Tushar
Chandra and Robert Griesemer wrote the replicated database that replaced Berkeley DB; Ramsey Haddad
connected the API to Google’s file system interface; Dave Presotto, Sean Owen, Doug Zongker and Praveen
Tamara wrote the Chubby DNS, Java, and naming protocol-converters, and the full Chubby proxy respectively; Vadim Furman added the caching of open handles and file-absence; Rob Pike, Sean Quinlan and Sanjay Ghemawat gave valuable design advice; and many Google developers uncovered early weaknesses.
[1] BIRMAN, K. P., AND JOSEPH, T. A. Exploiting virtual synchrony in distributed systems. In 11th SOSP (1987), pp. 123–138.
[2] BIRRELL, A., JONES, M. B., AND WOBBER, E. A simple and efficient implementation for small databases. In 11th SOSP (1987), pp. 149–154.
[3] CHANG, F., DEAN, J., GHEMAWAT, S., HSIEH, W. C., WALLACH, D. A., BURROWS, M., CHANDRA, T., FIKES, A., AND GRUBER, R. Bigtable: A distributed structured data storage system. In 7th OSDI (2006).
[4] DEAN, J., AND GHEMAWAT, S. MapReduce: Simplified data processing on large clusters. In 6th OSDI 2004), pp. 137–150.
[5] FISCHER, M. J., LYNCH, N. A., AND PATERSON, M. S. Impossibility of distributed consensus with one faulty process. J. ACM 32, 2 (April 1985), 374–382.
[6] FRENCH, R. S., AND KOHL, J. T. The Zephyr Programmer’s Manual. MIT Project Athena, Apr. 1989.
[7] GHEMAWAT, S., GOBIOFF, H., AND LEUNG, S.-T. The Google file system. In 19th SOSP (Dec. 2003), pp. 29–43.
[8] GOSLING, J., JOY, B., STEELE, G., AND BRACHA, G. Java Language Spec. (2nd Ed.). Addison-Wesley, 2000.
[9] GRAY, C. G., AND CHERITON, D. R. Leases: An efficient fault-tolerant mechanism for distributed file cache
consistency. In 12th SOSP (1989), pp. 202–210.
[10] HOWARD, J., KAZAR, M., MENEES, S., NICHOLS, D., SATYANARAYANAN, M., SIDEBOTHAM, R., AND WEST, M. Scale and performance in a distributed file system. ACM TOCS 6, 1 (Feb. 1988), 51–81.
[11] JEFFERSON, D. Virtual time. ACM TOPLAS, 3 (1985), 404–425.
[12] LAMPORT, L. The part-time parliament. ACM TOCS 16, 2 (1998), 133–169.
[13] LAMPORT, L. Paxos made simple. ACM SIGACT News 32, 4 (2001), 18–25.
[14] LAMPSON, B. W. How to build a highly available system using consensus. In Distributed Algorithms, vol. 1151 of LNCS. Springer–Verlag, 1996, pp. 1–17.
[15] LIANG, S. Java Native Interface: Programmer’s Guide and Reference. Addison-Wesley, 1999.
[16] MACCORMICK, J., MURPHY, N., NAJORK, M., THEKKATH, C. A., AND ZHOU, L. Boxwood: Abstractions as the foundation for storage infrastructure. In 6th OSDI (2004), pp. 105–120.
[17] MANN, T., BIRRELL, A., HISGEN, A., JERIAN, C., AND SWART, G. A coherent distributed file cache with directory write-behind. TOCS 12, 2 (1994), 123–164.
[18] MCJONES, P., AND SWART, G. Evolving the UNIX system interface to support multithreaded programs. Tech. Rep. 21, DEC SRC, 1987.
[19] OKI, B., AND LISKOV, B. Viewstamped replication: A general primary copy method to support highly-available distributed systems. In ACM PODC (1988).
[20] OLSON, M. A., BOSTIC, K., AND SELTZER, M. Berkeley DB. In USENIX (June 1999), pp. 183–192.
[21] PIKE, R., PRESOTTO, D. L., DORWARD, S., FLANDRENA, B., THOMPSON, K., TRICKEY, H., AND WINTERBOTTOM, P. Plan 9 from Bell Labs. Computing Systems 8, 2 (1995), 221–254.
[22] RITCHIE, D. M., AND THOMPSON, K. The UNIX timesharing system. CACM 17, 7 (1974), 365–375.
[23] SNAMAN, JR., W. E., AND THIEL, D. W. The VAX/VMS distributed lock manager. Digital Technical Journal 1, 5 (Sept. 1987), 29–44.
[24] YIN, J., MARTIN, J.-P., VENKATARAMANI, A., ALVISI, L., AND DAHLIN, M. Separating agreement from execution for byzantine fault tolerant services. In 19th SOSP (2003), pp. 253–267.
======================
cell Chubby 单元Chubby cell Chubby 单元session 会话bug 缺陷antipodes 洲际section $4.1 [P11]:but 250msbetweenantipodes.lock 锁locking 锁定