分布式锁服务关键技术和常见解决方案 ( 下)

《分布式锁服务关键技术和常见解决方案 ( 上)》

3、基于分布式一致性算法实现的锁服务

​ 分析完上述的锁服务方案,可以看到,各种方案核心还是在一致性和可用性之间做取舍。对于锁服务本身的定位和用途而言,其是一个相对中心化,对数据一致性有严格要求的场景。所以在分布式环境下,把数据严格一致性作为第一要求的情况下,Paxos算法是绕不开的一个算法(“all working protocols for asynchronous consensus we have so far encountered have Paxos at their core”)。于是就有了Chubby和Zookeeper这类,基于分布式一致性算法(核心是Paxos和相关变种)实现的锁服务。 ​ Chubby是由Google开发实现,在其内部使用的一个分布式锁服务,其核心设计Google通过论文的形式开源出来。而Zookeeper作为Chubby的开源实现版本,由开源社区开发,目前也广泛应用在各种场景下。由于Zookeeper和Chubby之间的关系,两者在绝大部分的设计上都十分相似,因此此部分以Chubby为例,来分析此类锁服务的相关特点,关于Zookeeper和Chubby设计上的差异,在本节最后简要分析。 ​

3.1 Chubby设计细节

Chubby核心架构

​ 图3.Chubby的系统结构 ​ 如上图,一个典型的Chubby集群,或者叫Chubby Cell,通常由5台服务器(奇数台)组成。这些服务器之间采用Paxos协议,通过投票方式决定一个服务器作为Master。一旦一个服务器成为Master,Chubby会保证一段时间其他服务器不会成为Master,这段时间被称为租期。在运行过程中,Master服务器会不断续租,如果Master服务器发生故障,余下的服务器会选举新的Master产生新的Master服务器。 ​ Chubby客户端通过DNS发现Chubby集群的地址,然后Chubby客户端会向Chubby集群询问Master服务器IP,在询问过程,那些非Master服务器会将Master服务器标识反馈给客户端,可以非常快的定位到Master。 ​ 在实际运行中,所有的读写请求都发给Master。针对写请求,Chubby Master会采用一致性协议将其广播到所有副本服务器,并且在过半机器接受请求后,再响应客户端。对于读请求,Master服务器直接处理返回。 ​ Chubby的一致性协议是Paxos算法的工程实现,对于Paxos协议本身,由于不是此文的重点,所以此处不展开详细介绍。总体上,可以理解为Chubby的一致性协议,可以保证通过Master写成功之后的数据,最终会扩散到集群内的所有机器上,同时对于多次的写操作,Chubby可以严格保证时序(无论是Master挂掉重新选举产生新Master,还是其中非Master机器的故障或者是被替换),另外从Master读取的数据也是最新的数据。而满足这一切要求的前提,只需要Chubby集群中的大部分机器可以正常提供服务即可。 ​ 每台Chubby服务器的基本架构大致分三层: ​

​ 图4.Chubby结点的基本架构 ​ 1、最底层是容错日志系统,通过Paxos协议保证集群上的日志完全一致。 ​ 2、日志之上是KV类型的容错数据库,通过下层的日志来保证一致性和容错性。 ​ 3、最上层是对外提供的分布式锁服务和小文件存储服务。 Chubby数据组织形式 ​ Chubby作为分布式锁服务,提供的数据操作接口是类似于Unix文件系统接口风格的接口,这样设计的初衷据说是文件系统操作风格的接口在Google内部更加符合使用者的习惯。Chubby中所有的数据都是以文件结点的形式提供给调用者访问,Chubby中典型的结点如下:/ls/foo/wombat/pouch。 ​ 结点分为永久结点和临时结点,临时结点在没有客户端打开或者其子目录下已经为空的情况下自动删除。每个结点都可以设置访问控制权限(ACL),同时结点的原数据(MeteData)中有四个递增的64位数,用于区分结点在各个方面的修改时序:1、实体编号:区分同名结点的先后;2、文件内容编号:文件内容修改时自增;3、锁编号:锁结点被获取时自增;4、ACL编号:ACL变化时自增。 ​ 基于文件结点的组织形式,Chubby提供的数据操作API如下: ​ 1、Open():打开文件结点; ​ 2、Close():关闭文件结点; ​ 3、GetContentsAndStat():获取文件内容; ​ 4、SetContents():写文件,可以同时提供文件内容编号,Chubby验证文件目前的序号和提供的一致,方能修改; ​ 5、Delete():删除文件(需要文件无子节点); ​ 6、 Acquire(), TryAcquire(), Release():获取和释放锁; ​ 7、 GetSequencer():加锁成功后,获取锁序号; ​ 8、 CheckSequencer():检验锁序号是否有效。 ​

