Google Chubby 是一个大名鼎鼎的分布式锁服务,GFS和Big Table等大型系统都用他来解决分布式协作、元数据存储和Master选择等一系列与分布式锁服务相关的问题。Chubby的底层一致性实现就是以Paxos算法为基础的。

概述

        Chubby是一个面向松耦合分布式系统的锁服务,通常用于为一个由大量小型计算机构成的松耦合分布式系统提供高可用的分布式锁服务。一个分布式锁服务的目的是允许他的客户端进程同步彼此的操作,并对当前所处环境的基本状态信息达成一致。针对这个目的,Chubby提供了粗粒度的分布式锁服务,开发人员不需要使用复杂的同步协议,而是直接调用Chubby的锁服务接口即可实现分布式系统中多个进程之间粗粒度的同步控制,从而保证分布式数据的一致性。

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

应用场景

        在Chubby的众多应用场景中,最为典型的就是集群中服务器的Master选举。例如在Google文件系统(Google File System,GFS)中使用Chubby锁服务来实现对GFS Master服务器的选举。而在BigTable 中,Chubby同样被用于Master选举,并且借助Chubby,Master能够非常方便的感知到其所控制的那些服务器。同时,通过Chubby,BigTable的客户端还能够方便的定位到当前Bigtable集群的Master服务器。此外,在GFS和Bigtable中,都使用Chubby来进行系统运行时元数据的存储。

设计目标

        对于Chubby的设计,有的开发人员觉得作为Paxos算法的实现者,Chubby应该构建成一个包含Paxos算法的协议库,从而使应用程序能够便捷的使用Paxos算法。但是Chubby的最初设计者并没有这样做,而是将Chubby设计成一个需要访问中心化节点的分布式锁服务。

        Chubby之所以设计成这样一个完整的分布式锁服务,是因为锁服务具有以下4个传统算法库所不具有的优点。

  • 对上层应用程序的侵入性更小
  • 便于提供数据的发布与订阅
  • 开发人员对基于锁的接口更为熟悉
  • 更便捷的构建更可靠的服务

        因此,Chubby被设计成一个需要访问中心化节点的分布式锁服务。同时,在Chubby的设计过程中,提出了以下几个设计目标。

  • 提供一个完整的、独立的分布式锁服务,而非仅仅是一个一致性协议的客户端库
  • 提供粗粒度的锁服务
  • 在提供锁服务的同时提供对小文件的读写功能
  • 高可用、高可靠
  • 提供事件通知机制

Chubby技术架构

系统结构

        Chubby的整个系统结构主要由服务端和客户端两部分组成,客户端通过RPC调用与服务端进行通信,如下图所示。

        一个典型的Chubby集群,或称为Chubby cell,通常由5台服务器组成。这些副本服务器采用Paxos协议,通过投票的方式来选举产生一个获得过半投票的服务器作为Master。一旦某台服务器成为了Master,Chubby就会保证在一段时间内不会再有其他服务器称为Master——这段时间被称为Master租期(Master Lease)。在运行过程中,Master服务器会通过不断续租的方式来延长Master租期,而如果Master服务器出现故障,那么余下的服务器就会进行新一轮的Master选举,最终产生新的Master服务器,开始新的Master租期。

        集群中的每个服务器都维护着一份服务端数据库的副本,但在实际运行过程中,只有Master服务器才能对数据库进行写操作,而其他服务器都是使用Paxos协议从Master服务器上同步数据库数据的更新。

        现在,我们再看下Chubby的客户端是如何定位到Master服务器的。Chubby客户端通过诸葛发起请求询问该服务器是否是Master。在这个询问过程中,那些非Master的服务器,则会将当前Master所在服务器标识反馈给客户端,这样客户端就能够非常快速地定位到Master服务器了。

        一旦客户端定位到Master 服务器之后,只要该Master正常运行,那么客户端就会将所有的请求都发送到该Master服务器上。针对写请求,Chubby Master会采用一致性协议将其广播给集群中所有的副本服务器,并且在过半的服务器接受了该写请求之后,再响应给客户端正确的应答。而对于读请求,则不需要在集群内部进行广播处理,直接由Master服务器单独处理即可。

        在Chubby运行过程中,服务器难免会发生故障。如果当前的Master服务器崩溃了,那么集群中的其他服务器会在Master租期到期后,重新开启新一轮的Master选举。通常,进行一次Master选举大概需要花费几秒钟的时间。而如果是集群中任意一台非Master服务器崩溃,那么整个集群是不会停止工作的,这个崩溃的服务器会在恢复之后自动加入到Chubby集群中去。新加入的服务器首先需要同步Chubby最新的数据库数据,完成数据同步之后,新的服务器就可以加入到正常的Paxos运行流程中与其他服务器副本一起协同工作。

        如果集群中的一个服务器发生崩溃并在几小时后仍无法恢复正常,那么就需要加入新的机器,并同时更新DNS列表。Chubby服务器的更换方式非常简单,只需要启动Chubby服务端程序,然后更新DNS上的机器列表(即使用新机器的IP地址替换老机器的IP地址)即可。在Chubby运行过程中,Master服务器会周期性的轮询DNS列表,因此其很快就会感知到服务器地址列表的变更,然后Master就会将集群数据库中的地址列表做相应的变更,集群内部的其他副本服务器通过复制方式就可以获取到最新的服务器地址列表了。

