一、前言

在上一篇理解了Paxos算法的理论基础后,接下来看看Paxos算法在工程中的应用。

二、Chubby

Chubby是一个面向松耦合分布式系统的锁服务,GFS(Google File System)和Big Table等大型系统都是用它来解决分布式协作、元数据存储和Master选举等一些列与分布式锁服务相关的问题。Chubby的底层一致性实现就是以Paxos算法为基础,Chubby提供了粗粒度的分布式锁服务,开发人员直接调用Chubby的锁服务接口即可实现分布式系统中多个进程之间粗粒度的同控制,从而保证分布式数据的一致性。

2.1 设计目标

Chubby被设计成为一个需要访问中心化的分布式锁服务

① 对上层应用程序的侵入性更小,使用一个分布式锁服务的接口方式对上层应用程序的侵入性更小,应用程序只需调用相应的接口即可使用分布式一致性特性,并且更易于保持系统已有的程序结构和网络通信模式。

② 便于提供数据的发布与订阅,在Chubby进行Master选举时,需要使用一种广播结果的机制来向所有客户端公布当前Master服务器,这意味着Chubby应该允许其客户端在服务器上进行少量数据的存储和读取(存储主Master地址等信息),也就是对小文件的读写操作。数据的发布与订阅功能和锁服务在分布式一致性特性上是相通的。

③ 开发人员对基于锁的接口更为熟悉,Chubby提供了一套近乎和单机锁机制一直的分布式锁服务接口。

④ 更便捷地构建更可靠的服务,Chubby中通常使用5台服务器来组成一个集群单元(Cell),根据Quorum机制(在一个由若干个机器组成的集群中,在一个数据项值的选定过程中,要求集群中过半的机器达成一致),只要整个集群中有3台服务器是正常运行的,那么整个集群就可以对外提供正常的服务。

⑤ 提供一个完整的、独立的分布式锁服务,Chubby对于上层应用程序的侵入性特别低,对于Master选举同时将Master信息等级并广播的场景,应用程序只需要向Chubby请求一个锁,并且在获得锁之后向相应的锁文件写入Master信息即可,其余的客户端就可以通过读取这个锁文件来获取Master信息。

⑥ 提供粗粒度的锁服务,Chubby针对的应用场景是客户端获得锁之后会进行长时间持有(数小时或数天),而非用于短暂获取锁的场景。当锁服务短暂失效时(服务器宕机),Chubby需要保持所有锁的持有状态,以避免持有锁的客户端出现问题。而细粒度锁通常设计为为锁服务一旦失效就释放所有锁,因为其持有时间很短,所以其放弃锁带来的代价较小。

⑦ 高可用、高可靠,对于一个由5太机器组成的集群而言,只要保证3台正常运行的机器,整个集群对外服务就能保持可用,另外,由于Chubby支持通过小文件读写服务的方式来进行Master选举结果的发布与订阅,因此在Chubby的实际应用中,必须能够支撑成百上千个Chubby客户端对同一个文件进行监控和读取。

⑧ 提供时间通知机制,Chubby客户端需要实时地感知到Master的变化情况,这可以通过让你客户端反复轮询来实现,但是在客户端规模不断增大的情况下,客户端主动轮询的实时性效果并不理想,且对服务器性能和网络带宽压力都非常大,因此,Chubby需要有能力将服务端的数据变化情况以时间的形式通知到所有订阅的客户端。

2.2 技术架构

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

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

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

Chubby客户端通过向记录有Chubby服务端机器列表的DNS来请求获取所有的Chubby服务器列表,然后逐一发起请求询问该服务器是否是Master,在这个询问过程中,那些非Master的服务器,则会将当前Master所在的服务器标志反馈给客户端,这样客户端就能很快速的定位到Master服务器了。

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

若该Master服务器发生故障,那么集群中的其他服务器会在Master租期到期后,重新开启新的一轮Master选举,通常一次Master选举大概花费几秒钟的时间,而如果其他副本服务器崩溃,那么整个集群继续工作,该崩溃的服务器会在恢复之后自动加入到Chubby集群中去,新加入的服务器首先需要同步Chubby最新的数据库数据,完成数据库同步之后,新的服务器就可以加入到正常的Paxos运作流程中与其他服务器副本一起协同工作。若崩溃后几小时后仍无法恢复工作,那么需要加入新的机器,同时更新DNS列表(新IP代替旧IP),Master服务器会周期性地轮询DNS列表,因此会很快感知服务器地址的变更,然后Master就会将集群数据库中的地址列表做相应的变更,集群内部的其他副本服务器通过复制方式就可以获取到最新的服务器地址列表了。

