The Chubby lock service for loosely-coupled distributed systems 阅读笔记
声明
本文是对文献[1]的阅读报告,内容非本人原创。
摘要
本文描述了使用Chubby锁服务的经验,该服务旨在为松散耦合的分布式系统提供粗粒度的锁以及可靠的(尽管容量较小的)存储。Chubby提供了一个类似于带有建议锁的分布式文件系统的接口,但是设计的重点是可用性和可靠性,而不是高性能。
引言
Chubby的锁服务,它适用于由高速网络连接的中等数量的小型机器组成的松散耦合分布式系统。锁服务的目的是允许其客户端同步其活动并就其环境的基本信息达成一致。主要目标包括可靠性、对中等规模客户的可用性和易于理解的语义;吞吐量和存储容量被认为是次要的。Chubby的客户端界面类似于执行整个文件读写的简单文件系统,并增加了建议锁和各种事件(如文件修改)的通知。Chubby能够帮助开发人员处理系统内的粗粒度同步,特别是处理从一组对等的服务器中选择领导者的问题。
合理性
锁服务有几个优势:
-
开发者在最开始的时候并不会考虑高可用,加上使用锁服务就可以让维护现有的程序结构以及交互模式变得更加简单。
-
我们的许多服务会选择一个主要的服务,或者在他们的组件之间对数据做区分,这些都需要一个能够传播结果的机制。
-
程序员更熟悉基于锁的接口。
-
分布式一致性算法使用多数表决的方法来做决策,所以它们使用副本集来达到高可用。例如,Chubby最少需要3个才能保证正常工作,通常使用5个副本。作为对比,如果一个客户端系统使用锁服务,即便是单独的客户端也能获得锁来保证程序的安全性。所以,一个锁服务能够减少客户端所依赖的服务数。
一个可能会让一些读者感到惊讶的选择是,我们不期望锁使用是细粒度的,在这种情况下,它们可能只被持有很短的时间(秒或更短);相反,我们期望粗粒度的使用。例如,应用程序可能使用锁来选择主节点,然后主节点将在相当长的一段时间内(可能是几个小时或几天)处理对该数据的所有访问。这两种使用方式与锁服务器的要求不同。
粒度锁对锁服务器的负载要小得多。特别是,锁定获取速率通常与客户端应用程序的事务速率关系不大。粗粒度锁很少被获取,因此临时锁服务器不可用对客户端的延迟更少。另一方面,从客户机到客户机的锁传输可能需要昂贵的恢复过程,因此人们不会希望锁服务器的故障转移导致锁丢失。因此,粗粒度锁在锁服务器故障中存活是很好的,不需要担心这样做的开销,这样的锁允许少量可用性较低的锁服务器为许多客户端提供充分的服务。细粒度锁会导致不同的结论。即使是短暂的锁服务器不可用也可能导致许多客户端停滞。
chubby只提供粗粒度的锁,但是应用程序可以根据需要实现细粒度的控制。应用程序可以将它的锁划分成组,然后使用chubby的粗粒度锁来分配这些锁组给特定于应用程序的锁服务器。小状态需要维护这些细粒度锁;这些服务器只需要保持一个非易失性的、单调递增的获取计数器,很少更新。客户端可以在解锁时得知丢失的锁,如果使用简单的固定长度租约,协议可以简单和高效。
系统结构
Chubby主要有两个通过RPC进行通信的组件:一个是服务器,另一个是客户端程序需要链接的库。第三个组件,代理服务器,是可选的。
由上图1所示,每个Chubby Cell由一组被称为副本的服务器组成,放置这些服务器的目的是减少相关故障的可能性。副本使用分布式共识协议来选择主节点;主服务器必须获得大多数副本的投票,并承诺这些副本在几秒钟的时间间隔内不会选举另一个主服务器,即主租约。主租约由副本定期更新,只要主租约继续赢得多数选票。
副本维护简单数据库的副本,但只有主数据库启动对该数据库的读写。所有其他副本只是简单地从使用共识协议发送的主服务器复制更新。
客户端通过向DNS中列出的副本发送主位置请求来找到主服务器。非主副本通过返回主副本的标识来响应此类请求。一旦客户端找到了主服务器,客户端就会将所有请求指向它,直到它停止响应,或者直到它表明它不再是主服务器。写请求通过共识协议传播到所有副本;当写操作到达计算单元中的大部分副本时,将确认此类请求。读请求由主节点单独满足。
如果一个副本失败,并且在几个小时内没有恢复,那么一个简单的替换系统将从空闲池中选择一台新的机器,并在其上启动锁服务器二进制文件。然后它更新DNS表,用新副本的IP地址替换失败副本的IP地址。当前主服务器定期轮询DNS,并最终注意到更改。然后更新单元格数据库中的单元格成员列表;通过常规复制协议,该列表在所有成员之间保持一致。同时,新副本从存储在文件服务器上的备份和活动副本的更新组合中获得数据库的最新副本。一旦新副本处理了当前主服务器等待提交的请求,就允许该副本在新主服务器的选举中投票。
文件、目录和句柄
Chubby对外提供了一个类似于Unix系统文件系统的目录树,可以通过目录树来访问所有的文件。
例如/ls/foo/wombat/pouch
,ls
是所有Chubby的名字,foo
是Chubby Cell的名字;local
是一个特殊的Chubby Cell名,这表示应该使用客户端的本地Chubby Cell。
命名空间(name space)仅包含文件和目录,通常称之为节点(node)。每一个这样的节点在其Chubby Cell中只有一个名称;没有软链接和硬链接。
每个节点都用不同的元数据,包含三个访问控制列表(access control lists, ACLs)以控制读权限、写权限和修改ACL权限。除非重写ACL,每个文件在创建时都会继承其父目录的权限。
ACL本身是位于ACL目录中的文件。如果一个文件F的写访问控制文件的名字是foo,那么该ACL目录就会包含一个名为foo的文件,该文件中包含一个项,该项的值为bar,那么用户bar就拥有了对F文件的写访问控制文件。用户通过RPC系统中内置的机制进行身份验证。
每个节点的元数据包括四个单调递增的64位数字,允许客户端轻松地检测变化:
- instance number,大于前面任何具有相同名称的节点的实例数。
- content generation number(只有文件才有该数字),当文件的内容被写入时,他的值会增加。
- lock generation number,当节点的锁从自由转换为持有时,该值会增加。
- ACL generation number,当该节点的ACL名被写入时该值会增加
Chubby也向外提供了一个64位的文件校验和来显示该文件之间是否有差异。
Chubby客户端打开节点时可以获得类似于unix文件描述符的Handle,该handle包括:
- 检查防止客户端创建或猜测句柄的数字,因此只需要在句柄创建时执行完全访问控制检查(与UNIX相比,UNIX在打开时检查其权限位,但不是在每次读/写时检查,因为文件描述符不能伪造)。
- 一个序列号,允许主服务器判断句柄是由它自己生成的还是由前一个主服务器生成的。
- 打开时提供的模式信息,如果旧句柄被提供给新重启的主程序,则允许主程序重新创建其状态。
lock and sequencer
Chubby中使用的是读写锁。读锁是可共享的,写锁是互斥的。同时,Chubby中的锁是建议性的(advisory),而非强制性的(mandatory)[2],这使得没有使用锁的客户端也可以访问文件。
在Chubby中,在任何一种模式下获取锁都需要写权限,这样无特权的读取器就不能阻止写入器进行操作。
在分布式系统中,锁定是复杂的,因为通信通常是不确定的,进程可能独立地失败。因此,持有锁L的进程可能发出请求R,但随后失败。另一个进程可能获得L并在R到达目的地之前执行某些操作。如果R随后到达,则可能在没有L保护的情况下对其进行操作,并且可能对不一致的数据进行操作。对接收信息的无序问题进行了深入的研究;解决方案包括虚拟时间(virtual time)[3]和虚拟同步(virtual synchrony)[4],它们通过确保按照与每个参与者观察结果一致的顺序处理消息来避免这个问题。
在现有的复杂系统中,将序列号引入到所有的交互中是非常昂贵的。相反,Chubby提供了一种方法,通过这种方法,序列号只能被引入到那些使用锁的交互中。在任何时候,锁持有者都可以请求一个序列(sequener),一个不透明的字节串,描述获得锁后立即的状态。它包含锁的名称、获取锁的模式(独占或共享)以及锁生成编号。如果客户端希望该操作受到锁的保护,则将顺序器(sequener)传递给服务器(例如文件服务器)。接收服务器将测试序列是否仍然有效,是否具有适当的模式;如果没有,它应该拒绝请求。顺序器(sequener)的有效性可以根据服务器的Chubby缓存进行检查,或者如果服务器不希望维护与Chubby的会话,则根据服务器最近观察到的顺序器(sequener)进行检查。顺序器(sequener)机制只需要在受影响的消息中添加一个字符串,并且很容易向我们的开发人员解释。
顺序器(sequener)协议的实现仍然是一个问题。目前采取了锁延迟的机制。如果一个客户端以正常的方式释放锁,那么其他客户端可以立即获得锁。但是,如果一个锁因为持有者失败或变得不可访问而变得空闲,锁服务器将在一段称为锁延迟的时间内阻止其他客户端申请锁。客户端可以指定锁延迟,目前是一分钟;这个限制可以防止错误的客户端使锁(以及一些资源)在任意长的时间内不可用。虽然锁延迟并不完美,但它可以保护未修改的服务器和客户端免受消息延迟和重启引起的日常问题。
events
客户端在创建句柄(handle)时,会订阅一系列的事件,这些事件包括:
- file contents modified
- child node added, removed, or modified
- Chubby master 通知客户端某些事件监听丢失,必须重新扫描数据
- a handle (and its lock) has become invalid
- lock acquired -- can be used to determine when a primary has been elected
- conflicting lock request from another client
缓存
为了减少读开销,Chubby客户端将文件数据和节点元数据(包括文件缺失)缓存到内存中一致的缓存中,该缓存的一致性使用写直达(write-through)技术实现。关于write-through读者应当在计算机体系结构有关CPU缓存的课程中学到,而文章中也没有提出什么新鲜的idea,因此不再赘述。需要说明的是,CPU中的各个核心就是论文中的客户端,CPU的一级或二级缓存就是论文中的内存缓存。
会话与保活 sessions and keepalive
一个Chubby session是一个Chubby cell和一个Chubby client之间的连接,使用周期性握手维护KeepAlive。只要它的会话保持有效,客户端句柄、锁和缓存的数据都保持有效,除非Chubby客户端通知主服务器某些信息失效。
会话建立:客户端会主动向Chubby Cell的主节点发送连接请求,当会话终止或者会话处于idle状态(一分钟内没有打开的句柄或者进行调用)时,会主动结束会话。
每次会话都会有一段租期,主节点保证在租期内不会主动断开会话。主节点只会在以下情况中主动终止会话:在创建会话时或者响应KeepAlive RPC时,主节点发生故障。客户端调用KeepAlive RPC时,主节点会阻塞该调用,直到客户端的租期接近结束时才会响应。客户端在收到应答后会立即再次发送一个KeepAlive RPC。主节点通常会在租期上延长一段时间,以确保客户端发出的下次KeepAlive RPC到达服务器。KeepAlive应答还用于向客户端发送事件和缓存失效。主节点允许在交付事件或失效时提前返回KeepAlive。KeepAlive响应上的附带事件确保了客户端在没有确认缓存失效的情况下无法维护会话。
客户端也会维护一个租期,但是该租期与服务器并不完全相同。因为客户端必须考虑网络时延和服务器时钟速度。为了保持一致性,我们要求服务器的时钟不能比客户端的时钟快于已知常数因子。
当客户端本地租期到期时,客户端无法确定服务器是否已经关闭了会话。此时,该会话处于危险之中,客户端将会清空并金庸其缓存。客户端会等待一个宽限期,如果在宽限期内成功响应keepalive,那么会重新使能缓存。否则,客户端认为会话已经过期。
chubby库会通过危险事件(jeopardy event)通知客户端宽限期过期。
如果客户端持有节点上的句柄H,并且由于关联的会话已过期而导致对H的任何操作都失败,那么对H的所有后续操作(除了Close()和Poison())都会以同样的方式失败。客户端可以使用这一点来保证网络和服务器中断只会导致操作序列的一个后缀丢失,而不是任意的子序列丢失,从而允许将复杂的更改标记为已提交的最终写入。
fails-over
master一旦丢掉了master资格,就会清空掉session、handle、lock的内存状态。但session timer会直到新master产生才结束(不立即让client的session失效)。这样如果client能在grace period与新master建立通信,它的所有现存session都不会受影响。在grace period中,client会阻塞住应用调用,以避免应用看到不一致的数据。新master产生后部分通过读取稳定存储在磁盘上的数据(通过正常的数据库复制协议进行复制),部分通过从客户端获取状态,部分通过保守的假设实现。数据库记录每个会话、持有的锁和临时文件。
其他机制
Database
Chubby的第一个版本使用Berkeley DB作为它的数据库。
backup
每隔一段事件,每个Chubby Cell的master都会将数据库的快照写入不同的GFS进行灾备。
mirroring
Chubby允许将一组文件从一个Chubby Cell镜像到另一个Chubby Cell。如果没有网络问题,变化会在不到一秒钟的时间内反映在全球几十个镜像上。如果一个镜像不可达,它将保持不变,直到连接恢复。然后通过比较它们的校验和来识别更新的文件。