目录与文件

        Chubby对外提供了一套与Unix文件系统非常相近但是更简单的访问接口。Chubby的数据结构可以看作是一个由文件和目录组成的树,其中每一个节点都可以表示为一个使用斜杠分割的字符串,典型的节点路径表示如下:

/ls/foo/wombat/pouch

        其中,ls是所有Chubby节点所共有的前缀,代表着锁事务,是Lock Service的缩写;foo 则指定了 Chubby 集群的名字,从DNS 可以查询到由一个或多个服务器组成该Chubby集群;剩余部分的路径/wombat/pouch则是一个真正包含业务含义的节点名字,由Chubby服务器内部解析并定位到数据节点。

        Chubby的命名空间,包括文件和目录,我们称之为节点。在同一个Chubby集群数据库中,每一个节点都是全局唯一的。和Unix系统一样,每个目录都可以包含一系列的子文件和子目录列表,而每个文件中则会包含文件内容。当然,Chubby并非模拟一个完整的文件系统,因此没有符号链接和硬连接的概念。

        由于Chubby的命名空间结构组成了一个近似标准文件系统的视图,因此Chubby的客户端应用程序也可以通过自定义的文件系统访问接口来访问Chubby服务端数据,比如可以使用GFS的文件系统访问接口,这就大大减少了用户使用Chubby的成本。

        Chubby上的每个数据节点都分为持久节点和临时节点两大类,其中持久节点需要显示的调用接口API来进行删除,而临时节点则会在其对应的客户端会话失效后被自动删除。也就是说,临时节点的生命周期和客户端会话绑定,如果该临时节点对应的文件没有被任何客户端打开的话,那么他就会被删除掉。因此,临时节点通常可以用来进行客户端会话有效性的判断依据。

        另外,Chubby上的每个数据节点都包含了少量的元数据信息,其中包括用于权限控制的访问控制列表(ACL)信息。同时,每个节点的元数据中还包括4个单调底层的64位编号,分别如下。

  • 实例编号
  • 文件内容编号
  • 锁编号
  • ACL编号

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