Chubby事件机制

​ 为了避免大量客户端轮询服务器带来的压力,Chubby提供了事件通知机制。Chubby客户端可以向Chubby注册事件通知,当触发了这些事件后服务端就会向客户端发送事件通知。Chubby支持的事件类型包括不限于: ​ 1、文件内容变化:通常用于监控文件是否被其他客户端修改; 2、子节点新增、删除、变化:用于监控文件结点的变化; 3、Master故障:警告客户端可能已经无法再收到其他事件了,需要重新检查数据; 4、文件句柄已经失效:通知客户端目前使用的文件句柄已经失效,通常是网络问题。 ​

3.2 Chubby加锁流程

​ 结合上述Chubby的设计细节,Chubby中客户端完成加锁的操作序列如下: ​ 1、 所有客户端打开锁文件(Open),尝试获取锁(Acquire)。 ​ 2、 只有一个客户端成功,其他失败。 ​ 3、 成功的客户端获得了锁,可以写自己的相关信息到文件(SetContent),其他客户端可以读取到锁持有者的信息(GetContentsAndStat)(可以通过订阅事件,也可以通过加锁失败后去读取结果)。 ​ 4、 获取锁失败的客户端,可以随后轮询再试,也可以通过订阅事件,等待锁释放后Chubby的主动通知。 ​ 5、 加锁成功的客户端从Master获取一个序列号(GetSequencer),然后在与后端资源服务器的通信的时候带上此序列号,后端资源服务器处理请求前,调用CheckSequence去检查锁是否依然有效。有效则认为此客户端依旧是锁的持有者,可以为其提供服务。 ​ Chubby的加锁流程看起来十分简单,我们来详细分析下,Chubby如何解决之前几种方案碰到的问题: ​ 结点故障,数据的一致性保证 ​ 1、 Master故障后,Chubby集群内部会通过一致性协议重新发起选主流程,在新Master产生之前,Chubby集群无法对外提供服务,一致性协议保证Chubby集群内可以选出唯一的Master。 ​ 2、 新当选的Master结点,由于之前已经在集群中,所以其上已经有绝大部分的数据。而对于暂未同步的数据(即新Master原来作为普通结点,未参与投票的那部分数据),新Master通过一致性协议从集群内其他结点学习得到。学习完毕后,新Master开始对外提供服务。整个过程,耗时在6-30S之间。 ​ 3、 对于一台普通结点故障,如果在短时间内恢复,那么其使用本地的一致性日志恢复数据,再用一致性协议和Master学习未同步的数据,学习完毕后,参与投票。 ​ 4、 对于一台普通结点故障,如果在长时间都无法恢复,那么使用新的空闲结点替换,替换时使用集群其他结点的一致性日志文件恢复绝大部分数据,剩余的再用一致性协议和Master学习,学习完毕后,参与投票。 ​ 因此,Chubby通过一致性协议,解决了单点Redis数据没有多份的问题,同时解决了RedLock无法识别缺失数据和学习缺失数据的问题。在可用性方面,只要集群大部分机器正常工作,Chubby就能保持正常对外提供服务。在数据一致性和可用性方面,Chubby这类方案明显优于前两种方案(这本身就是Paxos协议的长处)。 ​ 预防死锁 ​ 1、 Client和Master之间通过保活包(KeepLive)维护会话(Session)。保活包由Client定时发起给Master,Master收到KeepLive包之后,可以立即回复(有事件需要下发时),也可以等待一段时间,等到Session快超时前回复,Client收到回复后,再立刻发起另外一个新的保活包。保活包承担了延长Session租约和通知事件的功能。 ​ 2、 当锁持有方A发生故障,Session无法维护。Master会在Session租约到期后,自动删除该Client持有的锁,以避免锁长时间无法释放而导致死锁。 ​ 3、 另外一个客户端B发现锁已经被释放,发起获取锁操作,成功获取到锁。 ​ 4、 对于同一把锁,每一次获取锁操作,都会使得锁的序列号(GetSequencer)自增。锁序号是一个64位自增的整形。 ​ 5、 对于锁持有方,每次去访问后续的资源服务器时,都会带上自己的锁序号;而资源服务器在处理请求之前,会去Master请求验证当前锁序号是否是最新(CheckSequencer)。 ​ 6、 所以考虑这样一种情况,如果在B获取锁成功之后,A恢复了,A认为自己仍旧持有锁,而发起修改资源的请求,会因为锁序号已经过期而失败,从而保障了锁的安全性。 ​ 总结起来,Chubby引入了资源方和锁服务的验证,来避免了锁服务本身孤立地做预防死锁机制而导致的破坏锁安全性的风险。同时依靠Session来维持锁的持有状态,在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。 ​ 不过引入的代价是资源方需要作对应修改,对于资源方不方便作修改的场景,Chubby提供了一种替代的机制Lock-Delay,来尽量避免由于预防死锁而导致的锁安全性被破坏。Chubby允许客户端为持有的锁指定一个Lock-Delay的时间值(默认是1分钟)。当Chubby发现客户端被动失去联系的时候,并不会立即释放锁,而是会在Lock-Delay指定的时间内阻止其它客户端获得这个锁。这是为了在把锁分配给新的客户端之前,让之前持有锁的客户端有充分的时间把请求队列排空(draining the queue),尽量防止出现延迟到达的未处理请求。 ​ 可见,为了应对锁失效问题,Chubby提供的两种处理方式:CheckSequencer()检查和Lock-Delay,它们对于安全性的保证是从强到弱的。但是Chubby确实提供了单调递增的锁序号,以及资源服务器和Chubby的沟通渠道,这就允许资源服务器在需要的时候,利用它提供更强的安全性保障。 ​

