page contents

分布式锁在存储系统中的技术实践

针对共享资源的互斥访问历来是很多业务系统需要解决的问题。在分布式系统中,通常会采用分布式锁这一通用型解决方案。本文将就分布式锁的实现原理、技术选型以及阿里云存储的具体实践进行论述。

attachments-2021-03-Cn7jPQUy603eed6707f52.png


1 背景

针对共享资源的互斥访问历来是很多业务系统需要解决的问题。在分布式系统中,通常会采用分布式锁这一通用型解决方案。本文将就分布式锁的实现原理、技术选型以及阿里云存储的具体实践进行论述。

attachments-2021-03-n4Cefhwl603eed712aceb.png

图1 锁


2 从单机锁到分布式锁

在单机环境中,当共享资源自身无法提供互斥能力的时候,为了防止多线程/多进程对共享资源的同时读写访问造成的数据破坏,就需要一个第三方提供的互斥的能力,这里往往是内核或者提供互斥能力的类库,如下图所示,进程首先从内核/类库获取一把互斥锁,拿到锁的进程就可以排他性的访问共享资源。演化到分布式环境,我们就需要一个提供同样功能的分布式服务,不同的机器通过该服务获取一把锁,获取到锁的机器就可以排他性的访问共享资源,这样的服务我们统称为分布式锁服务,锁也就叫分布式锁。

attachments-2021-03-R401DeIU603eed7d6b842.jpg

图2 单机锁到分布式锁

由此抽象一下分布式锁的概念,首先分布式锁需要是一个资源,这个资源能够提供并发控制,并输出一个排他性的状态,也就是:

锁 = 资源 + 并发控制 + 所有权展示

以常见的单机锁为例:

Spinlock = BOOL + CAS (乐观锁) 
Mutex = BOOL + CAS + 通知 (悲观锁)

Spinlock和Mutex都是一个Bool资源,通过原子的CAS指令:当现在为0设置为1,成功的话持有锁,失败的话不持有锁,如果不提供所有权的展示,例如AtomicInteger,也是通过资源(Interger)+CAS,但是不会明确的提示所有权,因此不会被视为一种锁,当然,可以将“所有权展示”这个更多地视为某种服务提供形式的包装。

单机环境下,内核具备“上帝视角”,能够知道进程的存活,当进程挂掉的时候可以将该进程持有的锁资源释放,但发展到分布式环境,这就变成了一个挑战,为了应对各种机器故障、宕机等,就需要给锁提供了一个新的特性:可用性。

如下图所示,任何提供三个特性的服务都可以提供分布式锁的能力,资源可以是文件、KV等,通过创建文件、KV等原子操作,通过创建成功的结果来表明所有权的归属,同时通过TTL或者会话来保证锁的可用性。

attachments-2021-03-NAnaJ35S603ef13ee15fe.jpg

图3 分布式锁的特性和实现


3 分布式锁的系统分类

根据锁资源本身的安全性,我们将分布式锁分为两个阵营:

A:基于异步复制的分布式系统,例如mysql,tair,redis等;

B:基于paxos协议的分布式一致性系统,例如zookeeper,etcd,consul等;

基于异步复制的分布式系统,存在数据丢失(丢锁)的风险,不够安全,往往通过TTL的机制承担细粒度的锁服务,该系统接入简单,适用于对时间很敏感,期望设置一个较短的有效期,执行短期任务,丢锁对业务影响相对可控的服务。

基于paxos协议的分布式系统,通过一致性协议保证数据的多副本,数据安全性高,往往通过租约(会话)的机制承担粗粒度的锁服务,该系统需要一定的门槛,适用于对安全性很敏感,希望长期持有锁,不期望发生丢锁现象的服务。


4 阿里云存储分布式锁

阿里云存储在长期的实践过程中,在如何提升分布式锁使用时的正确性、保证锁的可用性以及提升锁的切换效率方面积累比较多的经验。

4.1 严格互斥性

互斥性作为分布式锁最基本的要求,对用户而言就是不能出现“一锁多占”,那么存储分布式锁是如何避免该情况的呢?

答案是,服务端每把锁都和唯一的会话绑定,客户端通过定期发送心跳来保证会话的有效性,也就保证了锁的拥有权。当心跳不能维持时,会话连同关联的锁节点都会被释放,锁节点就可以被重新抢占。这里有一个关键的地方,就是如何保证客户端和服务端的同步,在服务端会话过期的时候,客户端也能感知,如下图所示,在客户端和服务端都维护了会话的有效期的时间,客户端从心跳发送时刻(S0)开始计时,服务端从收到请求(S1)开始计时,这样就能保证客户端会先于服务端过期。用户在创建锁之后,核心工作线程在进行核心操作之前可以判断是否有足够的有效期,同时我们不再依赖墙上时间,而是基于系统时钟来对时间进行判断,系统时钟更加精确,且不会向前或者向后移动(秒级别误差毫秒级,同时在NTP跳变的场景,最多会修改时钟的速率)。

attachments-2021-03-wqiG9MOe603ef14b54a46.png

图4 存储场景的使用方式