2.3 目录与文件

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

/ls/foo/wombat/pouch

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

Chubby的命名空间,包括文件和目录,我们称之为节点(Nodes,我们以数据节点来泛指Chubby的文件或目录)。在同一个Chubby集群数据库中,每一个节点都是全局唯一的。和Unix系统一样,每个目录都可以包含一系列的子文件和子目录列表,而每个文件中则会包含文件内容。Chubby没有符号链接和硬连接的概念。

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

Chubby的每个数据节点都包含了少量的元数据信息,其中包括用于权限控制的访问控制列表(ACL)信息,同时每个节点的元数据还包括4个单调递增的64位标号:

① 实例标号,用于标识创建该数据节点的顺序,节点的创建顺序不同,其实例编号也不相同,可以通过实例编号来识别两个名字相同的数据节点是否是同一个数据节点,因为创建时间晚的数据节点,其实例编号必定大于任意先前创建的同名节点。

② 文件内容编号(针对文件),用于标识文件内容的变化情况,该编号会在文件内容被写入时增加。

③ 锁编号,用于标识节点锁状态的变更情况,该编号会在节点锁从自由状态转化到被持有状态时增加。

 ACL编号,用于标识节点的ACL信息变更情况,该编号会在节点的ACL配置信息被写入时增加。

2.4 锁和锁序列器

分布式环境中锁机制十分复杂,消息的延迟或是乱序都有可能引起锁的失效,如客户端C1获得互斥锁L后发出请求R,但请求R迟迟没有到达服务端(网络延迟或消息重发等),这时应用程序会认为该客户端进程已经失败,于是为另一个客户端C2分配锁L,然后在发送请求R,并成功应用到了服务器上。然而,之前的请求R经过一波三折后也到达了服务器端,此时,它可能不瘦任何锁控制的情况下被服务端处理,而覆盖了客户端C2的操作,也是导致了数据不一致问题。

在Chubby中,任意一个数据节点均可被当做一个读写锁来使用:一种是单个客户端排他(写)模式持有这个锁,另一种则是任意数目的客户端以共享(读)模式持有这个锁,Chubby放弃了严格的强制锁,客户端可以在没有获取任何锁的情况下访问Chubby的文件。

Chubby采用了锁延迟锁序列器两种策略来解决上述由于消息延迟和重排序引起的分布式锁问题,对于锁延迟而言,如果一个客户端以正常的方式主动释放了一个锁,那么Chubby服务端将会允许其他客户端能够立即获取到该锁;如果锁是以异常情况被释放的话,那么Chubby服务器会为该锁保留一定的时间,这称之为锁延迟,这段时间内,其他客户端无法获取该锁,其可以很好的防止一些客户端由于网络闪断的原因而与服务器暂时断开的场景。对于锁序列器而言,其需要Chubby的上层应用配合在代码中加入相应的修改逻辑,任何时候,锁的持有者都可以向Chubby请求一个锁序列器,其包括锁的名字、锁模式(排他或共享)、锁序号,当客户端应用程序在进行一些需要锁机制保护的操作时,可以将该锁序列器一并发送给服务端,服务端收到该请求后,会首先检测该序列器是否有效,以及检查客户端是否处于恰当的锁模式,如果没有通过检查,那么服务端就会拒绝该客户端的请求。

2.5 事件通知机制

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

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

2.6 缓存

其是为了减少客户端与服务端之间的频繁的读请求对服务端的压力设计的,会在客户端对文件内容和元数据信息进行缓存,虽然缓存提高了系统的整体性能,但是其也带来了一定复杂性,如如何保证缓存的一致性。其通过租期机制来保证缓存的一致性。

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

2.7 会话

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

2.8 KeepAlive请求

Master服务端在收到客户端的KeepAlive请求时,首先会将该请求阻塞住,并等到该客户端的当前会话租期即将过期时,才为其续租该客户端的会话租期,之后再向客户端响应这个KeepAlive请求,并同时将最新的会话租期超时时间反馈给客户端,在正常情况下,每个客户端总是会有一个KeepAlive请求阻塞在Master服务器上。

2.9 会话超时

客户端也会维持一个和Master端近似相同(由于KeepAlive响应在网络传输过程中会花费一定的时间、Master服务端和客户端存在时钟不一致的现象)的会话租期。客户端在运行过程中,按照本地的会话租期超时时间,检测到其会话租期已经过期却尚未收到Master的KeepAlive响应,此时,它将无法确定Master服务端是否已经中止了当前会话,这个时候客户端处于危险状态,此时,客户端会清空其本地缓存并将其标记为不可用,同时客户端还会等待一个被称作宽限期的时间周期,默认为45秒,若在宽限期到期前,客户端与服务端之间成功地进行了KeepAlive,那么客户端就会再次开启本地缓存,否则,客户端就会认为当前会话已经过期了,从而终止本次会话。