Zookeeper和Chubby的设计差异

1、 对于服务读负载的取舍 ​ Chubby设计为所有的读写都经过Master处理,这必然导致Master的负载过高,因此Chubby在Client端实现了缓存机制。Client端在本地有文件内容的Cache,Client端对Cache的维护只是负责让Cache失效,而不持续更新Cache,失效后的Cache,在Client下一次访问Master之后重新创建。每次修改后,Master通过Client的保活包(所以保活包除了有延长Session租约和通知事件的功能外,还有一个功能是Cacha失效),通知每个拥有此Cache的Client(Master维护了每个Client可能拥有的Cache信息),让他们的Cache失效,Client收到保活包之后,删除本地Cache。如果Client未收到本次保活包,那么只有两种可能,后续的保活包学习到Cache失效的内容,或者Session超时,清空所有Cache重新建立Session。所以对Cache机制而言,不能够保证Cache数据的随时最新,但是可以保证最终的Cache数据一致性,同时可以大量避免每次向Master读带来的网络流量开销和Master的高负载。 ​ Zookeeper设计采取了另外一个思路,其中Client可以连接集群中任意一个节点,而不是必须要连接Master。Client的所有写请求必须转给Master处理,而读请求,可以由普通结点直接处理返回,从而分担了Master的负载。同样的,读数据不能保证时刻的一致性,但是可以保证最终一致性。 ​ 2、 预防死锁方面 ​ Chubby提供了CheckSequencer()检查和Lock-Delay两种方式来避免锁失效带来的问题,引入了资源方和锁服务方的交互来保证锁数据安全性。不过Zookeeper目前还没有类似于CheckSequencer的机制,而只有类似于Lock-Delay的等待机制来尽量避免锁失效带来的安全性问题。所以在锁失效方面的安全性来说,Chubby提供了更好的保证。 ​ 3、 锁使用便利性方面的差异 ​ Chubby和Zookeeper都提供了事件机制,这个机制可以这样来使用,比如当客户端试图创建/lock的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。客户端可以进入一种等待状态,等待当/lock节点被释放的时候,锁服务通过事件机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。 ​ 但是考虑这样一个问题,当有大量的客户端都阻塞在/lock结点上时,一旦之前的持有者释放锁,那么阻塞的潜在调用方都会被激活,但是大量客户端被激活,重新发起加锁操作时,又只有一个客户端能成功,造成所谓的“惊群”效应。 ​ 考虑到这一点,Zookeeper上实现了一个“有序临时结点”的功能,来避免惊群。对于一个临时锁结点,Zookeeper支持每次创建都可以成功,但是每次创建的结点通过一个自增的序号来区别。创建成功最小结点的客户端表明获得了锁,而其他调用方创建的结点序号表明其处于锁等待队列中的位置。所以,对于获取锁失败的客户端,其只需要监听序号比其小的最大结点的释放情况,就可以判断何时自己有机会竞争锁。而不是每次一旦有锁释放,都去尝试重新加锁,从而避免“惊群”效应产生。 ​