在分布式锁互斥性上,我们是不是做到完美了?并非如此,还是存在一种情况下业务基于分布式锁服务的访问互斥会被破坏。我们来看下面的例子:如下图9所示,客户端在时间点S0尝试去抢锁,在时间点S1在后端抢锁成功,因此也产生了一个分布式锁的有效期窗口。在有效期内,时间点S2做了一个访问存储的操作,很快完成,然后在时间点S3判断锁的有效期依旧成立,继续执行访问存储操作,结果这个操作耗时良久,超过了分布式锁的过期时间,那么可能这个时候,分布式锁已经被其他客户端抢占成功,进而出现两个客户端同时操作同一批数据的可能性,这种可能性是存在的,虽然概率很小。

attachments-2021-03-OG0ZVHuS603ef154c5a66.png

图6 越界场景

针对这个场景,具体的应对方案是在操作数据的时候确保有足够的锁有效期窗口,当然如果业务本身提供回滚机制的话,那么方案就更加完备,该方案也在存储产品使用分布式锁的过程中被采用。

还有一个更佳的方案,即,存储系统本身引入IO Fence能力。这里就不得不提Martin Kleppmann和redis的作者antirez之间的讨论了,redis为了防止异步复制导致的锁丢失的问题,引入redlock,该方案引入了多数派的机制,需要获得多数派的锁,最大程度的保证了可用性和正确性,但仍然有两个问题:

• 墙上时间的不可靠(NTP时间)
• 异构系统的无法做到严格正确性

墙上时间可以通过非墙上时间MonoticTime来解决(redis目前仍然依赖墙上时间),但是异构系统的只有一个系统并没有办法保证完全正确,如下图10所示,Client1获取了锁,在操作数据的时候发生了GC,在GC完成时候丢失了锁的所有权,造成了数据不一致。

attachments-2021-03-BZwd6BuL603ef15dcc578.png

图7 异构系统无法做到完全正确性

因此需要两个系统同时协作来完成一个完全正确的互斥访问,在存储系统引入IO Fence能力,如下图11所示,全局锁服务提供全局自增的token,Client1拿到锁返回的token是33,并带入存储系统,发生GC,当Client2抢锁成功返回34,带入存储系统,存储系统会拒绝token较小的请求,那么经过了长时间full gc重新恢复后的Client 1再次写入数据的时候,因为存储层记录的Token已经更新,携带token值为33的请求将被直接拒绝,从而达到了数据保护的效果(chubby的论文中有讲述,也是Martin Kleppmann提出的解决方案)。

attachments-2021-03-SLyedtFY603ef1669f109.jpg

图8 引入IO Fence能力

这与阿里云分布式存储平台盘古的设计思路不谋而合,盘古支持了类似IO Fence的写保护能力,引入Inline File的文件类型,配合Seal File操作,这就有着类似IO Fence的写保护能力,首先,SealFile操作用来关闭已经打开的cs上面的文件,防止旧的Owner继续写数据;其次,InlineFile可以防止旧的Owner打开新的文件。这两个功能事实上也是提供了存储系统中的Token支持。

4.2 可用性

存储分布式锁通过持续心跳来保证锁的健壮性,让用户不用投入很多精力关注可用性,但也有可能异常的用户进程持续占据锁。针对该场景,为了保证锁最终可以被调度,提供了可以安全释放锁的会话加黑机制。

当用户需要将发生假死的进程持有的锁释放时,可以通过查询会话信息,并将会话加黑,此后,心跳将不能正常维护,最终导致会话过期,锁节点被安全释放。这里我们不是强制删除锁,而是选用禁用心跳的原因如下:

  1. 删除锁操作本身不安全,如果锁已经被其他人正常抢占,此时删锁请求会产生误删除。
    b.删除锁后,持有锁的人会话依然正常,它仍然认为自己持有锁,会打破锁的互斥性原则。

4.3 切换效率

当进程持有的锁需要被重新调度时,持有者可以主动删除锁节点,但当持有者发生异常(如进程重启,机器宕机等),新的进程要重新抢占,就需要等待原先的会话过期后,才有机会抢占成功。默认情况下,分布式锁使用的会话生命期为数十秒,当持有锁的进程意外退出后(未主动释放锁),最长需要经过很长时间锁节点才可以被再次抢占。

attachments-2021-03-gb5FNVG0603ef1706d6c2.png

图5 客户端和服务各自维护过期时间

要提升切换精度,本质上要压缩会话生命周期,同时也意味着更快的心跳频率,对后端更大的访问压力。我们通过对进行优化,使得会话周期可以进一步压缩。

同时结合具体的业务场景,例如守护进程发现锁持有进程挂掉的场景,提供锁的CAS释放操作,使得进程可以零等待进行抢锁。比如利用在锁节点中存放进程的唯一标识,强制释放已经不再使用的锁,并重新争抢,该方式可以彻底避免进程升级或意外重启后抢锁需要的等待时间。


5 结语

阿里云存储提供了完整的分布式锁解决方案,经过了阿里云众多云产品宝贵的业务场景中长期锤炼,稳定高可靠,且提供了多种语言的SDK选择,甚至是RESTful集成方案。

分布式锁提供了分布式环境下共享资源的互斥访问,业务或者依赖分布式锁追求效率提升,或者依赖分布式锁追求访问的绝对互斥。同时,在接入分布式锁服务过程中,要考虑接入成本、服务可靠性、分布式锁切换精度以及正确性等问题,正确和合理的使用分布式锁,是需要持续思考并予以优化的。


attachments-2021-03-BmvpSlp2603ef17bc3be8.jpg本文来源 |阿里技术

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
轩辕小不懂
轩辕小不懂

2403 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1478 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章