锁与锁序列器

        在分布式系统中,锁是一个非常复杂的问题,由于网络通信的不确定性,导致在分布式系统中锁机制变得非常复杂,消息的延迟或是乱序都有可能会引起锁的失效。一个典型的分布锁错乱案例是,一个客户端C1获取到了互斥锁L,并且在锁L的保护下发出请求R,但请求R迟迟没有到达服务端(可能出现网络延时或反复重发等),这时应用程序会认为该客户端进程已经失败,于是便会为另一个客户端C2分配锁L,然后再重新发起之前的请求R,并成功的应用到了服务器上。此时,不幸的事情发生了,客户端C1发起的请求R在经过一波三折之后也到达了服务端,此时,他有可能会在不受任何锁控制的情况下被服务端处理,从而覆盖了客户端C2的操作,于是导致系统数据出现不一致。当然,诸如此类消息接收顺序紊乱引起的数据不一致问题已经在人们对分布式计算的长期研究工程中得到了很好的解决,典型的解决方案包括虚拟时间和虚拟同步。

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

        在Chubby中,主要采用锁延迟和锁序列器两种策略来解决上面我们提到的由于消息延迟和重排序引起的分布式锁问题。其中锁延迟是一种比较简单的策略,使用Chubby的应用几乎不需要进行任何的代码修改。具体的,如果一个客户端以正常的方式主动释放了一个锁,那么Chubby服务端将会允许其他客户端能够立即获取到该锁。而如果一个锁是因为客户端的异常情况(如客户端无响应)而被释放的话,那么Chubby服务器会为该锁保留一定的时间,我们称之为“锁延迟”(lock-delay),在这段时间内,其他客户端无法获取这个锁。所延迟措施能够很好的防止一些客户端由于网络闪断等原因而与服务器暂时断开的场景出现。总的来说,该方案尽管不完美,但是锁延时能够有效地保护在出现消息延时情况下发生的数据不一致现象。

        Chubby提供的另一种方式是使用锁序列器,当然该策略需要Chubby的上层应用配合在代码中加入相应的修改逻辑。任何时候,锁的持有者都可以向Chubby请求一个锁序列器,其包括锁的名字、锁模式(排他或共享模式),以及锁序号。当客户端应用程序在进行一些需要锁机制保护的操作时,可以将该锁序列器一并发送给服务端。Chubby服务端接收到这样的请求后,会首先检测该序列器是否有效,以及检查客户端是否处于恰当的锁模式;如果没有通过检查,那么服务端就会拒绝该客户端请求。

Chubby中的时间通知机制

        为了避免大量客户端轮询Chubby服务端状态所带来的压力,Chubby提供了事件通知机制。Chubby的客户端可以向服务端注册时间通知,当触发这些事件的时候,服务端就会向客户端发送对应的事件通知。在Chubby的事件通知机制中,消息通知都是通过异步的方式发送给客户端的,常见的Chubby事件如下。

  • 文件内容变更
  • 节点删除
  • 子节点新增、删除
  • Master服务器转移

Chubby中的缓存

        为了提高Chubby的性能,同时也是为了减少客户端和服务端之间频繁的读请求对服务端的压力,Chubby除了提供事件通知机制之外,还在客户端中实现了缓存,会在客户端对文件内容和元数据信息进行缓存。使用缓存机制在提高系统整体性能的同时,也为系统带来了一定的复杂性,其中最主要的问题就是应该如何保证缓存的一致性。在Chubby中,通过租期机制来保证缓存的一致性。

        Chubby缓存的生命周期和Master租期机制紧密关联,Master会维护每个客户端的数据缓存情况,并通过向客户端发送过期信息的方式来保证客户端数据的一致性。在这种机制下,Chubby就能够保证客户端要么能够从缓存中访问到一致的数据,要么访问出错,而一定不会访问到不一致的数据。具体的,每个客户端的缓存都有一个租期,一旦该租期到期,客户端就需要向服务端续订租期以继续维持缓存的有效性。当文件数据或元数据系你先被修改时,Chubby服务端首先会阻塞该修改操作,然后由Master向所有可能缓存了该数据的客户端发送缓存过期信号,以使其缓存失效,等到Master在接收到所有相关客户端针对该过期信号的应答(应答包括两类,一类是客户端明确要求更新缓存,另一类则是客户端允许缓存租期过期)后,再继续进行之前的修改操作。

        Chubby的缓存数据保证了强一致性。尽管要保证严格的数据一致性对于性能的开销和系统的吞吐影响最大,但由于弱一致性模型在实际使用过程中极容易出现问题,因此Chubby在设计之初就决定了选择强一致性模型。

会话和会话激活(KeepAlive)

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

KeepAlive请求

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

        除了为客户端进行会话续租外,Master还将通过KeepAlive响应来传递Chubby事件通知和缓存过期通知给客户端。具体的,如果Master发现服务端已经触发了针对该客户端的事件通知或缓存过期通知,那么会提前将KeepAlive响应反馈给客户端。

