谷歌的项目团队可以自由地建立自己的Chubby单元,但这样做会增加他们的维护负担,并消耗额外的硬件资源。因此,许多服务使用共享的Chubby单元,这使得隔离客户与其他客户的不当行为变得非常重要。Chubby的目的是在一个单一的公司内运作,因此针对它的恶意拒绝服务攻击是罕见的。然而,错误、误解和我们开发人员的不同期望导致了与攻击相似的效果。
我们的一些补救措施是严厉的。例如,我们审查项目团队计划使用Chubby的方式,并拒绝访问共享的Chubby命名空间,直到审查满意为止。这种方法的一个问题是,开发人员往往无法预测他们的服务在未来将如何被使用,以及使用将如何增长。读者会注意到,我们自己也没能预测Chubby本身会被如何使用,这是一种讽刺。
我们审查的最重要方面是阻止Chubby的任何资源(RPC速率、磁盘空间、文件数量)的使用是否随着用户数量或项目处理的数据量而线性增长(或更糟)。任何线性增长都必须通过一个补偿参数来缓解,该参数可以被调整以将Chubby的负载降低到合理的范围内。尽管如此,我们早期的审查还不够彻底。
一个相关的问题是大多数软件文档中缺乏性能建议。一个团队编写的模块可能在一年后被另一个团队重新使用,结果是灾难性的。有时很难向接口设计者解释,他们必须改变他们的接口,不是因为它们不好,而是因为其他开发者可能不太了解RPC的成本。
下面我们列出一些我们遇到的问题案例。
缺乏积极的缓存 最初,我们没有意识到对文件的缺失进行缓存或者重新使用开放的文件柄的关键需求。尽管我们试图进行教育,但我们的开发人员经常写一些循环,在文件不存在时无限期地重试,或者通过反复打开和关闭文件来轮询,而人们可能期望他们只打开一次文件。
起初,我们通过引入指数级增加的延迟来对付这些重试循环,当一个应用程序在短时间内多次尝试打开()同一个文件时。在某些情况下,这暴露了开发人员所承认的错误,但往往需要我们花更多的时间来教育。最后,让重复的Open()调用变得更便宜。
缺少配额 Chubby从未打算被用作大量数据的存储系统,所以它没有存储配额。事后看来,这是很幼稚的。
谷歌的一个项目写了一个模块来跟踪数据的上传,将一些元数据存储在Chubby中。这种上传很少发生,而且只限于一小部分人,所以空间是有限制的。然而,另外两项服务开始使用相同的模块,作为跟踪更多用户上传数据的手段。不可避免地,这些服务越来越多,直到Chubby的使用达到了极致:一个1.5MByte的文件在每个用户的操作上都被全部改写,而该服务使用的总体空间超过了所有其他Chubby客户端的空间需求之和。
我们引入了对文件大小的限制(256kBytes),并鼓励这些服务迁移到更合适的存储系统。但是,对由忙碌的人维护的生产系统进行重大改变是很困难的--大约花了一年的时间,数据才被迁移到其他地方。
发布/订阅 已经有一些人尝试将Chubby的事件机制作为Zephyr[6]风格的发布/订阅系统。Chubby的重量级保证和它在维护缓存一致性时使用的无效化而不是更新,使得它除了最微不足道的发布/订阅例子外,其他都是缓慢而低效的。幸运的是,在重新设计应用程序的成本过大之前,所有这样的使用都被抓住了。
在这里,我们列出了教训,以及如果有机会的话,我们可能会做出的各种设计改变。
开发人员很少考虑可用性 我们发现,我们的开发人员很少考虑失败的概率,他们倾向于把Chubby这样的服务当作是永远可用的。例如,我们的开发者曾经建立了一个拥有上百台机器的系统,当Chubby选出一个新的主人时,启动恢复程序需要数十分钟。这使得单一故障的后果在时间和受影响机器的数量上都放大了一百倍。我们更希望开发者能够为Chubby的短暂中断做计划,这样一来,这样的事件对他们的应用程序几乎没有影响。这就是第2.1节中讨论的粗粒度锁的论据之一。
开发者们也没有意识到一个服务的启动和该服务对他们的应用程序的可用性之间的区别。例如,全局Chubby单元(见第2.12节),几乎总是在线的,因为很少有两个以上的地理距离的数据中心同时停机。然而,对于一个给定的客户,它的观察可用性通常低于客户的本地Chubby单元的观察可用性。首先,本地单元不太可能从客户那里被分割出来,其次,虽然本地单元可能因为维护而经常停机,但同样的维护会直接影响到客户,所以Chubby的不可用性不会被客户观察到。
我们的API选择也会影响开发者选择处理Chubby中断的方式。例如,Chubby提供了一个事件,允许客户端检测何时发生了主站故障。本意是让客户检查可能的变化,因为其他事件可能已经丢失。不幸的是,许多开发者在收到这个事件时选择了崩溃他们的应用程序,从而大大降低了他们系统的可用性。我们可能会做得更好,以发送冗余的 "文件更改 "事件来代替,或者甚至确保在故障切换期间没有事件丢失。
目前,我们使用三种机制来防止开发者对Chubby的可用性过于乐观,特别是全局单元的可用性。首先,如前所述(第4.5节),我们审查项目组计划如何使用Chubby,并建议他们不要使用会将他们的可用性与Chubby的可用性绑得太紧的技术。第二,我们现在提供了执行一些高级任务的库,这样,开发人员就可以自动地与Chubby的故障隔离开来。第三,我们把每次Chubby故障的事后总结作为一种手段,不仅可以消除Chubby和我们的操作程序中的错误,而且可以降低应用程序对Chubby可用性的敏感性--两者都可以使我们的系统总体上有更好的可用性。
细粒度的锁可以被忽略 在第2.1节的末尾,我们勾画了一个服务器的设计,客户可以运行它来提供细粒度的锁。也许令人惊讶的是,到目前为止,我们还不需要编写这样的服务器;我们的开发人员通常发现,为了优化他们的应用程序,他们必须删除不必要的通信,而这往往意味着找到一种方法来使用粗粒度的锁定。
差劲的API选择会带来意想不到的影响 在大多数情况下,我们的API发展得很好,但有一个错误很突出。我们取消长期运行的调用的方法是Close()和Poison() RPC,它也抛弃了句柄的服务器状态。这可以防止可以获得锁的句柄被共享,例如被多个线程共享。我们可能会添加一个Cancel() RPC来允许更多的共享开放的句柄。
RPC的使用影响了传输协议 KeepAlives既用于刷新客户端的会话租约,也用于将事件和缓存失效从主站传递给客户端。这种设计有一个自动的和理想的结果,即客户端不能在不确认缓存无效的情况下刷新其会话租约。
这似乎很理想,除了它在我们选择的协议中引入了一个缺陷。TCP的回退策略没有注意到更高层次的超时,如Chubby租约,所以基于TCP的KeepAlive在网络高度拥堵时导致许多会话丢失。我们被迫通过UDP而不是TCP来发送KeepAlive RPC;UDP没有避免拥堵的机制,所以我们宁愿只在必须满足高级别的时间界限时才使用UDP。
我们可以用一个额外的基于TCP的GetEvent()RPC来增强协议,该RPC将用于在正常情况下沟通事件和失效,使用方式与KeepAlives相同。KeepAlive回复仍将包含一个未确认的事件列表,因此事件最终必须被确认。
Chubby是基于成熟的理念。Chubby的缓存设计来自于分布式文件系统的工作[10]。它的会话和缓存令牌与Echo[17]中的行为相似;会话减少了V系统中租赁[9]的开销。在VMS[23]中发现了暴露通用锁服务的想法,尽管该系统最初使用了一个特殊用途的高速互连,允许低延迟的交互。就像它的缓存模型一样,Chubby的API是基于文件系统模型的,包括类似文件系统的命名空间不仅仅是方便文件的想法[18, 21, 22]。
Chubby与分布式文件系统如Echo或AFS[10]在性能和存储方面的诉求不同。客户端不读、不写、不存储大量的数据,他们不期望高吞吐量,甚至不期望低延迟,除非数据被缓存。他们确实期望一致性、可用性和可靠性,但当性能不那么重要时,这些属性更容易实现。因为Chubby的数据库很小,我们能够在线存储它的许多副本(通常是五个副本和几个备份)。我们每天进行多次完整的备份,并通过数据库状态的校验,每隔几小时对副本进行比较。对正常文件系统性能和存储要求的削弱,使我们能够从一个Chubby主站为数以万计的客户提供服务。通过提供一个中心点,许多客户可以共享信息和协调活动,我们解决了系统开发者面临的一类问题。
文献中描述的大量文件系统和锁服务器使我们无法进行详尽的比较,因此我们提供了一个细节:我们选择与Boxwood的锁服务器[16]进行比较,因为它是最近设计的,它也被设计为在一个松散耦合的环境中运行,然而它的设计与Chubby有不同的地方,有些是有趣的,有些是偶然的。
Chubby在一个服务中实现了锁、一个可靠的小文件存储系统和一个会话/租赁机制。相比之下,Boxwood将这些分离成三个:一个锁服务,一个Paxos服务(一个可靠的状态存储库),以及一个故障检测服务。Boxwood系统本身将这三个组件放在一起使用,但另一个系统可以独立使用这些构件。我们怀疑这种设计上的差异是源于目标受众的不同。Chubby是为不同的受众和应用组合而设计的;它的用户包括创建新的分布式系统的专家,以及编写管理脚本的新手。对于我们的环境来说,一个具有熟悉的API的大规模共享服务似乎很有吸引力。相比之下,Boxwood提供的工具包(至少在我们看来)适合于数量较少的更复杂的开发人员,他们工作的项目可能共享代码,但不需要一起使用。
在许多情况下,Chubby提供了一个比Boxwood更高层次的界面。例如,Chubby结合了锁和文件名空间,而Boxwood的锁名是简单的字节序列。Chubby客户端默认会缓存文件状态;Boxwood的Paxos服务的客户端可以通过锁服务实现缓存,但可能会使用Boxwood本身提供的缓存。这两个系统有明显不同的默认参数,为不同的期望而选择。每个Boxwood故障检测器每200ms与每个客户端联系一次,超时1s;Chubby的默认租赁时间是12s,KeepAlives每7s交换一次。Boxwood的子组件使用两个或三个副本来实现可用性,而我们通常每个单元使用五个副本。然而,这些选择本身并不表明深层次的设计差异,而是表明在这种系统中必须调整参数以适应更多的客户机,或与其他项目共享机架的不确定性。
一个更有趣的区别是引入了Chubby的宽限期,而Boxwood却没有。(回顾一下,宽限期允许客户在不丢失会话或锁的情况下度过漫长的Chubby主站停运期。Boxwood的 "宽限期 "相当于Chubby的 "会话租赁",是一个不同的概念)。) 同样,这种差异是两个系统中对规模和故障概率的不同期望的结果。虽然主系统的故障率很低,但丢失的Chubby锁对客户来说是很昂贵的。
最后,这两个系统中的锁有不同的目的。Chubby锁的重量较重,需要序列器来使外部资源得到安全保护,而Boxwood锁的重量较轻,主要用于Boxwood内部。
Chubby是一个分布式锁服务,旨在为谷歌分布式系统内的活动提供粗粒度的同步;它已被发现作为一个名称服务和配置信息的存储库而被广泛使用。
它的设计是基于众所周知的想法,这些想法已经很好地融合在一起:在几个副本之间的分布式共识以实现容错,一致的客户端缓存以减少服务器负载,同时保留简单的语义,及时通知更新,以及熟悉的文件系统界面。我们使用缓存、协议转换服务器和简单的负载适应,使其能够扩展到每个Chubby实例的数万个客户进程。我们希望通过代理和分区来进一步扩展它。
Chubby已经成为谷歌的主要内部名称服务;它是MapReduce[4]等系统的常见交会机制;存储系统GFS和Bigtable使用Chubby从冗余的副本中选出一个主副本;它是需要高可用性的文件的标准存储库,如访问控制列表。
许多人对Chubby系统做出了贡献。Sharon Perl在Berkeley DB上编写了复制层;Tushar Chandra和Robert Griesemer编写了取代Berkeley DB的复制数据库;Ramsey Haddad将API连接到Google的文件系统接口。Dave Presotto、Sean Owen、Doug Zongker和Praveen Tamara分别编写了Chubby的DNS、Java和命名协议转换器,以及完整的Chubby代理;Vadim Furman增加了开放句柄和文件缺失的缓存;Rob Pike、Sean Quinlan和San jay Ghemawat提供了宝贵的设计建议;许多Google开发人员发现了早期的弱点。