下表给出了作为Chubby单元的快照的统计数据;RPC率是在10分钟内看到的。这些数字是Google中的典型单元。
可以看到几件事:
现在我们简要地描述一下我们单元中的典型故障原因。如果我们假设(乐观地),如果一个单元有一个愿意服务的主站,那么它就是 “在线"的,在我们的一个单元样本中,我们在几周内记录了61次中断,总共有700个单元日的数据。我们排除了因维护而关闭数据中心的故障。所有其他原因都包括在内:网络拥堵、维护、过载,以及由于运营商、软件和硬件造成的错误。大多数中断时间为15s或更少,52个中断时间在30s以下;我们的大多数应用没有受到30s以下的Chubby中断的显著影响。其余9次中断是由网络维护(4次)、疑似网络连接问题(2次)、软件错误(2次)和过载(1次)引起的。
在几十个单元年的运行中,我们有六次数据丢失,原因是数据库软件错误(4次)和操作错误(2次);没有一次涉及硬件错误。具有讽刺意味的是,操作错误涉及升级以避免软件错误。我们两次纠正了由软件在非主副本中造成的损坏。
Chubby的数据存储在RAM中,所以大多数操作都很便宜。在我们的生产服务器上,无论单元负载如何,平均请求延迟始终是一小部分毫秒,直到单元接近过载时,延迟急剧增加,会话被放弃。过载通常发生在许多会话(>90,000)活跃的时候,但也可能是由特殊情况造成的:当客户同时提出数百万的读取请求时(在第4.3节中描述),以及当客户端库中的一个错误禁用了一些读取的缓存,导致每秒数万次的请求。因为大多数RPC都是KeepAlives,服务器可以通过增加会话租赁期(见第3节),在许多活跃的客户端中保持较低的平均请求延迟。当突发的写入到达时,群组提交减少了每个请求的有效工作,但这是很少的。
在客户端测量的RPC读取延迟受到RPC系统和网络的限制;对于本地单元来说,它们低于1ms,但在反节点之间则是250ms。写入(包括锁操作)因数据库日志更新而进一步延迟5-10毫秒,但如果最近失败的客户端缓存了该文件,则延迟可达数十秒。即使写延迟的这种变化对服务器的平均请求延迟也没有什么影响,因为写的频率很低。
客户端对延迟的变化相当不敏感,只要会话不被放弃。有一次,我们在Open()中加入了人为的延迟,以遏制滥用的客户端(见第4.5节);只有当延迟超过10秒并且反复应用时,开发者才会注意到。我们发现,扩展Chubby的关键不是服务器的性能;减少与服务器的通信可以产生更大的影响。我们没有在调整读/写服务器代码路径方面做出重大努力;我们检查了没有令人震惊的错误存在,然后把重点放在可以更有效的扩展机制上。另一方面,如果一个性能错误影响到本地的Chubby缓存,开发人员确实会注意到,客户端可能每秒会读取数千次。
Google的基础设施大多是C++语言,但越来越多的系统是用Java编写的[8]。这种趋势给Chubby带来了一个意想不到的问题,它有一个复杂的客户端协议和一个非琐碎的客户端库。
Java鼓励整个应用程序的可移植性,但却牺牲了渐进式的采用,因为它使与其他语言的链接变得有些烦躁。通常Java访问非本地库的机制是JNI[15],但它被认为是缓慢和麻烦的。我们的Java程序员非常不喜欢JNI,为了避免使用它,他们宁愿把大型库翻译成Java,并承诺支持它们。
Chubby的C++客户端库有7000行(与服务器相当),而且客户端协议很微妙。在Java中维护这个库需要谨慎和额外开销,而没有缓存的实现会给Chubby服务器带来负担。因此,我们的Java用户运行一个协议转换服务器的副本,该服务器输出一个简单的RPC协议,与Chubby的客户端API紧密对应。即使事后看来,我们也不清楚如何避免编写、运行和维护这个额外的服务器的成本。
尽管Chubby被设计成一个锁服务,我们发现它最受欢迎的用途是作为一个名称服务器。在正常的互联网命名系统(DNS)中,缓存是基于时间的。DNS条目有一个生存时间(TTL),当DNS数据在这段时间内没有被刷新时就会被丢弃。通常情况下,选择一个合适的TTL值是很简单的,但如果需要及时更换失败的服务,TTL可以变得足够小,从而使DNS服务器过载。例如,对于我们的开发人员来说,运行涉及数千个进程的工作是很常见的,而且每个进程都要与其他进程进行通信,这就导致了四倍数量的DNS查询。我们可能希望使用60s的TTL;这将允许在没有过度延迟的情况下替换有问题的客户端,并且在我们的环境中不被认为是不合理的短替换时间。在这种情况下,要维护一个小到3000个客户端的DNS缓存,每秒需要15万次查询。(作为比较,一个2-CPU 2.6GHz的Xeon DNS服务器可能每秒处理5万个请求) 更大的任务会产生更糟糕的问题,而且有很多任务是同时进行的。在Chubby问世之前,我们的DNS负载的可变性已经成为谷歌的一个严重问题。
相比之下,Chubby的缓存使用显式无效,所以在没有变化的情况下,一个恒定的KeepAlive请求率可以在客户端无限期地保持任意数量的缓存条目。一个2-CPU 2.6GHz的Xeon Chubby主站已经可以处理9万个与它直接通信的客户(没有代理);这些客户包括具有上述通信模式的大型工作。Chubby能够提供快速的名称更新,而不需要对每个名称进行单独轮询,这种能力非常吸引人,现在Chubby为公司的大多数系统提供名称服务。
尽管Chubby的缓存允许一个单元维持大量的客户端,但负载高峰仍然是一个问题。当我们第一次部署基于Chubby的名称服务时,启动一个3000个进程的任务(从而产生900万个请求)可以使Chubby主站陷入困境。为了解决这个问题,我们选择将名字条目分组,以便一次查询可以返回并缓存一个任务中大量(通常是100个)相关进程的名字映射。
Chubby提供的缓存语义比名称服务所需要的更精确;名称解析只需要及时通知而不是完全一致。因此,有机会通过引入一个专门为名称查询设计的简单协议转换服务器来减少Chubby的负载。如果我们预见到Chubby作为一个名字服务的使用,我们可能会选择比我们之前所做更早地实现完全代理,以避免对这个简单的、但却额外的服务器的要求。
还有一个协议转换服务器存在:Chubby DNS服务器。这使得存储在Chubby中的命名数据对DNS客户可用。这台服务器很重要,既能缓解从DNS名称到Chubby名称的过渡,又能适应不能轻易转换的现有应用,如浏览器。
主站故障转移的原始设计(§2.9)要求主站在创建新会话时将其写入数据库中。在Berkeley DB版本的锁服务器中,当许多进程同时启动时,创建会话的开销成为一个问题。为了避免过载,服务器被修改为当会话尝试首次修改、获取锁或打开一个短暂文件时在数据库中存储会话,而不是在其首次创建时。此外,在每次KeepAlive时,活跃的会话会以一定的概率被记录在数据库中。因此,只读会话的写入在时间上是分散的。
虽然有必要避免过载,但这种优化有一个不理想的效果,即新的只读会话可能不会被记录在数据库中,因此在发生故障时可能被丢弃。虽然这种会话没有锁,但这是不安全的;如果所有记录的会话在被丢弃的会话的租约到期之前与新的主站签到,那么被丢弃的会话就会在一段时间内读取陈旧的数据。这种情况在实践中很少见;在一个大系统中,几乎可以肯定的是,一些会话将无法签入,从而迫使新的主站等待最大的租赁时间。尽管如此,我们还是修改了故障转移的设计,既是为了避免这种影响,也是为了避免目前的方案给代理带来的复杂情况。
在新的设计中,我们完全避免在数据库中记录会话,而是以主站目前重新创建句柄的相同方式重新创建会话(§2.9,§8)。一个新的主站现在必须等待一个完整的最坏情况下的租赁时间,然后才允许操作继续进行,因为它不能知道所有的会话是否已经签入(§2.9,§6)。同样,这在实践中也没有什么影响,因为很可能不是所有的会话都会签到。
一旦会话可以在没有存盘状态的情况下被重新创建,代理服务器就可以管理主站所不知道的会话。一个仅对代理服务器可用的额外操作允许他们改变锁所关联的会话。这允许一个代理服务器在一个代理失败时从另一个代理接管一个客户。主站唯一需要的进一步改变是保证不放弃与代理会话相关的锁或短暂的文件处理,直到新的代理有机会索取它们。