会话超时

        谈到会话租期,Chubby的客户端也会维持一个和Master端近似相同的会话租期。为什么是近似相同呢?这是因为客户端必须考虑两方面的因素:一方面,KeepAlive响应在网络传输过程中会花费一定的时间;另一方面,Master服务端和Chubby客户端存在时钟不一致性现象。因此在Chubby会话中,存在Master端会话租期和客户端本地会话租期。

         如果Chubby客户端在运行过程中,按照本地的会话租期超时时间,检测到其会话租期已经过期却尚未接收到Master的KeepAlive响应,那么这个时候,他将无法确定Master服务端是否已经终止了当前会话,我们称这个时候客户端处于“危险状态”。此时,Chubby客户端会清空其本地缓存,并将其标记为不可用。同时,客户端还会等待一个被称作“宽限期”的时间周期,这个宽限期默认是45秒。如果在宽限期到期前,客户端和服务端之间成功的进行了KeepAlive,那么客户端就会再次开启本地缓存,否则,客户端就会认为当前会话已经过期了,从而中止本次会话。

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

Chubby Master 规章恢复

        Chubby的Master服务器上会于宁着会话租期计时器,用来管理所有会话的生命周期。如果在运行过程中Master出现了故障,那么该计时器会停止,直到新的Master选举产生后,计时器才会继续计时,也就是说,从旧的Master崩溃到新的Master选举产生所花费的时间将不计入会话超时的计算中,这等价于延长了客户端的会话租期。如果新的Master在短时间内就选举产生了,那么客户端就可以在本地会话租期过期前与其创建连接。而如果Master的选举花费了较长的时间,就会导致客户端只能清空本地的缓存,并进入宽限期进行等待。从这里我们可以看出,由于宽限期的存在,使得会话能够很好地在服务端Master转换的过程中得到维持。整个Chubby Master故障恢复过程中服务端和客户端的交互情况如下图所示。

        上图展示了一个完整的Chubby服务端Master故障恢复过程中所触发的所有事件序列。在整个故障恢复过程中,客户端必须使用宽限期来保证在Master转换过程完成之后,其会话依然有效。在图中,从左向右代表了时间轴的变化,使用横向粗箭头代表会话租期,并且在图中通过“M”和“C”来分别标记Master和客户端上的视图,例如“lease M1” 和 “lease C1”。斜向上的箭头代表了客户端向Master发出的KeepAlive请求,而斜向下的箭头则代表了Master反馈的KeepAlive响应。

        从上图中我们可以看出,一开始在旧的Master服务器上维持了会话租期“lease M1”,在客户端上维持了对应的 “lease C1”,同时客户端的KeepAlive请求 1 一直被Master 阻塞着。在一段时间之后,Master向客户端反馈了KeepAlive响应2,同时开始了新的会话租期“lease M2”,而客户端在接收到该KeepAlive响应之后,立即发送了新的KeepAlive请求3,并同时也开始了新的会话租期“lease C2”。至此,客户端和服务段Master之间的所有交互都是正常的。但是随后,Master发生了故障,从而无法反馈客户端的KeepAlive 请求3.在这个过程中,客户端检测到会话租期“lease C2” 已经过期,他会清空本地缓存,并进入宽限期。在这段时间内,客户端无法确定Master上的会话周期是否也已经过期,因此他不会销毁他的本地会话,而是将所有应用程序对他的API调用都阻塞住,以避免在整个期间进行的API调用导致数据不一致现象。同时,在客户端宽限期开始时,Chubby客户端会向上层应用程序发送一个“jeopardy”事件。一段时间之后,Chubby服务端选举产生了新的Master,并为该客户端初始化了新的会话租期“lease M3”。当客户端向新的Master发送KeepAlive请求4时,Master检测到该客户端的Master周期号(Master epoch number)已经过期,因此会在KeepAlive响应5中拒绝这个客户端请求,并将最新的Master周期号发送给客户端。之后,客户端会携带上新的Master周期号,再次发送KeepAlive请求6给Master,最终,整个客户端和服务端之间的会话就会再次恢复正常。

        通过上面的详细介绍,不难看出,在Master转换的这段时间内,只要客户端的宽限期足够长,那么客户端应用程序就可以在没有任何察觉的情况下,实现Chubby的故障恢复,但如果客户端的宽限期设置得比较短,那么Chubby客户端就会丢弃当前会话,并将这个异常情况通知给上层应用程序。

        一旦客户端与新的Master建立上连接之后,客户端和Master之间会通过互相配合来实现对故障的平滑恢复。新的Master会设法将上一个Master服务器的内存状态构造出来。具体的,由于本地数据库记录了每个客户端的会话信息,以及其持有的锁和临时文件等信息,因此Chubby会通过读取本地磁盘的数据来恢复一部分状态。总的来讲,一个新的Chubby Master 服务器选举产生之后,会进行如下几个主要处理。

  1. 新的Master选举产生后,首先需要确定Master周期。Master周期用来唯一标识一个Chubby集群的Master统治轮次,以便区分不同的Master。一旦新的Master走起确定下来之后,Master就会拒绝所有携带其他Master周期编号的客户端请求,同时告知其最新的Master周期编号,例如上述提到的 KeepAlive 请求 4.需要注意的一点是,只要发生Master重新选举,就一定会产生新的Master周期,即使是在选举前后Master都是同一台机器的情况下也是如此。
  2. 选举产生的新Master能够立即对客户端的Master寻址请求进行响应,但是不会立即开始处理客户端会话相关的请求操作。
  3. Master 根据本地数据库中存储的会话和锁信息,来构建服务器的内存状态。
  4. 到现在为止,Master已经能够处理客户端的KeepAlive请求了,但依然无法处理其他会话相关的操作。
  5. Master会发送一个“Master 故障切换”事件给每一个会话,客户端接收到这个事件后,会清空他的本地缓存,并警告上层应用程序可能已经丢失了别的事件,之后再向Master反馈应答。
  6. 此时,Master会一直等待客户端的应答,直到每一个会话都应答了这个切换事件。
  7. 在Master接收到了所有客户端的应答之后,就能够开始处理所有的请求操作了。
  8. 如果客户端使用了一个在故障切换之前创建的句柄,Master会重新为其创建这个句柄的内存对象,并执行调用。而如果该句柄在之前的Master周期中已经被关闭了,那么他就不能在这个Master周期再次被重建了——这一机制就确保了即使由于网络原因使得Master接收到那些延迟或重发的网络数据包,也不会错误的重建一个已经关闭的句柄。

