癸巳年正月初三。本篇博文是本次“癸巳春节分布技术系列”的尾声--介绍分布式锁。分布系统总是在某种场景下需要确定顺序、决出主次的,此时就要用到分布式锁。通常来说这种行为开销比较大,在应用处理逻辑中应该较少地使用。近年来,由于Google对Paxos的成功实现-Chubby,以及Hadoop家族的ZooKeeper的广泛应用,现在搞分布系统的技术工作者如果不了解Paxos算法,可能都不好意思和人打招呼。本文不能免俗,会将Chubby和ZooKeeper的原理做概况介绍,然而在提及Pasox算法之前,我还会介绍一种更传统的分布式锁模型DLM,这种模型在规模较小的分布系统的共享文件系统中得到了很好的应用,也在一些分布式交易系统中发挥着重要的作用。
-WJF,2013年2月12日(三)分布式锁
1、传统分布式锁
分布式锁管理器DLM最早由VMS操作系统提供。首次出现在Digitial于1982年发布的VMS版本3中。从1984年发布的VMS版本4之后,其对用户提供的应用编程接口基本确定。2006年,开源操作系统Linux的版本2.6.19内核正式集成了与VMS DLM接口几乎一致的DLM。同年,Google通过论文公开了其分布式锁服务Chubby的设计。之后,Yahoo公开了其分布式锁服务ZooKeeper的源代码。
Linux上的DLM需要基于Corosync机群引擎才能运行。DLM为整个集群中所有计算机提供一个统一的、共享的锁。每个计算机运行一个锁管理核心守护进程,这些守护进程之间相互通信,维护一个集群范围内的“数据库”,管理锁资源和拥有这些锁资源的锁。
在这个集群范围内的“数据库”,锁管理器维护一份每个锁资源的主拷贝。初始化时,主拷贝驻留在锁初始请求的计算机上。之后的运行过程中,主拷贝可能驻留在任意一个计算机中。为此,锁管理器维护一个集群范围的目录,说明集群内所有锁资源的主拷贝的位置。同时,锁管理器试图均匀地把这个目录分布到所有的集群计算机节点上。这种设计下,当一个应用申请一个锁或者锁资源,锁管理器首先决定哪一个计算机节点拥有该锁资源的目录条目,之后通过读目录条目找出哪一个计算机节点拥有该锁资源的主拷贝。
通过允许所有计算机都能维护锁资源的主拷贝的方式,当锁请求可以在本地处理时,降低了采用一个集群只在一个计算机上设计一个锁管理器可能带来的网络通信流量,以及减少了在设备故障发生后重建锁数据库所需的时间,并提高了锁请求处理的吞吐量。
当一个计算机发生故障,运行在其他计算机上的锁管理器释放该故障计算机所持有的锁。之后,锁管理器处理之前被该故障计算机阻塞的其他计算机的锁请求。同时,其他计算机会对故障计算机之前维护的主拷贝进行重新分配。
锁管理器通过锁资源的名称来区分锁。一个锁资源可以与多个锁关联,既在锁资源与锁之间具有一对多的关系。锁管理器提供6种上锁模式,通过转换动作来在不同模式之间进行升级或者降级,并且提供同步和异步执行的编程接口。
锁具有如下若干种模式:空NL(Null),这个模式并不对资源进行访问,通常用来作为对资源“感兴趣”的标志,或者作为未来锁转换的占位符。并发读CR (Concurrent Read),表示对资源进行读访问,并且允许其他访问者对该资源也进行共享读和写。这个模式通常用来进行细粒度的锁操作,或者从一个资源进行“不保护”的读取。并发写CW(Concurrent Write),表示对资源进行写访问,并且允许其他访问者对该资源也进行共享写。这个模式通常用来进行细粒度的锁操作,或者向一个资源进行“不保护”的写入。保护读PR(Protected Read),表示对资源进行保护读,其他访问者对该资源可进行共享读,但是不可写。保护写PW(Protected Write),表示对资源进行保护写,允许其他访问者用并发读模式来共享读。但是不可写。专属EX(Exclusive),表示对资源进行独占,任何其他访问者都不可访问。
这些模式的等级从最松到最严的关系是:NL;CR;CW、PR;PW;EX。其中CW和PR的等级是相同的。新申请的锁可以与原来的锁共享被称为“兼容”。
对于一个调用进程,锁状态表明请求某个锁的当前状态。锁管理器根据新请求的模式与该锁资源上的其他模式之间的关系来确定锁状态。锁可能处在如下3个状态:已授予-锁请求成功,达到请求的模式。转换中-新模式与旧模式不兼容。已阻塞-新模式与旧模式冲突。锁管理器还有一个功能,可通过“锁取值块”在不同计算机节点之间共享一小块全局数据。
锁管理器在内部设计时,把锁资源定义为一个可上锁的实体,该实体包括一个名字:资源名;一块内存:锁取值块;三个队列:授予队列、转换队列、等待队列。锁管理器在应用程序第一次申请锁时创建锁资源实体,在最后一个锁被释放时释放锁资源实体。
对于指定的锁资源,如果当前没有任何一把锁与之相关,或者新申请的模式与该资源授予队列中所有锁中最严格模式都兼容,同时转换队列为空,那么锁管理器就把锁授予申请者,并把该锁增加到授予队列中。
对已经拥有的锁,可以发出改变模式的请求,请求即可以从低等级模式向高等级模式转换,也可从高等级模式向低等级模式转换。需要注意的是,只有已经授予的锁才可以进行转换,不允许转换正在转换中的锁或者被阻塞在等待队列中的锁。
在模式从低等级向高等级转换时,如果请求转换的目标模式与授予队列中所有锁中最严格模式兼容,同时没有阻塞的转换请求等候在转换队列中,那么锁管理器就授予该请求者锁。如果请求转换的模式与授予队列中已经授予的最严格模式不兼容,那么将该锁从授予队列移到转换队列的队尾。
对于指定的锁资源,如果有锁位于转换队列中,那么在处理该资源相关锁的所有的向高等级转换的请求时,直接将该申请锁从授予队列移动到转移队列的队尾,并且不再判断申请转换的模式是否与授予队列中最严格的模式兼容。
一把锁进入转换状态后,如果发生下列三种情况之一,状态会改变为授予或者取消:(1)申请该锁的进程中止,(2)原申请进程取消转换请求,(3)请求模式与授予队列中最严格模式都兼容且排在队列之前的申请都已经授予或者取消。
在模式从高等级向低等级转换时,转换过程非常简单,由于新申请模式必然和授予队列中最严格模式兼容,所以锁管理器不去判断转换队列中是否有其他锁,而是直接修改授予队列中对应锁的模式。之后,锁管理器会按照先进先出的原则处理转换队列和阻塞队列中的锁,检查是否可以授予(由于优先级关系,一旦队列中遇到一个锁不可以授予,那么检查过程中止 )。
如果请求转换的目标模式与授予队列中所有锁中最严格模式不兼容,那么请求被阻塞,锁管理器把阻塞的锁从授予队列移入阻塞队列的队尾。一把锁进入阻塞状态后,如果发生下列三种情况之一,会停止等待:(1)申请该锁的进程中止,(2)原申请进程取消转换请求,(3)请求模式与授予队列中最严格模式都兼容,转换队列没有排队的锁,阻塞队列内没有排在该锁之前的锁。
2、Paxos锁
Paxos协议是Leslie Lamport于1990年提出的一种针对分布式系统,基于消息传递的一致性算法。Leslie Lamport 在1985年到2001年间曾就职于开发VMS系统的Digitial公司和Compaq公司。Paxos协议是高可用且去中心化的,系统被视作由一组完全对等的节点组成,这组节点各自就某一事件做出决议,如果某个决议获得了超过半数节点的同意则生效。也就是说Paxos协议中只要有超过一半的节点正常,就可以工作,所以能够应对宕机、网络分化等异常情况。
Paxos算法中把专门节点按照角色分为三类:提议者(Proposer)、接收者(Acceptor)和学习者(Learner)。提议者提出提案值(Proposal Value),告诉系统接下来处理什么指令。接收者批准提案,半数接收者批准才算通过,通过之后的提案称为决议(Agreed Value)。学习者获取并使用已经通过的决议,学习者也必须至少读取半数以上接收者的数据才算学习到一个决议。这三类角色只是逻辑上的,系统中的一个物理节点可以同时充当这三类角色。需要满足三个条件就能保证整个系统数据的一致性:(1)决议只有在提议者提出后才能批准(2)每次只批准一个决议(3)只有决议确定被批准后学习者才能获取这个决议。在批准决议时,采用少数服从多数的原则,也就是大多数接收者接收的决议才能成为最终正式的决议。这里隐含着集合论的一个原则,两组多数派至少有一个公共的接收者。
Paxos算法被分成了两个阶段:准备阶段和批准阶段。准备阶段中,提议者选择一个提案并把编号(Proposal Number)设为n,然后发给接收者。接收者收到后,如果提案的编号大于它已经回复的所有消息,则接收者将自己上次的批准消息,也称为承诺(Promise)消息,回复给提议者,并不再批准小于n的提案。批准阶段中,当提案者接收到接收者中的多数节点的回复后,就向回复承诺消息的接收者发送接收(Accept)请求消息。在符合接收者一方的约束条件下,接收者收到这个接收请求消息后即批准这个请求,并发回一个接收响应(Accepted)消息。为了减少决议发布过程中的消息量,接收者将这个通过的决议发送给学习者的一个子集,然后由这个子集中的学习者去通知所有其他的学习者。有一种特殊情况,如果两个提议者在遇到编号更大的提案后,都转而提出一个编号更大的提案,那么就有可能进入“活锁”。此时,需要选举出一个总统(President),仅允许总统提出提案。
(1)Chubby
与1982年即现雏形的DLM不同,Chubby通过Paxos协议对节点进行主从选择。一旦某个节点获得了超过半数的节点认可,该节点成为主(Primary)节点,其余节点成为从(Secondary)节点。在选举出主节点后,所有读写操作都由主节点控制,系统从一个完全对等的去中心化状态变为一个主从的中心化状态。这样,可以在一个无中心的分布式系统上,实现中心化的副本控制协议。
Chubby除了实现Paxos协议之外,还采用了租约(Lease)机制。1989年斯坦福大学的Cary G.Gray 和 David R. Cheriton提出了利用租约来维护缓存一致性的方法,通过使用它,可以确保非拜占庭式的失效只会影响到性能,但是不会破坏正确性,同时通过使用短租约可以将这种影响减少到最低。租约机制中定义租约以及租约的颁发者和持有者,在颁发者和持有者之间建立约定的一致承诺。特别要注意的是,持有者在约定的有效期满后,一定不能继续使用颁发者的承诺。
租约机制考虑了分布系统中复杂的网络故障与节点故障场景。即便持有者由于网络分化没有收到租约,颁发者也会在约定时间内执行自己的承诺。如果在持有者收到租约后出现网络分化,也不影响双方对承诺理解的一致性。如果颁发者宕机,持有者可以继续使用租约。在颁发者恢复后,如果颁发者能够恢复之前的租约,双方可继续遵守承诺。如果颁发者无法恢复之前的租约,那么只需要等待租约到期,收回之前的承诺。
租约机制从早期设计中用来维护缓存一致性持续发展,在分布式系统设计中得到广泛应用。承诺的内容也多种多样,可以是缓存中数据的正确性,还可以是某种权限。Cubby中,从节点向主节点发送租约,承诺的内容就是权限,指在约定时间内,不选举其他节点成为主节点。只要主节点持有超过半数节点的租约,那么剩余的节点就不能选举出新的主节点。一旦主节点宕机,剩余的从节点由于不能向主节点发送租约,将发起新的一轮 paxos 选举,选举出新的主节点。
同时,Chubby还用租约机制来控制主节点和客户(Client)节点。主节点向每个客户节点颁发租约,用来判断客户节点的状态。一个客户节点只有拥有合法的租约,才能与主节点进行读写操作。一个客户节点如果占有Chubby中的一个节点锁后租约超时,那么这个客户节点占有的锁会被自动释放,从而实现对节点状态进行监控的功能。
综上所述,通过Chubby,用户可以确保数据操作过程中的一致性。但是,与DLM不同的是,这种锁只是建议性的锁(Advisory Lock),不是强制性的锁(Mandatory Lock)。Google通过Chubby构造了丰富的分布应用,包括Google文件系统GFS(Google File System)、BigTable等。
(2)ZooKeeper
Zookeeper使用的是修改后的Paxos协议。Zookeeper中的角色名称为:领导者(Leader)、跟随者(Follower)、观察者(Observer)。领导者对应Chubby中的主节点(Primary),跟随者对应Chubby中的从节点(Secondary)。状态为Leading、Following、Observing和Looking。与Paxos协议相比,观察者是新增的,在大多数情况下,行为与跟随者一致,不过其不参加选举和投票,只是观察(oberving)选举和投票的结果。引入观察者的目的是为了解决系统规模扩大后,网络复杂性可能带来的拜占庭问题。
Zookeeper把系统中状态的转换分为两个阶段,一个阶段是领导者激活阶段(Leader activation),此时系统中没有领导者,通过一个类似Paxos协议的过程选出领导者,选出领导者并同步数据后,转入另外一个阶段-活跃消息阶段(Active messaging)。处在活跃消息阶段时,领导者接收客户端发送的更新操作,在各个跟随者节点上进行更新操作。如果活跃消息阶段领导者发生异常,系统转入领导者激活阶段,重新选举领导者。
Zookeeper定义了一个全局版本号-zxid。zxid由epoch和count两部分组成。 epoch是选举编号,每次提议进行新的领导者选举时epoch都会增加,每一个领导者对应一个唯一的epoch。count是领导者为每个更新操作决定的序号,每个 领导者任期内产生的更新操作对应一个唯一的有序的 count。这样的方式,使得整个分布系统有一个代表了更新操作的全局序号。
每个节点都有各自最后提交的zxid。在领导者激活阶段,每个节点都以自己的 zxid与节点编号nodeid组合后一起作为 Paxos 中的提案值发起 Paxos协议,设置自己作为领导者。每个节点既是提案者又是接受者,所以每个节点只会接受提案编号大于自身zxid的提案。通过paxos协议过程,某个超过半数的节点中持有最大的zxid的节点会成为新的领导者。虽然zxid可能相同,但是由于节点编号不同,这个选举过程不会出现所有节点都以相同b参数作为提案的场景,从而避免了无法选出领导者的可能。成为新领导者的节点与跟随者进行数据同步,当与至少半数节点完成数据同步后,领导者更新epoch,各个跟随者更新领导者信息,以(epoch + 1, 0)为 zxid 写一条NEW_LEADER 消息。 当领导者收到超过半数的跟随者对 NEW_LEADER 的确认后,领导者发起对 NEW_LEADER的提交操作,并进入活跃消息状态。数据同步过程可能会涉及 删除跟随者的最后一条脏数据。
进入活跃消息状态的领导者会接收从客户端发来的更新操作,为每个更新操作生成递增的count,组成递增的zxid。领导者将更新操作以zxid 的顺序发送给各个跟随者和自身,当收到超过半数的更新者的确认后,领导者发送针对该更新操作的提交消息给各个跟随者。这个更新操作的过程与船艇的两阶段提交很类似,但是进行了简化,领导者不会对更新操作做废弃(abort)操作。
如果领导者不能更新超过半数的跟随者,说明这个领导者处在分裂的网络中偏小的部分,最后一条更新操作处于“中间状态”,其是否生效取决于另外一部分节点选举出的新领导是否有该条更新操作。
总之,由于同一时刻只有一个节点能获得超过半数的跟随者,所以同一时刻最多只存在唯一的领导者,每个领导者顺序更新各个跟随者,只有成功完成前一个更新操作的才会进行下一个更新操作,使得在同一个领导者“任期”内,读超过半数的节点一定可以读到最新已提交的数据,每个成功的更新操作都至少被超过半数的节点确认,故障发生后新选举的领导者一定可以包括最新的已成功提交的数据。
与Chubby中从节点向主节点发送租约不同,Zookeeper 中的跟随者并不向领导者发送租约,Zookeeper中的跟随者发现没有领导者则发起新的Paxos 选举。与 Chubby 类似的是,Zookeeper的领导者向客户发送租约,当一个客户超时,那么这个客户创建的临时节点会被自动删除。