Google Chubby 学习: 基本概念

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

概述

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

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

应用场景

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

Chubby 技术架构

系统结构

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

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

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

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

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

在 Chubby 运行过程中,服务器难免会发生故障,如果当前的 Master 服务器崩溃了,那么集群中的其他服务器会在 Master 租期到期后,重新开始新一轮 Master 选举。而如果是任意一台非 Master 服务器崩溃,那么整个集群是不会停止工作的,这个崩溃的服务器会在恢复之后自动加入 Chubby 集群中。

如果集群中的一个服务器发送崩溃并在几小时后仍然无法恢复正常,那么就需要加入新的机器,并同时更新 DNS 列表。

目录与文件

Chubby 的数据结构可以看作是一个由文件和目录组成的树,其中每一个节点都可以表示为一个使用斜杠分割的字符串,典型的节点路径如下:

/ls/foo/wombat/pouch

其中 ls 是所有 Chubby 节点所共有的前缀,代表着锁服务;foo 则指定了 Chubby 机器的名字;剩余部分 /wombat/pouch 则是一个真正包含业务含义的节点名字,由 Chubby 服务器内部解析并定位到数据节点。

锁与锁序列器

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

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

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

Chubby 中的事件通知机制

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

文件内容变更

BigTable 集群使用 Chubby 锁来确定集群中的哪台 BigTable 机器是 Master;获得锁的 BigTable Master 会将自身信息写入 Chubby 上对应的文件中。 BigTable机器中的其他客户端可以通过监视这个 Chubby 文件的变化来确定新的 BigTable Master 机器。

节点删除

当 Chubby 上指定节点被删除的时候,会产生“节点删除”事件,通常是临时节点,可以利用该特性来间接判断该临时节点对应的客户端会话是否有效。

子结点新增、删除

当 Chubby 上指定节点的子节点新增或是减少时,会产生”子节点新增、删除“事件。

Master 服务器转移

当 Chubby 服务器发生 Master 转移时,会以事件的形式通知客户端。

Chubby 中的缓存

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

Chubby 保证缓存一致性的主要方法是使用了缓存的生命周期这一概念。具体的,每个客户端的缓存都有一个租期,一旦该租期到期,客户端就需要向服务端续订租期以继续维持缓存的有效性。当文件数据或元数据信息被修改时,Chubby服务端首先会阻塞该修改操作,然后由 Master 向所有可能缓存了该数据的客户端发送缓存过期信号,以使其缓存失效,等到 Master 在接收到所有相关客户端针对该过期信号的应答后(1. 客户端明确要求更新缓存;2. 客户端允许缓存租期过期。),再继续进行之前的修改操作。

会话和会话激活

Chubby 客户端和服务端之间通过创建一个 TCP 连接来进行所有的网络通信操作,我们将这一连接称为会话(Session)。会话有生命周期,并且存在一个超时时间,在超时时间内,Chubby客户端和服务端之间可以通过心跳检测来保持会话的活性。

Chubby Master 故障恢复

Chubby 的 Master 服务器上会运行着会话租期计时器,用来管理所有会话的生命周期。如果在运行过程中 Master 出现了故障,那么该计时器会停止,直到新的 Master 选举产生后,计时器才会继续计时,

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

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