Chubby的锁服务

最近在完成Zeppelin的中心节点重构的过程中,反思了我们对分布式锁的实现和使用。因此重读了Chubby论文The Chubby lock service for loosely-coupled distributed systems,收益良多的同时也对其中的细节有了更感同身受的理解,论文中将众多的设计细节依次罗列,容易让读者产生眼花缭乱之感。本文希望能够更清晰的展现Chubby的设计哲学和实现方式,以及带给我们的思考和启发。首先介绍Chubby的定位和设计初衷,这也是Chubby众多细节的目标和本质;之后从一致性、锁的实现和锁的使用三个方面介绍Chubby作为分布式锁的设计和实现;最后总结一些Chubby对我们设计开发分布式系统的一般性的经验和启发。

定位

Chubby的设计初衷是为了解决分布式系统中的一致性问题,其中最常见的就是分布式系统的选主需求及一致性的数据存储。Chubby选择通过提供粗粒度锁服务的方式实现:

Chubby provide coarse-grained locking as well as reliable storage for a loosely-coupled distributed system.

这里的粗粒度(Coarse-grained)锁服务相对于细粒度(Fine-grained)锁服务,指的是应用加锁时间比较长的场景,达到几个小时或者几天。Chubby的三个重要的设计目标是:可靠性(reliability)、可用性(availability)、易于理解(easy-to-understand),除此之外,一致性当然也是锁服务的立命之本。这些就是稍后会提到的各种设计细节所追求的目标。对于为什么选择锁服务,而不是一致性库或者一致性服务的问题,作者总结了如下几点:

分布式锁

分布式锁是Chubby的设计初衷,我们这里就以分布式锁来展开其设计实现,Chubby的结构如下图所示:

System structure

我们认为分布式锁的问题其实包含三个部分,分别是一致性协议、分布式锁的实现、分布式锁的使用。三个部分自下而上完成了在分布式环境中对锁需求,下面我们就将从这三个方面介绍Chubby的设计。

Lock

1, 一致性协议

一致性协议其实并不是锁需求直接相关的,假设我们有一个永不宕机的节点和永不中断的网络,那么一个单点的存储即可支撑上层的锁的实现及使用。但这种假设在互联网环境中是不现实的,所以才引入了一致性协议,来保证我们可以通过副本的方式来容忍节点或网络的异常,同时又不引起正确性的风险,作为一个整体对上层提供高可用的服务。

Chubby采用的是一个有强主的Multi-Paxos,其概要实现如下:

具体的Paxos实现可以参考论文Paxos Made Simple,在这里我们只需要把它近似看做一个不会宕机不会断网的节点,能保证所有成功写入的操作都能被后续成功的读取读到。

2,分布式锁的实现

这部分是Chubby实现的重点,为了更好的梳理这部分的脉络,我们先看看Chubby提供的API以及给Client的使用机制,他们一起组成了Chubby对外的接口;之后介绍锁的实现;最后结合Chubby对读写请求比例,可用性,Corase-Lock等定位引出的Cache,Session及故障恢复等内部机制

接口

Chubby的对外接口是外部使用者直接面对的使用Chubby的方式,是连接分布式锁的实现及使用之间的桥梁:

锁实现

每一个File或者Directory都可以作为读写锁使用,接受用户的Aquire,Release等请求。锁依赖下层的一致性服务来保证其操作顺序。Chubby提供的是Advisory Lock的实现,相对于Mandatory Lock,由于可以访问加锁Node的数据而方便数据共享及管理调试。分布式锁面对的最大挑战来自于客户端节点和网络的不可靠,Chubby提供了两种锁实现的方式:

1),完美实现:

2),简易实现:

对比两种实现方式,简易版本可以使用在无法检查Sequencer的场景从而更一般化,但也因为lock-delay的设置牺牲了一定的可用性,同时需要用户在业务层面保证lock-delay之后不会再有依赖锁保护的操作。

