2.7 Caching
为了减少读取流量,Chubby客户端将文件数据和节点元数据(包括文件缺失)缓存在内存中的一个一致的、可写入的缓存中。缓存由下面描述的租赁机制来维护,并由主服务器发送的无效信息来保持一致,主服务器保持着每个客户端可能缓存的内容的列表。该协议确保客户端看到的是Chubby状态的一致视图,或者是一个报错。
当文件数据或元数据被改变时,修改会被阻止,同时主服务器端会向每个可能已经缓存的客户端发送数据的无效信息;这个机制基于KeepAlive RPCs,在下一节会有更全面的讨论。在收到无效信息时,客户端会刷新无效状态,并通过下一次KeepAlive调用来确认。只有在服务器知道每个客户端都已经使其缓存失效后,才会进行修改,要么因为客户端确认了失效,要么是因为客户端允许其缓存租约过期。
失效确认只需要一轮,因为当缓存失效仍未被确认时,主服务器端会将节点视为不可缓存。这种方法允许读总是被无延迟地处理;这很有用,因为读的数量大大超过写的。另一种方法是在验证期间阻止访问节点的调用;这将使过于急切的客户端在失效期间用未缓存的访问轰炸主服务器的可能性降低,但代价是偶尔的延迟。如果这是个问题,我们可以想象采用一种混合方案,在检测到过载时转换策略。
缓存协议很简单:它在变化时使缓存数据失效,并且永远不会更新它。更新而不是失效也同样简单,但只更新协议可能是相当低效的;一个访问文件的客户端可能会无限期地接收更新,导致不必要的更新数量无节制。
尽管提供严格的一致性有一定的开销,我们还是放弃了较弱的模型,因为我们觉得程序员会发现它们更难使用。同样,像虚拟同步这样要求客户在所有消息中交换序列号的机制在一个有各种预先存在通信协议的环境中被认为是不合适的。
除了缓存数据和元数据外,Chubby客户端还缓存了打开的句柄。因此,如果一个客户端打开它以前打开过的文件,只有第一次Open()调用必然会导致RPC到主服务器。这种缓存被限制在一些小的方面,所以它从不影响客户端观察到的语义:如果应用程序已经关闭了,那么短暂文件的句柄不能保持开放;允许锁定的句柄可以被重复使用,但不能被多个应用程序的句柄同时使用。最后一个限制的存在是因为客户端可以使用Close()或Poison()来取消对主站的未完成的Acquire()调用,这是其副作用。
Chubby的协议允许客户端缓存锁--也就是说,持有锁的时间超过严格意义上的需要,希望它们能被同一个客户端再次使用。如果另一个客户端请求了一个冲突的锁,一个事件会通知锁持有者,允许持有者在其他地方需要锁的时候释放它(见第2.5节)。
2.8 Sessions and KeepAlives
Chubby会话是Chubby单元和Chubby客户端之间的一种关系;它存在一定的时间间隔,并由称为KeepAlives的定期握手维持。除非Chubby客户端通知主服务器端,否则只要会话保持有效,客户端的句柄、锁和缓存的数据都会保持有效。(然而,会话维护协议可能要求客户端确认缓存失效以维护其会话,见下文)。
客户端在第一次联系Chubby单元的主服务器时请求一个新的会话。当会话终止时,或者如果会话一直处于空闲状态(没有打开的句柄,一分钟内没有呼叫),它就明确地结束会话。
每个会话都有一个相关的租约--一个延伸到未来的时间间隔,在这个时间间隔内,主服务器保证不会单方面终止会话。这个时间间隔的终点被称为会话租赁超时。主服务器可以自由地将这个超时时间提前到未来,但不能将其向后移动。
主服务器在三种情况下推进租赁超时:在创建会话时,当主服务器发生故障时(见下文),以及当它响应客户端的KeepAlive RPC时。在收到KeepAlive时,主服务器通常会阻止RPC(不允许它返回),直到客户的前一个租赁间隔接近到期。主服务器后来允许RPC响应客户端,并因此通知客户端新的租赁超时。主服务器可以将超时时间延长到任何数量。默认的延长时间是12s,但是一个超负荷的主服务器可以使用更高的值来减少它必须处理的KeepAlive呼叫的数量。客户端在收到之前的回复后立即启动新的KeepAlive。因此,客户端确保在主服务器几乎总是有一个KeepAlive呼叫被阻止。
除了延长客户端的租约,KeepAlive回复还被用来将事件和缓存失效传送回给客户端。主服务器允许KeepAlive提前返回,当事件或失效要被传递时。在KeepAlive回复上捎带事件,可以确保客户端在不确认缓存失效的情况下不能维持一个会话,并导致所有Chubby RPCs从客户端流向主服务器。这简化了客户端,并允许协议在只允许在一个方向启动连接的防火墙中运行。
客户端维护一个本地租期超时,这个超时是对主服务器租用超时的一种近似。它与主服务器的租用超时不同,因为客户端必须对其KeepAlive回复的飞行时间和主服务器的时钟前进速度做出保守的假设;为了保持一致性,我们要求服务器的时钟前进速度不超过一个已知的常数,比客户端的快。
如果客户端的本地租约超时,它就不能确定主服务器是否已经终止了它的会话。客户端清空并禁用其缓存,我们说它的会话处于危险(jeopardy)之中。客户端再等待一个时间间隔,称为宽限期,默认为45秒。如果客户端和主控端在客户端的宽限期结束前设法交换了一个成功的KeepAlive,客户端就会再次启用其缓存。否则,客户端会认为会话已经过期。这样做是为了在Chubby单元变得不可访问时,Chubby API调用不会无限期地阻塞;如果在通信重新建立之前,宽限期结束,调用会返回错误。
Chubby库可以通过jeopardy事件通知应用程序宽限期何时开始。当已知会话在通信问题中幸存下来时,一个安全事件会告诉客户端继续进行;如果会话反而超时,则会发送一个过期事件。这些信息允许应用程序在不确定其会话状态时自行关闭,如果问题被证明是短暂的,则无需重新启动即可恢复。这对于避免具有大量启动开销的服务的中断非常重要。
如果客户端在一个节点上持有一个句柄H,而对H的任何操作都因为相关会话过期而失败,那么对H的所有后续操作(除了Close()和Poison())都会以同样的方式失败。客户端可以利用这一点来保证网络和服务器的中断只导致操作序列中的一个后缀丢失,而不是一个任意的子序列,从而允许复杂的变化被标记为最终写入。