在客户端进入危险状态时,客户端会通过一个“jeopardy”事件来通知上层应用程序,如果恢复正常,客户端同样会以一个“safe”事件来通知应用程序可以继续正常运行,但如果客户端最终没能从危险状态中恢复过来,那么客户端会以一个“expired”事件来通知应用程序当前Chubby会话已经超时。

2.10 Master故障恢复

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

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

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

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

① 确定Master周期。Master周期用来唯一标识一个Chubby集群的Master统治轮次,以便区分不同的Master,只要发生Master重新选举,就一定会产生新的Master周期,即使选举前后Master是同一台机器。

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

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

④ 至此,Master已经能够处理客户端的KeepAlive请求,但仍然无法处理其他会话相关的操作。

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

⑥ 此时,Master会一直等待客户端的应答,直到每一个会话都应答了这个切换事件。

⑦ Master接收了所有客户端的应答之后,就能够开始处理所有的请求操作。

⑧若客户端使用了一个故障切换之间创建的句柄,Master会重新为其创建这个句柄的内存对象,并执行调用,如果该句柄在之前的Master周期中就已经被关闭了,那么它就不能在这个Master周期内再次被重建了,这一机制就确保了由于网络原因使得Master接收到那些延迟或重发的网络数据包也不会错误的重建一个已经关闭的句柄。

三、Paxos协议实现

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

① 最底层是容错日志系统(Fault-Tolerant Log),通过Paxos算法能够保证集群所有机器上的日志完全一致,同时具备较好的容错性。

② 日志层之上是Key-Value类型的容错数据库(Fault-Tolerant DB),其通过下层的日志来保证一致性和容错性。

③ 存储层之上的就是Chubby对外提供的分布式锁服务和小文件存储服务。

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

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

在Paxos中,每一个Paxos Instance都需要进行一轮或多轮的Prepare->Promise->Propose->Accept这样完整的二阶段请求过程来完成对一个提议值的选定,为了保证正确性的前提下尽可能地提高算法运行性能,可以让多个Instance共用一套序号分配机制,并将Prepare->Promise合并为一个阶段。具体做法如下:

① 当某个副本节点通过选举成为Master后,就会使用新分配的编号N来广播一个Prepare消息,该Prepare消息会被所有未达成一致的Instance和目前还未开始的Instance共用。

② 当Acceptor接收到Prepare消息后,必须对多个Instance同时做出回应,这通常可以通过将反馈信息封装在一个数据包中来实现,假设最多允许K个Instance同时进行提议值的选定,那么:

-当前之多存在K个未达成一致的Instance,将这些未决的Instance各自最后接受的提议值封装进一个数据包,并作为Promise消息返回。

-同时,判断N是否大于当前Acceptor的highestPromisedNum值(当前已经接受的最大的提议编号值),如果大于,那么就标记这些未决Instance和所有未来的Instance的highestPromisedNum的值为N,这样,这些未决Instance和所有未来Instance都不能再接受任何编号小于N的提议。

③ Master对所有未决Instance和所有未来Instance分别执行Propose->Accept阶段的处理,如果Master能够一直稳定运行的话,那么在接下来的算法运行过程中,就不再需要进行Prepare->Promise处理了。但是,一旦Master发现Acceptor返回了一个Reject消息,说明集群中存在另一个Master并且试图使用更大的提议编号发送了Prepare消息,此时,当前Master就需要重新分配新的提议编号并再次进行Prepare->Promise阶段的处理。

利用上述改进的Paxos算法,在Master稳定运行的情况下,只需要使用同一个编号来依次执行每一个Instance的Promise->Accept阶段处理。

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

在恢复过程中,会出现磁盘未损坏和损坏两种情况,若未损坏,则通过磁盘上保存的数据库快照和事务日志就可以恢复到之前的某个时间点的状态,之后再向集群中其他正常运行的副本节点索取宕机后缺失的部分数据变更记录即可;若磁盘损坏,就笑从其他副本节点索取全部的状态数据。

四、总结

本部分详细介绍了Chubby系统,并且对Paxos在Chubby中的使用也做了较为详细的介绍,后面我们将会介绍开源分布式系统Zookeeper与Paxos的联系。本部分理论知识较强(文字较多),需要慢慢研究才能理解其中的含义,谢谢各位园友的观看~

Leave a Reply

Your email address will not be published. Required fields are marked *