Chubby:面向松耦合的分布式系统的锁服务[译]

本文是Google Chubby论文《The Chubby Lock Service for Loosely-Coupled Distributed Systems》的部分翻译。


1.  简介

Chubby是一种面向松耦合的分布式系统的锁服务,通常用于为一个由适度规模的大量小型计算机构成的的松耦合的分布式系统提供高可用的分布式锁服务。锁服务的目的是允许它的客户端进程同步彼此的操作,并对当前所处环境的基本状态信息达成一致。因此,Chubby的主要设计目标是为一个由适度大规模的客户端进程组成的分布式场景提供高可用的锁服务,以及易于理解的API接口定义。而值得一提的是,在Chubby的设计过程中,系统的吞吐量和存储容量并不是首要考虑的因素。

Chubby的客户端接口设计非常类似于文件系统结构,不仅能够对Chubby上的整个文件进行读写操作,还能够添加对文件节点的锁控制,并且能够订阅Chubby服务端发出的一系列文件变动的事件通知。

通常,开发人员使用Chubby来解决分布式系统多个进程之间粗粒度的同步控制,其中最为典型的应用场景就是集群中服务器的Master选举。例如在Google文件系统(Google File System[1]中使用Chubby锁服务来实现对GFS Master服务器的选取。另外,在Bigtable[2]中,Chubby同样被用于进行包括Master选举,并且能够非常方便的让Master感知到其所控制的那些服务器。同时,借助Chubby,还能够便于Bigtable的客户端定位到当前Bigtable集群的Master。此外,在GFSBigtable中,都使用Chubby最为典型的应用场景:系统元数据的存储。

Chubby发布之前,Google的大部分分布式系统使用必需的、未提前规划的(ad hoc)方法做主从选举(primary election)(在工作可能被重复但无害时),或者需要人工干预(在正确性至关重要时)。对于前一种情况,Chubby可以节省一些计算能力。对于后一种情况,它使得系统在失败时不再需要人工干预,显著改进了可用性。

熟悉分布式计算的读者都知道,在分布式系统的多个服务器中进行Master选举是一个特殊的分布式一致性问题,并且需要一种异步通信的解决方案。异步通信这个术语描述了绝大多数真实网络环境(如以太网或因特网)的通信行为:它们允许数据包的丢失、延时和重排。值得庆幸的是,异步一致性已经由Paxos协议解决了,甚至可以说,迄今为止所有可用的异步通信的网络协议,其一致性的核心都是Paxos

Chubby并非是一个分布式一致性的学术研究,而是一个满足上文提到的各种一致性需求的工程实践,同时在Chubby中,没有提出新的算法或技术。这里我们主要讲解下Chubby的设计思路以及这么做的原因。在接下来的小节中,将重点讲解Chubby的设计与实现,以及在实际实践过程中发生变更的地方。另外,在章节中,还会讲解一些Chubby在实际使用过程中那些生僻的应用方式,以及Chubby中一些在实践中被证明是错误的特性。当然,对于那些诸如一致性协议和RPC调用等方面的内容将不再本章中重点展开讲解,感兴趣的读者可以在其他资料中对其进行进一步的了解。

2.  设计

2.1             设计原则

有人可能会争论说Chubby应该构建一个包含Paxos协议的库,而不是一个需要访问中心化的集群锁服务的库,尽管这个中心化的集群能够提供高可靠的锁服务。他们的原因的很简单,因为一个仅仅包含Paxos协议集合的客户端库就不需要依赖于其他服务器(当然一些特定场景下,还是需要一个分布式命名服务),并且如果假定这些服务都可以实现为状态机,那么将为开发者提供一个标准化的框架。而事实上,Chubby的开发人员确实也为我们提供了这样一个与Chubby无关的客户端库。

然而,Chubby之所以设计成一个完整的锁服务,具有一些类似于上面提到的仅仅是Paxos协议客户端库所不具有的优点。

第一.       对上层应用的侵入性更小

有时候,在系统开发初期,开发人员并没有从一开始就为系统的高可用性做充分的考虑。绝大部分的系统都是从一个只需要支撑较小负载,且保证大体可用的原型开始的,往往并没有在代码层面为一致性协议的实现留有余地。当系统提供的服务日趋成熟,并且得到一定规模的用户认可之后,系统的可用性就会变得越来越重要了,于是,副本复制和Master选举等一系列提高分布式系统可用性的措施,就会被加入到一个已有的系统中去。在这种情况下,尽管这些措施都可以通过一个封装了分布式一致性协议的客户端库来完成,但相比之下,使用一个锁服务的方式对上层应用的侵入性则更小,并且更易于保持系统已有的程序结构和网络通信模式。举个例子来说,如果采用锁服务的模式,那么对于一个Master选举的场景来说,仅仅只需要在已有的系统中添加两条语句和一个RPC调用参数即可:一条语句用来获取一个分布式锁并成为Master,另外,可以通过在发起更新操作的RPC调用时传递一个整数(可以看做是一个版本信息,用来标识当前RPC更新操作基于某个版本进行更新),当服务端检测到某一个数据项当前的版本大于该RPC调用中包含的版本号是,就会拒绝本次RPC调用,这可以有效的防止分布式的非原子操作。由此可见,使用Chubby的锁服务远比将一个一致性协议库加入到已有的系统中来的简单。

第二.       便于提供数据的发布与订阅

几乎在所有使用Chubby来进行Master选举的应用场景中,都需要一种广播结果的机制。这就意味着Chubby应该允许其客户端在服务器上进行少量数据的读取与存储——也就是对小文件的读写操作。虽然这个特性也能够通过一个命名服务来完成,但是根据我们的经验来说,分布式锁服务本身也非常适合提供这个功能,这一方面能够大大减少客户端依赖的外部服务器数量,另一方面,数据的发布与订阅功能和锁服务在一致性特性上是相通的。

第三.       开发人员对基于锁的接口更为熟悉

对于绝大部分的开发人员来说,在平常的编程过程中,对基于锁的接口都非常熟悉。因此,对于开发人员来说,提供一套近乎和单机锁机制一致的分布式锁服务,远比提供一个一致性协议的库来的友好。

第四.       更便捷的构建更可靠的服务

通常一个分布式一致性算法都需要使用Quorum机制来进行数据项值的选定,因此绝大部分的实现系统采用多个副本来实现高可用。例如,在Chubby中通常使用5个服务器来组成一个集群单元(cell),只要整个集群中有三台服务器是正常运行的,那么集群就可以正常运行。相反的,如果仅仅是提供一个分布式一致性协议的客户端库,那么这些高可用性的系统部署将交给开发人员自己来处理,这无疑提高了成本。

基于以上四点,Chubby遵守以下两个主要的设计原则,分别是:

  • Chubby需要提供的是一个完整的分布式锁服务,而非仅仅是一个一致性协议的客户端库。

  • Chubby同时需要提供小文件的读写服务,以使得被选举出来的Master可以在不依赖额外的服务情况下,非常方便的向所有客户端发布自己的状态信息。

同时,根据Chubby的应用场景,在设计上还需要考虑到几下几方面:

  • 由于Chubby提供了通过小文件读写服务的方式来进行Master选举结果的发布与订阅,因此在Chubby的实际应用过程中,即使在少量的服务器规模情况下,必须能够支撑成百上千个Chubby客户端对同一个文件进行监视和读取。

  • Chubby客户端需要实时的感知到Master的变化情况,当然这可以通过让客户端反复的轮询来实现,但是在客户端规模不断增大的情况下,客户端主动轮询的实时性效果并不理想。因此,Chubby需要有能力将服务端的数据变化情况以时间通知的形式通知到所有订阅的客户端。

  • 根据上面这点,即使在机制上,已经支持客户端不需要轮询即可获取到Chubby服务端的数据变更,但由于在实现应用场景中,客户端用法非常多样,因此不可避免还是会出现有些客户端不断的轮询服务端。因此需要加入缓存机制来避免这些无谓的轮询。

  • 缓存数据必须是一致的。

  • 需要提供包括访问控制在内的安全机制来避免部分客户端对服务端数据的非法操作。

这里我们需要指出的一点是,在分布式锁的使用上,我的建议的是,一定不要将其使用在一些细粒度的锁控制上,因为细粒度的锁控制往往发生在几秒甚至更短的时间周期内,而分布式锁毕竟需要一些网络通信和分布式协调,因此在性能上和单机锁不可相提并论,比较合理的做法是,将分布式锁使用在粗粒度的协作控制上。例如,在一个使用Chubby的锁服务来实现的集群Master选举的应用场景中,应用程序在选举出Master之后,该Master会在小时,甚至是数天时间内,一直承担Master的角色来完成一些独占的数据访问。

由此可见,粗粒度的锁所控制机制对于分布式锁服务器带来的负载要小得多。尤其需要指出的是,在这种机制下,分布式系统对于锁的获取频率通常和客户端应用系统的事务频率并不存在正相关性。通常情况下,粗粒度的锁不会被客户端应用程序频繁的请求获取,因此即使在锁服务器出现短暂的不可用现象,也并不会给客户端应用程序带来特别严重的影响。同时,我们需要考虑到的一点是,将锁从一个客户端转移到另一个客户端的过程可能会引入其他复杂的恢复处理,因此,如果锁服务器真的出现短暂的不可用现象时,我们希望当锁服务器恢复正常之后,当前的锁控制依旧有效。

而细粒度的锁机制对锁服务器的可用性要求则要苛刻的多,因为即使是锁服务的短暂不可用,也可能导致许多客户端无法正常运行。其根本原因是对于细粒度锁的获取频率和客户端应用系统的事务频率存在正相关性。因此针对细粒度锁,当出现锁服务器短暂不可用时,就需要快速的将当前的锁控制失效。

Chubby设计为只提供面向粗粒度锁机制的锁服务,而放弃对细粒度锁控制的支持,这足以将开发人员从自己实现一整套分布式协调机制的复杂性中解放出来。

2.2       系统结构

Chubby主要由服务端和客户端两部分组成,如图所示是Chubby服务端和客户端的结构示意图,从图中可以看出,两者之间通过RPC通信来完成分布式协调。

wKioL1QNex-CBMnuAADcNssu3O0676.jpg

Chubby服务端与客户端结构示意图

一个典型Chubby集群,或称为Chubby cell,通常由五个服务器组成,这些副本服务器采用分布式一致性协议,通过投票的方式,并最终选举产生一个获得过半投票的服务器作为Master。在Master选举产生后,会规定在一个周期长度为数秒的Master租期(Master lease)内,不再选举另一个Master。之后,如果这个Master持续获得集群过半的投票,那么整个集群就会周期性的刷新该Master租期。

整个集群的所有机器上都维护着服务端数据库数据的拷贝,但在运行过程中,只有Master服务器才能对数据库进行读写操作,而其他的服务器都是使用一致性协议从Master服务器上同步数据的更新。

现在,我们在来看下Chubby的客户端是如何定位到Master服务器的。Chubby的客户端通过向记录有服务端机器列表的DNS来请求获取所有的Chubby服务器列表,然后逐个发起请求询问该服务器是否是Master,而那些非Master的服务器,则会将当前Master所在的服务器标识反馈给客户端,这样客户端就能够非常快速的定位到Master服务器了。

一旦客户端定位到Master服务器之后,只要该Master正常运行,那么客户端会将所有的请求都发送到该Master。针对写请求,Chubby会采用一致性协议将其广播给集群中所有的副本,并且在过半的服务器接受了该写请求之后,再响应给客户端正确的应答。而对于读请求,则不需要在集群内部进行广播处理,直接由Master服务器单独处理即可。如果当前的Master服务器崩溃了,那么集群中的其他服务器会在Master租期到期后,重新开启新一轮的Master选举。通常,进行一次Master选举大概需要花费几秒钟的时间,

如果集群中的一个服务器发生崩溃并在几小时后扔无法恢复正常,那么这个时候就需要进行机器更换了。Chubby服务器的更换方式非常简单,只需要启动一台新的服务器,启动Chubby服务端程序,然后更新DNS上的机器列表,即使用新机器的IP地址替换老机器的IP地址即可。在Chubby运行过程中,Master服务器会周期性地轮询DNS列表,因此很快就会感知到服务器地址列表的变更,然后Master就会将集群数据库中的地址列表做同样的变更,集群内部的其他副本服务器通过复制方式就可以获取到最新的服务器地址列表了。新加入集群的这台机器,首先会从集群其他服务器上同步数据和当前集群的事务变更操作。一旦这个新机器处理了当前Master广播的事务请求后,就可以在之后参与Master的选举了。

2.3       文件和目录

Chubby对外提供了一套类似于UNIX文件系统的接口,事实上,比Unix的文件系统操作简单很多。Chubby的数据结构可以看做是一个由文件和目录组成的树,其中每一个节点都可以表示为一个使用斜杠分割的字符串,典型的节点名字如下所示:

       /ls/foo/wombat/pouch

其中“ls”是所有Chubby节点所共有的前缀,代表着锁服务(lock service)。第二个部分(foo)是Chubby集群的名字,从DNS可以查询到由一个或多个服务器组成该Chubby集群。名字的剩余部分(/wombat/pouch),则是一个真正包含业务含义的节点名字,由Chubby服务器内部解析并定位到数据节点。同样跟UNIX系统一样,每个目录都可以包含一系列的子文件和子目录列表,而每个文件中则会包含文件内容。

由于Chubby的命名结构组成了一个文件系统,因此Chubby的客户端应用程序也可以通过自定义的文件系统访问接口来访问Chubby服务端数据,比如可以使用GFS的文件系统访问接口。这样显著减少了用户使用Chubby的成本,这点对于那些偶尔使用以下Chubby的用户来说,非常重要。

Chubby的命名空间,包括文件和目录,我们称之为节点(nodes),没有软连接和硬连接表示,在同一个Chubby集群数据库中,每一个节点都是全唯一的。

Chubby的节点分为持久节点和临时节点两大类,无论哪种类型的节点,都可以被显示地删除掉,但临时节点比较特殊,其生命周期和客户端会话绑定,也就是说,如果该临时节点对应的文件没有被任何客户端打开的话,那么它就会被删除掉——因此,通常使用临时节点来标识一个客户端是否存活。任何节点都可以被当做一个读/写锁来使用。

每个节点都包含一些元数据,包括访问控制列表(ACLs)的三个配置信息,分别用于控制节点的读、写和ACLs修改权限。需要注意的一点是,除非为节点重新设置ACL信息,否则每个节点都会在创建的时候从父节点继承ACLs信息。所有节点的ACLs信息都记录在Chubby一个单独的ACL目录中,这个ACL目录是Chubby服务器上保留的数据节点。

每个节点的元数据中还包括四个单调递增的64位编号,如下:

  • 实例编号:实例编号和节点的创建顺序有关,节点的创建顺序不同,其实例编号一定不同。根据实例编号,即使两个名字相同的数据节点,客户端也能够非常方便的识别出是否是同一个节点,因为新创建的节点必定大于任意先前创建的同名节点的实例编号。

  • 文件内容编号(只针对文件):这个编号在文件内容被写入时增加。

  • 锁编号:这个编号在节点的锁由自由(free)转换到被持有(held)状态时增加。

  • ACL编号:这个编号在节点的ACL配置信息被写入时增加。

同时,Chubby还会标识一个64位的文件内容校验码,以便客户端能够识别出文件是否变更。

Chubby客户端通过与服务端的数据节点创建一个类似于UNIX文件描述符的句柄(handles)来作为操作对象。该句柄包括:

  • 校验码:校验码能够有效的阻止客户端自行创建或伪造一个句柄,因此Chubby服务端只需要在客户端创建句柄的时候进行一次完整的访问控制检查即可(这和UNIX文件系统非常相似,由于UNIX文件描述符不可伪造,因此操作系统也只会在文件打开的时候检查文件的访问权限,而不会在每次文件读写时都进行检查)。

  • 序列号:序列号能够便于Master识别出该句柄是在当前Master租期内创建的,还是在之前的Master租期创建的。

  • 模式信息:如果当前句柄是由之前的Master创建的,那么模式信息用来设定当前Master在碰到这样的句柄时,是否为该句柄重置状态。

  • 锁和序号

每一个Chubby文件和目录都可以充当一个读写锁使用:一种是单个客户端以排他(写)模式持有这个锁,另一种则是任意数目的客户端以共享(读)模式持有这个锁。同时,在Chubby的锁机制中需要注意的一点是,Chubby舍弃了严格的强制锁,客户端可以在没有获取任何锁的情况下访问Chubby的文件,也就是说,持有锁F既不是访问文件F的必要条件,也不会阻止其他客户端访问文件F

由于网络通信的不确定性,导致在分布式系统中锁机制变得非常复杂。并且进程可能各自独立地失败(fail independently)。一个典型的分布式锁错乱案例是,当一个客户端C1在获得了锁L后发起请求R,但请求迟迟没有到达服务端(网络延时或反复重发等),这个时候,应用程序会认为这个客户端已经失败,于是就会让另一个客户端C2获取到锁L重新发起之前请求R对应的操作,并成功的应用到了服务器上。在这个时候,C1发起的请求R在经过一波三折后,也到达了服务端,此时,他有可能会在不受任何锁控制的请求被服务端处理,从而覆盖了C2的操作,于是导致系统数据出现不一致。当然,诸如此类消息接收顺序紊乱引起的数据不一致问题已经在人们对分布式计算的长期研究过程中得到了很好的解决,典型的解决方案包括虚拟时间和虚拟同步。这两个解决方案不是本书的重点,感兴趣的读者可以在互联网上了解更多公开的参考资料[3]

在一个已经初步成熟的复杂应用程序的每次操作中引入请求序号是非常麻烦的,因此,退一步求其次,Chubby采用了在所有使用锁的操作中引入请求序号。任何时候,锁的持有者都会请求一个序号器,包括锁的名字,锁模式(排他或共享模式),以及锁的编号。当客户端在进行一些希望锁机制保护的操作是,就会将该序号器发送给服务端。服务器接收到这样的请求后,会首先检测该序号器是否有效,以及检查客户端是否处于恰当的锁模式;如果没有检查通过,那么服务端就会拒绝该客户端请求。

同时,Chubby也为那些没有使用序号器的客户端提供了一种不是特别完美但是也能够降低消息的延迟或重排序对数据一致性造成影响的解决方案。具体的,如果一个客户端以正常的方式主动释放了一个锁,那么Chubby服务端将会允许其他客户端能够立即获取到该锁。而如果一个锁是因为客户端的异常情况(如客户端无响应)而被释放的话,那么Chubby服务器会为该锁保留一定的时间,我们称之为“锁延时”(lock-delay),在这段时间内,其他客户端无法获取这个锁。锁延时措施能够很好的防止一些客户端由于网络闪断等原因和服务器暂时断开的场景。总的来说,该方案尽管不完美,但是锁延时能够有效的保护在出现消息延时情况下发生的数据不一致现象。

2.5       事件

Chubby的客户端可以向服务端注册事件通知,当触发这些事件的时候,服务端就会向客户端发送对应的事件通知,常见的Chubby事件包括:

  • 文件内容变更。

  • 子节点增加、删除和修改。

  • Master服务器故障恢复。

  • 某客户端会话及其对应的锁失效了。

  • 锁被请求了。

  • 来自另一个客户端的相冲突的锁请求。

举个例子来说,假如一个客户端接收到服务端文件内容变更的事件通知,那么就能够保证该客户端接下去从服务器上能够读取到最新,甚至比收到事件通知的时候更新的数据。

在上面提到的所有Chubby事件类型中,最后面两种事件类型并不是特别常用,事实上就目前来看,甚至可以将这两种事件删掉。

2.7       缓存

为了减少客户端和服务端之间读请求的数据传输量,Chubby客户端会将文件的数据及节点元数据缓存在内存的直写式(write-through)缓存中。这个缓存的生命周期和Master租期机制紧密相关,Master上维护着每个客户端的数据缓存情况,并通过向客户端发送过期信息来维护客户端数据的一致性。在这种机制下,Chubby能够保证客户端要么能够从缓存中访问到一致的数据,要么访问出错,也就是说,一定不会访问到不一致的数据。

当文件或节点的元数据信息被修改时,Chubby首先会阻塞该修改操作,然后由Master向所有可能缓存了该数据的客户端发送缓存过期信号,等到Master在接收到所有相关的客户端针对该过期信号的应答后(应答包括两类,一种是客户端明确要求更新缓存,另一类则是客户端允许缓存租期过期),在继续之前的修改操作。

以上这种处理方式,很好的保证了读操作能够被无延时地得到处理,这一点非常有用,因为在绝大部分的场景下,读操作的数量远远大于写操作。对于缓存处理,还有另外一个可选的方案是在缓存过期期间,阻塞所有对该节点的数据访问,这将会大大减少在缓存过期期间,那些采用非缓存模式的客户端访问Master的频率——当然,这将在短暂的时间段内牺牲实时性。另外还有一种折中的方案,就是根据系统的实际运行负载情况在不同的策略之间进行切换。

尽管要保证严格的数据一致性对于性能的开销很大,但由于弱一致性模型在实际使用过程中极容易出现问题,因此Chubby在设计之初就决定了选择强一致性。因为我们觉得程序员们将发现它们很难用。另一方面,如果采用类似于虚拟同步(virtual synchrony)的一致性协议机制,则需要在客户端和服务端之间的所有通信上都携带上序列号,考虑到在一些已经成熟的复杂系统上进行这项改造成本非常之大,尤其是那些自己实现了网络通信协议应用程序,兼容性的保证更是非常困难,因此也不合适。

此外,除了文件数据和节点元数据之外,Chubby客户端还缓存了那些打开的句柄。因此,如果客户端视图打开一个之前已经打开过的文件,那么这次对open()接口调用是不会触发一次客户端和服务端之间的RPC调用的。

2.8       会话和KeepAlives(会话激活)

Chubby客户端和服务端之间通过创建一个TCP连接来进行所有的网络通信操作,我们将这一连接称为会话(Session)。会话具有一定的生命周期,存在超时时间,在超时时间内,Chubby客户端和服务端之间可以通过心跳检测来保持会话的活性,以使会话周期得到延续,我们将这个过程称为KeepAlive(会话激活)。如果通过KeepAlive过程将Chubby会话一直延续下去,那么客户端创建的句柄、锁和缓存数据等,都依然有效。

在上面我们也提到了,Chubby会话存在一个超时时间,我们将这段时间成为会话租期,在这个会话租期内,Master会保证在会话租期内,不会单方面中止会话。在以下三种情况下,Master会为该会话续租,分别是:会话创建初始化阶段、Master自身故障恢复时和响应会话对应客户端的KeepAlive请求时。

其中我们主要来看下Master对客户端KeepAlive请求的处理。Master在接收到KeepAlive请求时,首先会将该请求阻塞住,并等到该客户端的当前会话租期即将过期时,才向客户端响应这个KeepAlive请求,并续租该客户端的会话租期,同时将最新的会话租期超时时间反馈给客户端。Master对于会话续租时间的设置,默认是12秒,但不是固定的,根据实际运行情况,Master会自行调节该周期。举个例子来说,如果当前Master处于高负载运行状态的话,那么Master将会适当的延长会话租期的长度,以减少客户端KeepAlive请求的发送频率。客户端在接收到来自Master的续租响应后,会立即发起一个KeepAlive请求。因此我们可以看出,在正常运行过程中,每一个Chubby客户端总是会有一个KeepAlive请求阻塞在Master上。

除了为客户端会话进行续租外,Master还将通过KeepAlive响应来传递Chubby事件通知和缓存过期通知给客户端。具体的,如果Master发现已经产生针对该客户端的事件通知或缓存过期通知需要发送时,那么会提前将KeepAlive响应反馈给客户端。Master通过在KeepAlive响应中传递事件的机制,保证了客户端只有在应答了缓存过期通知的情况才能够维持会话,并且使得所有Chubby中的RPC调用都是从客户端发起的,这大大简化了客户端的设计。

客户端维持着一个和Master近似相同的会话租期超时时间。为什么是近似相同的呢,是因为客户端必须考虑两方面的因素,一方面KeepAlive响应在网络传输过程中会花费一定的时间,而另一方面则是Master服务端时钟频率的超前程度。为了维护服务端和客户端对会话租期超时的一致性,我们要求服务端的时钟频率比客户端最大不能超前某个阈值。

如果客户端检测到按照本地的会话租期超时时间,其租期已经过期乐乐,这个时候,它将无法确定Master服务端是否中止了当前会话,我们称这个时候客户端处于“危险状态”。此时,Chubby客户端会清空其本地缓存,并将其标记为不可用。同时,客户端会等待一个被称作“宽限期”的时间间隔,这个宽限期默认是45秒。如果在宽限期到期前,客户端和服务端之间成功进行KeepAlive,那么客户端就会再次开启本地缓存,否则,客户端就会认为当前会话已经过期了。这样的机制,保证了Chubby客户端的API调用在服务端不可用的时候,不会无限期的阻塞下去,而是会在宽限期结束的时候,返回一个错误信息。

当客户端进入上述提到的危险状态时,Chubby的客户端库会通过一个“jeopardy”事件来通知上层应用程序。而如果在恢复正常后,客户端则同样会以一个“safe”事件来通知应用程序可以继续正常运行了。而如果最终没能从危险状态中恢复过来,那么客户端会以一个“expired”事件来通知应用程序当前Chubby会话已经超时。通过这些不同的事件类型的通知,能够很好的帮助上层应用程序在不明确Chubby会话状态的情况,能够很好的根据不同的事件类型来作出不同的处理:等待或重启。对于那些Chubby服务短时间不可用的场景下,这样的机制使得客户端应用程序可以在出现问题的情况下选择等待,而不是重启,这对于那些重启整个应用程序需要花费较大代价的应用场景来说,非常有帮助。

如果会话过期,那么客户端对其持有的句柄进行的任何操作都将失败,当然除了closepoison操作,这就保证了在出现网络和Chubby服务器不可用的情况下的数据一致性。

2.9       故障恢复

当一个Master崩溃了,或者是失去了Master权力,那么它就会丢弃所有内存中的会话、句柄和锁信息。在Master上会运行着会话租期计时器,用来管理所有会话的生命周期。如果Master出现故障,那么该计时器会停止,知道新的Master选举产生后,计时器才会继续计时,也就是说,从旧的Master崩溃到新的Master选举产生所花费的时间将不计入会话超时的计算中,这等价于延长了客户端的会话租期。如果新的Master在短时间内就选举产生了,那么客户端就可以在本地会话租期过期前与其创建连接。而如果Master的选举花费了较长的时间,以致于客户端的只能清空本地的缓存,并进入宽限期进行等待。从这里我们可以看出,由于宽限期的存在,是的会话能够很好的在服务端Master转换的过程得到维持。

wKiom1QNe4SDfh3sAAE729_02cA778.jpg

2展示了一个完整的故障恢复过程中的所触发的所有事件序列。在这整个恢复过程中,客户端必须使用宽限期来保证在这个过程之后,其会话依然有效。在上面这个示意图中,从左向右代表了时间的增长,使用粗箭头代表客户端的会话租期,并且在上图分别通过“M”和“C”来标记会话租期在Master和客户端上的视图,例如“lease M1”和“lease C1”。斜向上的箭头代表了客户端向Master发出的KeepAlive请求,而斜向下的箭头则代表了MasterKeepAlive响应。从上面这个图中我们可以看出,一开始在旧的Master上维持了会话租期“lease M1”,同时在客户端上维持了对应的“lease C1”,客户端的KeepAlive请求1一直被Master阻塞。之后Master向客户端反馈了KeepAlive响应2,同时开始了新的会话租期“lease M2”,客户端接收到该KeepAlive响应后,立即发送新的KeepAlive请求3,并同时也开始新的会话租期“lease C2”。

至此,客户端和服务端Master之间的所有交互都是正常的。但是随后,Master崩溃了,已经无法反馈客户端的KeepAlive请求3了。在一段时间之后,新的Master选举产生了。在这个过程中,客户端的会话租期“lease C2”过期了,它会清空本地缓存,并进入宽限期。

在这段时间内,客户端无法确定Master上的会话周期是否已经过期,因此不会销毁它的本地会话,而是将所有应用程序对它的API调用都阻塞住,以避免在这个期间进行的API调用导致数据不一致现象。在客户端宽限期开始的时候,Chubby会向应用程序发送一个“jeopardy”事件。

最终,一个新的Master选举产生,并为客户端初始化了新的会话租期“lease M3”。当客户端向新的Master发送KeepAlive请求4时,Master检测到该客户端的Master周期号(master epoch number)已经过期,因此会在KeepAlive响应5中拒绝这个客户端请求,关于Master周期,将在后面的章节中做详细的讲解。之后,客户端会携带上新的Master周期号,再次发送KeepAlive请求6Master,之后整个客户端和服务端之间的会话就会再次恢复正常。

Master崩溃到新的Master选举产生的这段时间内,只要客户端的宽限期足够长,那么客户端应用程序就可以在没有任何察觉的情况下,实现故障恢复,但如果客户端的宽限期设置的比较短的话,那么客户端就会丢弃该会话,并将这个失败通知给上层应用程序。

一旦客户端与新的Master建立上连接后,客户端和Master之间会通过互相配合来实现对故障的平滑恢复。为实现这个效果,新的Master必须将之前Master的内存状态构造出来。具体的,Chubby可以通过读取本地磁盘上的数据来恢复一部分状态,本地数据库记录了每个客户端的会话信息,以及其持有的锁和临时文件。之后,再通过从客户端获取相应的状态来完成一部分状态,此外,Chubby还会通过假设来完成另一部分状态。

一个新的Master选举产生后,会经过如下几个步骤:

1.      新的Master选举产生后,会首先确定本次的Master周期(master epoch number),Master周期用来唯一标识Chubby集群的Master统治轮次,以便区分不同的Master。一旦新的Master周期确定下来之后,Master就会拒绝所有使用之前的Master周期编号的客户端请求,同时告知其最新的Master周期编号,例如上述提到的KeepAlive请求4。需要注意的一点是,但凡发生Master重新选举后,就会产生新的Master周期,即使碰到选举前后Master都是同一个机器的情况。

2.     选举产生的新Master能够对客户端的Master寻址请求进行响应,但是不会立即开始处理客户端会话相关的操作。

3.     Master根据本地数据库中存储的会话和锁信息,来构建服务器的内存状态。

4.     到现在为止,Master已经能够处理客户端的KeepAlive请求,但还是不能处理其他会话相关的操作。

5.     Master会发送一个“故障切换”事件给每一个会话,客户端接收到这个事件后,会清空它们的本地缓存,并警告上层应用程序可能已经丢失了别的事件。

6.     在每一个会话都应答了这个切换事件,或者所有的客户端都使其本地会话过期之前,Master会一直等待。

7.     之后,Master就能够处理所有的请求操作了。

8.     如果客户端使用了一个在故障切换之前创建的句柄,Master会重新为其创建了这个句柄的内存印象,并执行调用。如果这个重建后的句柄被关闭了,那么Master会在内存中记录它,这样它就不能在这个Master周期内再次被重建了。这一机制就确保了由于网络原因,Master在接收到那些延迟的或者是重发的网络包时不会错误的重建一个已经关闭的句柄。

9.     由于Master会在经过一段时间(比如一分钟)之后,清除掉那些没有打开的文件句柄的临时文件。因此在故障切换后,客户端需要在这段时间内刷新它们在临时文件上创建的句柄。

2.10    数据库实现

在最初版本的Chubby中,使用了具有数据复制特性的Berkeley DB[4](下文中我们简称“BDB”)来作为它的数据库。BerkeleyDB的底层实现采用了B树,可以将其看成是一个能够存储大量数据的HashMap。在Chubby的使用中,将每一个数据节点的节点路径名作为键,同时按照节点路径名进行排序,这样就能够使得兄弟节点在排序顺序中相邻。

BDB使用分布式一致性协议来进行集群中不同服务器之间数据库日志的复制。因此Chubby的设计变得非常简单,只需要而在此基础上添加上Master租期特性即可。

但是在后来的开发维护过程中,Chubby的开发人员觉得使用BDB就会引入其他额外的风险和依赖,因此自己实现一套更为简单的,基于日志预写和数据按照技术的底层数据复制组件。这样就大大简化了整个Chubby的系统架构和实现逻辑。

2.11    备份

每隔几小时,Chubby集群中的Master服务器就会将它的数据库快照写到GFS上去,注意,此处用于备份数据的GFS必须是一个和当前Chubby集群毫无相关的文件系统,例如分布在不同的机房,同时,还需要注意的一点的,这个GFS必须有自己的Chubby依赖,而不是使用当前的Chubby,那样的话就会导致循环依赖了。备份机制一方面提供了一种Chubby容灾恢复的方法,同时,当Chubby集群中添加新的服务器时,新机器也可以从GFS上进行数据的初始化,而不是一味的从集群中其他服务器上同步数据,这也减轻了其他服务器的压力。


[1] Google文件系统,即GFS,是Google开发的一种面向廉价服务器架构的大型分布式文件系统,读者可以通过阅读Google2003年发表的论文《The Google File System》了解更多关于这一分布式文件系统的技术内幕。

[2] Bigtable,是Google开发的一种用于进行结构化数据存储与管理的大型分布式存储系统,读者可以通过阅读Google2006年发表的论文《Bigtable: A Distributed Storage System for Structured Data》了解更多关于这一分布式存储系统的技术内幕。

[3]相关参考资料包括DAVID R. JEFFERSON的《Virtual Timehttp://masters.donntu.edu.ua/2012/fknt/vorotnikova/library/virtual_time.pdfKenneth P.BirmanThomas A. Joseph的《Exploiting Virtual Synchrony in Distributed Systemshttp://www.cs.cornell.edu/home/rvr/sys/p123-birman.pdf

[4] Berkeley DB是一个历史非常悠久的嵌入式数据库系统,与2006年被Oracle收购,其官方主页是:http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/overview/index.html










  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值