Cache

从这里开始要提到的Chubby的机制是对Client透明的了。Chubby对自己的定位是需要支持大量的Client,并且读请求远大于写请求的场景,因此引入一个对读请求友好的Client端Cache,来减少大量读请求对Chubby Master的压力便十分自然,客户端可以完全不感知这个Cache的存在。Cache对读请求的极度友好体现在它牺牲写性能实现了一个一致语义的Cache:

Session and KeepAlive

Session可以看做是Client在Master上的一个投影,Master通过Session来掌握并维护Client:

Session及KeepAlive给了Chubby Server感知和掌握Client存活的能力,这对锁的实现也是非常重要的,因为这给了Master一个判断是否要释放失效Lock的时机。最后总结下,这些机制之间的关系,如下图:

Chubby Mechansim

故障恢复

Master发生故障或脱离集群后,它锁维护的Session信息会被集群不可见,一致性协议会选举新的Master。由于Chubby对自己Corase Lock的定位,使用锁的服务在锁的所有权迁移后会有较大的恢复开销,这也就要求新Master启动后需要恢复必要的信息,并尽量减少集群停止服务过程的影响:

Failed Over

3, 分布式锁的使用

锁的使用跟上面提到的锁的实现是紧密相关的,由于客户端节点及网络的不可靠,即使Chubby提供了直观如Aquire,Realease这样的锁操作,使用者仍然需要做出更多的努力来配合完成锁的语义,Chubby论文中以一个选主场景对如何使用锁给出了详细的说明,以完美方案为例:

如果是简单方案,则不需要Sequencer,但需要在Aquire操作时指定lock-delay,并保证所有需要锁保护的操作会在最后一次Session刷新后的lock-delay时间内完成。

启发

暂且抛开Chubby对分布式锁的实现,本质上Chubby是一个在分布式环境中提供服务的系统。其在复杂性控制,可用性,可靠性,可扩展性等方面作出的努力和思考对我们其他系统的设计开发也是很有指导和借鉴意义的,下面列举一些进行说明。

1,责任分散

分布式系统中,通常都会有多个角色进行协作共同完成某个目标,有时候合理的将某些功能的责任分散到不同角色上去,分散到不同时间去,会起到降低复杂度,减少关键节点压力的效果。比如Chubby中发生写事件需要更新Client Cache时,Master并没有尝试自己去更新所有的Client,而是简单的Invalid所有Client的Cache,这样就将更新所有Client Cache这项任务分散到所有的客户端上,分散到后边一次次的请求时机中去。这种推变拉的做法也是Zeppelin中大量使用的。

2,考虑可扩展时,减少通信次数有时候比优化单次请求处理速度更有效

Chubby作为一个为大量Client提供服务的中心节点,并没有花过多的精力在优化单条请求路径上,而是努力地寻找可以减少Client与Master通信的机制:

3,限制资源的的线性增长

论文中提到对Chubby使用资源情况的检查,包括RPC频率、磁盘空间、打开文件数等。任何可能随着用户数量或数据量的增加而线性增加的资源都应该有机制通过降级操作限制在一个合理的范围内,从而提供更加健壮的服务。负载较重时延长Session租约时间及存储配额的设置应该就是这方面的努力。

Any linear growth must be mitigated by a compensating parameter that can be adjusted to reduce the load on Chubby to reasonable bounds

4,故障恢复时的数据恢复

为了性能或负载,Master不可能将所有需要的信息全部通过一致性协议同步到所有副本。其内存维护的部分会在故障发生时丢失,新的Master必须能尽可能的恢复这些数据来让外部使用者尽量少的感知到故障的发生。恢复的数据来源方面,Chubby做了一个很好的范例:

参考

The Chubby lock service for loosely-coupled distributed systems

Talk about consensus algorithm and distributed lock

Paxos Made Simple

Qihoo360 Zeppelin

Table of Contents