Paxos协议实现

        Chubby服务端的基本架构大致分为三层:

  • 最底层是容错日志系统(Fault-Tolerant Log),通过Paxos算法能够保证集群中所有机器上的日志完全一致,同时具备较好的容错性。
  • 日志层之上是Key-Value类型的容错数据库(Fault-Tolerant DB),其通过下层的日志来保证一致性和容错性。
  • 存储层之上就是Chubby对外提供的分布式锁服务和小文件存储服务。

        其整体架构如下图所示。

        Paxos算法的作用就在于保证集群内各个副本节点的日志能够保持一致。Chubby事务日志中的每一个Value对应Paxos算法中的一个Instance,由于Chubby需要对外提供不间断的服务,因此事务日志会无限增长,于是在整个Chubby运行过程中,会存在多个Paxos Instance。同时,Chubby会为每一个Paxos Instance都按序分配一个全局唯一的Instance 编号,并将其顺序写入到事务日志中去。

        在多Paxos Instance的模式下,为了提升算法执行的性能,就必须选举出一个副本节点作为Paxos算法的主节点(以下简称Master或Coordinator),以避免因为每一个Paxos INstance 都提出提案而陷入多个Paxos Round并存的情况。同时,Paxos会保证在Master重启或出现故障而进行切换的时候,允许出现短暂的多个Master共存却不影响副本之间的一致性。

        在Paxos中,每一个PaxosInstance都需要进行一轮或多轮“Prepare→Promise→Propose→Accept”这样完整的二阶段请求过程来完成对一个提案值的选定,而多个Instance之间是完全独立的,每个Instance可以自己决定每一个Round的序号,尽尽只需要保证在Instance 内部不会出现序号重复即可。为了在保证正确性的前提下尽可能的提高算法运行性能,可以让多个Instance共用一套序号分配机制,并将“Prepare→Promise”合并为一个阶段,具体做法如下。

  • 当某个副本节点通过选举成为Master后,就会使用新分配的编号N来广播一个Prepare消息,该Prepare消息会被所有未达成一致的Instance和目前还未开始的Instance共用。
  • 当Acceptor接收到Prepare消息后,必须对多个Instance同时做出回应,这通常可以通过将反馈信息封装在一个数据包中来实现。假设最多允许K个Instance同时进行提案值的选定,那么:
  • 当前至多存在K个未达成一致的Instance,将这些未决的Instance各自最后接受的提案值(若该提案尚未接受任何值,则使用null来代替)封装进一个数据包,并作为Promise消息返回。
  • 同时,判断N是否大于当前Acceptor的highestPromisedNum值(当前已经接受的最大提案编号值),如果大于该值的话,那么就标记这些未决Instance和所有未来的Instance的highestPromisedNum值为N——这样,这些未决Instance和所有未来Instance都不能再接受任何编号小于N的提案。
  • 然后Master就可以对所有未决Instance和所有未来Instance分别执行“Propose→Accept”阶段的处理。值得注意的是,如果当前Master能够一致稳定运行的话,那么在接下来的算法运行过程中,就不再需要进行“Prepare→Promise”的处理了。但是一旦Master发现Acceptor返回了一个Reject消息,说明集群汇总存在另一个Master,并且试图使用更大的提案编号(比如M,其M>N)发送了Prepare消息。碰到这种情况,当前Master就需要重新分配新的提案编号(必须比M更大)并再次进行“Prepare→Promise”阶段的逻辑处理。

        利用上述改进的Paxos算法,在Master稳定运行的情况下,只需要使用同一个编号来依次执行每一个Instance的“Promise→Accept”阶段逻辑处理。在每个Instance的运行过程中,一旦接收到多数派的Accept反馈后,就可以将对应的提案值写入本地事务日志并广播Commit消息给集群中的其他副本节点,其他副本节点在接收到这个COMMIT消息之后也会将提案值写入到事务日志中去。如果某个副本节点因为宕机或者网络原因没有接收到COMMIT消息,可以主动向集群中的其他副本节点进行查询。因此,我们可以看到,在Chubby的Paxos算法的实现中,只要维持集群中存在多数派的机器能够正常运行,即使其他机器在任意时刻发生宕机,也能保证已经提交的提案的安全性。

        至此,我们已经实现了一套满足一致性的日志副本,在此基础上就可以在上层实现一个一致的状态机副本,这就是上图的容错数据库(Fault-Tolerant DB)层。在最初版本的Chubby中,使用了具有数据复制特性的Berkeley DB(下文简称“BDB”)来作为他的容错数据库。BDB使用分布式一致性协议来进行集群中不同的服务器之间数据库日志的复制。BDB的底层实现采用了经典的B树数据结构,我们可以将其看作是一个能够存储大量数据的HashMap(映射)。在Chubby的使用中,将每一个数据节点的节点路径名作为键,同时按照节点路径名进行排序,这样就能够使得兄弟节点在排序顺序中相邻,方便对数据节点的检索。因此Chubby的设计变得非常简单,只需要在此基础上添加上Master租期特殊即可。

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

        集群中的某台机器在宕机重启以后,为了恢复状态机的状态,最简单的方法就是将已经记录的所有事务日志重新执行一遍。但这会有一个明显的问题,就是如果机器上的事务日志已经积累了很多,那么恢复的时间就会非常长,因此需要定期对状态机数据做一个数据快照并将其存入磁盘,然后就可以将数据快照点之前的事务日志清除。

        通常副本节点在进行宕机后的恢复过程中,会出现磁盘未损坏和损坏两种情况。前者最为常见,一般通过磁盘上保存的数据库快照和事务日志就可以恢复到之前某个时间点的状态,之后再向集群中其他正常运行的副本节点索取宕机后缺失的部分数据变更记录,这样即可实现宕机后的数据恢复。另外一种则是磁盘损坏,无法直接从本地数据恢复的情况。针对这种异常情况。就需要从其他副本节点上索取全部的状态数据。

        副本节点在完成宕机重启之后,为了安全起见,不会立即参与Paxos Instance流程,而是需要等待检测到K(K是允许并发的最大Instance数目)个Paxos Instance流程成功完成之后才能开始参与——这样就能够保证新分配的提案编号不会和自己以前发过的重复。

        最后,为了提高整个集群的性能,还有一个改进之处在于:得益于Paxos算法的容错机制,只要任意时刻保证多数派的机器能够正常运行,那么在宕机瞬间未能真正写入到磁盘上(只有当真正调用操作系统Flush接口后,数据才能被真正写入物理磁盘中)的那一小部分事务日志也可以其他正常运行的副本上复制来进行获取,因此不需要实时地进行事务日志的Flush操作,这可以极大地提高事务写入的效率。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