4、 结语

​ 本文通过分析三类分布式锁服务,基本涵盖了所有分布式锁服务中涉及到的关键技术,以及对应具体的工程实现方案。 ​ 基于分布式存储实现的锁服务,由于其内存数据存储的特性,所以具有结构简单,高性能和低延迟的优点。但是受限于通用存储的定位,其在锁数据一致性上缺乏严格保证,同时 ​ 其在解锁验证、故障切换、死锁处理等方面,存在各种问题。所以其适用于在对性能要求较高,但是可以容忍极端情况下丢失锁数据安全性的场景下。 ​ 基于分布式一致性算法实现的锁服务,其使用Paxos协议保证了锁数据的严格一致性,同时又具备高可用性。在要求锁数据严格一致的场景下,此类锁服务几乎是唯一的选择。但是由于其结构和分布式一致性协议的复杂性,其在性能和加锁延迟上,比基于分布式存储实现的锁服务要逊色。 ​ 所以实际应用场景下,需要根据具体需求出发,权衡各种考虑因素,选择合适的锁服务实现模型。无论选择哪一种模型,需要我们清楚地知道它在安全性上有哪些不足,以及它会带来什么后果。更特别的,如果是对锁数据安全性要求十分严格的应用场景,那么需要更加慎之又慎。在本文的讨论中,我们看到,在分布式锁的正确性上走得最远的,是基于Paxos实现,同时引入分布式资源进行验证的方案。 ​ 参考 ​ [1]《Distributed locks with Redis》 [2]《The Chubby Lock Service for Loosely-Coupled Distributed Systems》 [3]《How to do distributed locking》 [4]《Note on fencing and distributed locks》 [5]《ZooKeeper Recipes and Solutions》 [6]《基于Redis的分布式锁真的安全吗?(上)》 [7]《基于Redis的分布式锁真的安全吗?(下)》

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏游戏开发那些事

【Cocos2d-x游戏开发】细数Cocos2d-x开发中那些常用的C++11知识

  自从Cocos2d-x3.0开始,Cocos2dx就正式的使用了C++11标准.C++11简洁方便的特性使程序的可拓展性和可维护性大大提高,也提高了代码的书...

693
来自专栏chenssy

【死磕Java并发】-----Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 1. 在单线程环境...

1172
来自专栏菜鸟前端工程师

JavaScript学习笔记022-原型链0原型继承0对象的深浅拷贝extends

521
来自专栏Java工程师日常干货

分布式利器Zookeeper(一)Hello,Zookeeper初步认识Zookeeper的数据模型初步认识Zookeeper的角色组成install Zookeeper基本的ZK命令ZooInspe

Zookeeper不论是在实际项目中,还是在各种分布式开源项目中都得到了广泛应用,从本篇博客开始,将为大家带来我对Zookeeper的认识。这个系列将会涵盖Zo...

792
来自专栏企鹅号快讯

你真的需要消息队列吗

我是一个极简主义者,我不喜欢让软件过早或不必要地复杂化。向软件系统添加组件是增加复杂性的一种方法。让我们以消息团队为例。 消息队列是一个系统,使您能够获得容错、...

1905
来自专栏java学习

面试题9(包含抽象方法的一定是抽象类吗)

编译并运行下面代码: class Base { abstract public void myfunc(); public void another...

2838
来自专栏一个会写诗的程序员的博客

RequireJS极简入门教程RequireJS核心功能:HOW TOmain.js使用 shim

随着网站功能逐渐丰富,网页中的js也变得越来越复杂和臃肿,原有通过script标签来导入一个个的js文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作...

893
来自专栏程序员宝库

Nginx 反向代理解决前后端联调跨域问题

keywords: Nginx反向代理 前后端联调 跨域 ---- 1.什么是跨域 跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏...

5054
来自专栏简单聊聊Spark

Spark内核分析之Master的注册机制实现原理

        这篇文章我们来讨论一下Master的注册机制;那么有哪些信息需要注册到Master上面去呢?很简单,分别有Worker的注册,Driver的注册...

953
来自专栏java一日一条

有了 GC 还会不会发生内存泄漏?

这个问题是我在写C++时考虑到的,C++需要手动管理内存,虽然现在标准库中提供了一些智能指针,可以实现基于引用计数的自动内存管理,但现实环境是很复杂的,我们仍要...

373

扫码关注云+社区