2.4 Locks and sequencers
每个Chubby文件和目录都可以作为一个读写锁:一个客户端句柄可以在独占(写)模式下持有该锁,或者任何数量的客户端句柄可以在共享(读)模式下持有该锁。就像大多数程序员所知道的mutexes,锁是建议性的。也就是说,它们只与其他试图获得相同锁的人发生冲突:持有一个名为F的锁既不是访问文件F的必要条件,也不会阻止其他客户这样做。我们不使用强制锁,它使被锁的对象无法被未持有其锁的客户端访问。
在Chubby中,在任何一种模式下获得一个锁都需要写权限,因此没有特权的读者不能阻止写者的进展。
在分布式系统中,锁是复杂的,因为通信通常是不确定的,而且进程可能会独立失败。因此,一个持有锁L的进程可能会发出一个请求R,但随后失败。另一个进程可能会获得L,并在R到达其目的地之前执行一些行动。如果R后来到达,它可能在没有L的保护下被执行,而且可能是在不一致的数据上。人们对接收消息失序的问题进行了很多的研究;解决方案包括虚拟时间[11]和虚拟同步[1],它通过确保消息的处理顺序与每个参与者的观察一致来避免这个问题。
在一个现有的复杂系统中的所有互动中引入序列号是很昂贵的。相反,Chubby提供了一种方法,只有那些使用锁的交互才可以引入序列号。在任何时候,锁持有者都可以请求一个序列号,这是一个不透明的字节串,描述了锁获得后的状态。它包含了锁的名称、获取锁的模式(独占或共享)以及锁的生成号码。如果客户端期望操作受到锁的保护,客户端会将序列器传递给服务器(如文件服务器)。接收的服务器应该测试序列器是否仍然有效并具有适当的模式;如果不是,它应该拒绝该请求。序列器的有效性可以根据服务器的Chubby缓存来检查,或者,如果服务器不希望与Chubby保持会话,可以根据服务器观察到的最新的序列器来检查。序列器机制只需要在受影响的信息中添加一个字符串,并且很容易向我们的开发者解释。
尽管我们发现序列器使用起来很简单,但重要的协议发展缓慢。因此,Chubby提供了一个不完美但更容易的机制,以减少对不支持排序器的服务器的延迟或重新排序的请求的风险。如果一个客户以正常的方式释放一个锁,它就会像人们期望的那样,立即供其他客户索取。然而,如果一个锁因为持有者失败或变得不可访问而变得空闲,锁服务器将阻止其他客户端索取该锁,这段时间称为锁延迟。客户端可以指定任何锁的延迟时间,目前是一分钟;这个限制可以防止一个有问题的客户端使一个锁(以及一些资源)在任意长的时间内不可用。虽然不完美,但锁延迟可以保护未经修改的服务器和客户端免受信息延迟和重启造成的日常问题。
Chubby客户端在创建句柄时可以订阅一系列的事件。这些事件通过Chubby库的向上调用,异步地传递给客户端。事件包括:
事件传递发生于在相应的行动之后。因此,如果客户端被告知文件内容发生了变化,那么如果它随后读取该文件,应该保证看到新的数据(或更近的数据)。
最后提到的两个事件很少使用,而且事后看来可以省略。例如,在主服务器选举之后,客户通常需要与新的主服务器通信,而不是简单地知道一个主服务器的存在;因此,他们等待一个文件修改事件,表明新的主服务器已经在文件中写入其地址。理论上,冲突锁事件允许客户端对其他服务器上的数据进行缓存,使用Chubby锁来保持缓存的一致性。一个冲突锁请求的通知将告诉客户端完成使用与该锁相关的数据:它将完成悬而未决的操作,将修改内容冲到一个主服务器位置,丢弃缓存的数据,然后释放。到目前为止,还没有人采用这种使用方式。
客户端将Chubby句柄视为一个指向不透明结构的指针,支持各种操作。句柄只由Open()创建,并由Close()销毁。
Open()打开一个命名的文件或目录,产生一个句柄,类似于UNIX的文件描述符。只有这个调用需要一个节点名称;其他的都是对句柄进行操作。
名称是相对于现有的目录句柄进行计算的;Chubby库提供了一个在"/"上总是有效的句柄。目录句柄避免了在一个包含许多抽象层的多线程程序中使用一个程序范围内的当前目录的困难[18]。
客户端指向各种选项:
Close()关闭一个打开的句柄。不允许进一步使用该句柄。这个调用不会失败。一个相关的调用Poison()导致对句柄的未完成操作和后续操作失败,而不关闭它;这允许客户端取消由其他线程进行的Chubby调用,而不用担心它们所访问的内存被删除。作用于句柄的主要调用是:
GetContentsAndStat()返回一个文件的内容和元数据。文件的内容被原子化地全部读取。我们避免了部分读和写,以阻止大文件的出现。一个相关的调用GetStat()只返回元数据,而ReadDir()返回一个目录的子文件夹的名称和元数据。
SetContents()写入一个文件的内容。可选的是,客户端可以提供一个内容生成号,以允许客户端在一个文件上模拟比较和交换;只有当生成号是当前的,内容才会被改变。文件的内容总是以原子方式完整地写入。一个相关的调用SetACL()对与节点相关的ACL名称执行类似的操作。
Delete()将删除没有子节点的节点。Acquire(), TryAcquire(), Release()获取和释放锁。
GetSequencer()返回一个描述由这个句柄持有的任何锁的序列器(§2.4)。
SetSequencer()将一个序列器与一个句柄联系起来。如果序列器不再有效,对该句柄的后续操作将失败。
CheckSequencer()检查一个序列器是否有效(见§2.4)。
如果节点在句柄被创建后被删除,即使文件随后被重新创建,调用也会失败。也就是说,句柄与一个文件的实例相关,而不是与一个文件名相关。Chubby可以在任何调用中应用访问控制检查,但总是检查Open()调用(见§2.3)。
上述所有的调用除了调用本身需要的其他参数外,还需要一个操作参数。操作参数持有可能与任何调用相关的数据和控制信息。特别是,通过操作参数,客户可以:
客户端可以使用这个API来进行主服务器选举,如下所示:所有潜在的主服务器候选打开锁文件并试图获得锁。其中一个成功了,成为主服务器,而其他的则作为副本。主服务器用SetContents()将其身份写入锁文件,这样就可以被客户端和复制者找到,他们用GetContentsAndStat()读取文件,也许是为了响应文件修改事件(§2.5)。理想情况下,主服务器用GetSequencer()获得一个序列器,然后将其传递给与之通信的服务器;它们应该用CheckSequencer()确认它仍然是主服务器。锁延迟可用于不能检查序列器的服务(§2.4)。