写点什么

开源 | WLock:高可用分布式锁设计实践

作者:
  • 2022 年 8 月 16 日
    北京
  • 本文字数:4653 字

    阅读完需:约 15 分钟

● 项目名称:WLock

● Github 地址:

https://github.com/wuba/WLock.git

● 简介:WLock 是一套基于 58 已开源的一致性算法组件WPaxos实现的高可靠、高吞吐分布式锁服务,可应用于分布式环境下协调多进程/线程对共享资源的访问控制、多节点 Master 选主等业务场景。

核心特性

  • 丰富的锁类型:可重入锁、公平锁、优先级锁、读写锁、进程锁、线程锁;

  • 灵活的锁操作:支持阻塞/非阻塞、同步/异步 watch 等方式获取锁,支持锁续约,TTL 等机制;


  • 高可靠性:基于 Paxos 算法实现多副本数据同步,Master 节点故障时主从自动切换,在无 Master 或者 Master 漂移过程仍可保证锁状态的持续一致性,不影响正常锁操作;


  • 高吞吐:多 Paxos 分组的 Master 均匀分布在所有集群节点,不同 Paxos 分组的锁操作并行处理,相同 Paxos 分组锁操作批量合并处理,大大提升了系统的吞吐量;


  • 多租户:提供秘钥作为集群分配、锁操作隔离、权限控制的租户单位,支持单个秘钥跨集群动态平滑迁移;


  • 易用性:丰富的锁接口封装,开箱即用;

1. 项目背景

在分布式部署的应用集群中,经常会存在一些业务场景,为了保证某些业务逻辑的准确性,或者避免某些逻辑被重复执行,需要限制多个应用进程或线程对共享数据资源进行互斥访问,例如秒杀下单、商品抢购等场景,通常的解决方案是引入分布式锁技术。

对于一些比较复杂的分布式场景,除了要求分布式锁具有互斥性、避免死锁、可重入等基本特性外,同时对分布式锁服务的吞吐性能、数据一致性、可靠性等方面也有很强的要求,了解 CAP 理论的都知道,这是分布式系统设计的难点。当前已有的分布式锁解决方案,也很难同时满足这几个特性,通常需要做出取舍,如基于 Redis 封装实现的分布式锁牺牲数据强一致性来保证吞吐量,或基于 Zookeeper、Etcd 封装实现的分布式锁牺牲一定的可用性保证数据强一致性。本项目希望提供一种分布式锁方案,不仅满足高吞吐的业务场景需求,还能够保证服务具有比较高的可用性和可靠性。

2. 设计实践

功能架构如下:

目前开源的模块主要包括负责锁核心交互的客户端、服务端,以及负责配置管理的注册中心三部分。其中核心实现有以下几个部分:


2.1 可靠存储设计

WLock 选择高吞吐的键值存储系统 RocksDB 来持久化锁的状态信息,基于 WPaxos 组件实现多副本数据同步以及主从自动切换能力;每个节点配置相同数量的 Paxos 分组,客户端根据锁名称,将锁哈希到某一个固定 Paxos 分组,默认将锁请求发送到该 Paxos 分组对应的 Master 节点,锁状态更新时,由 Master 节点发起 Propose 请求同步给其它 Slave 节点,最终在执行状态机时写入 RocksDB,保证多副本数据的强一致性,如下图所示:

我们知道 Zookeeper、Etcd 同样也采用一致性协议实现多副本可靠存储,但不同的是,它们只有单个 Zab 或 Raft 实例串行同步数据,而 WLock 采用多 Paxos 分组机制,不同分组的 Master 在集群中均匀分布,集群节点对等部署,可并行同步数据,大大提升了系统的吞吐能力;另外,得益于 Paxos 协议的灵活性,不对 Master 强依赖,当 Master 节点关闭或者出现异常时,未选出新的 Master 之前,可将锁操作请求发送到指定的候选节点来处理,保证系统持续提供服务,具有更高的可用性。


2.2 锁操作实现

WLock 封装提供了丰富的锁类型如可重入锁、公平锁、优先级锁、读写锁等,下面以可重入锁为例,介绍下加锁/释放锁实现。






  • 阻塞/非阻塞获取锁

WLock 提供有阻塞与非阻塞方式获取锁,区别在于是否等待服务端加锁成功。非阻塞方式根据服务端锁当时的状态,直接返回客户端加锁成功或失败;而阻塞方式在锁已被其它 owner 抢占的情况下,会在服务端内存注册一个 WatchEvent 事件等待获取锁,当锁被释放时,服务端立即从 Watch 等待队列中选择一个优先级最高的 WatchEvent 直接执行加锁,并通知唤醒监听的客户端。

相对于其它分布式锁通过客户端定时轮询实现的阻塞机制,WLock 的阻塞机制在锁竞争度比较高时,获取锁的延迟低、实时性更强,还能够更好支持优先级锁,但也存在一个缺陷,WatchEvent 长期存储于服务端缓存队列,若客户端所在机器宕机或者出现一些网络异常,服务端不能及时感知到客户端异常变化时,挂起在服务端的 WatchEvent 变为无效状态且不会被立即剔除,这时唤醒该 WatchEvent 加锁成功后,不能成功通知到客户端,锁过期前又不能被其它客户端抢占。为此,WLock 引入两种机制来优化这个问题,

1)WatchEvent 添加心跳保活机制。客户端每隔 20s 定时向服务端发起 WatchEvent 心跳,服务端接收到后,发现 WatchEvent 已存在,便延长 WatchEvent 的有效时间,服务端定时检测内存中 WatchEvent 是否有效,做过期剔除处理。

2)通过监听 WatchEvent 获取到锁时,客户端首先进行续约 Touch。服务端在选择 WatchEvent 执行加锁时,会先把锁的过期时间调整为 Math.min(锁真实过期时间,10s),客户端收到加锁成功通知后需要立即发起续约 Touch,服务端收到续约请求后再延长锁的过期时间为客户端接口设置的真实过期时间。这样,若通知的客户端已不活跃,最长阻塞 10s 锁不能被其它客户端抢占。

该 WatchEvent 处理机制,同样应用于异步 Watch 方式的加锁处理。


  • 可重入实现

WLock 可重入机制是由客户端通过锁上下文中的 AcquireCount 原子变量计数控制,第一次获取锁时请求服务端加锁,成功后进行本地计数,其它情况下,将加锁请求转为续约锁请求发送到服务端;释放锁过程,只有 AcquireCount 小于等于 0 时,才向服务端释放锁,否则只是本地计数减一处理。这样设计考虑的是,若在服务端计数控制,在某些异常情况下,当返回客户端的加锁请求超时,实际服务端执行加锁计数成功时,可能会导致客户端与服务端重入锁计数不一致而产生死锁。


  • TTL 机制

为了避免死锁,服务端要求每个锁都设有过期时间,过期时间需要根据客户端对共享资源正常访问时间合理设置。如果设置太短,有可能在客户端完成对共享资源访问之前,锁就发生过期,从而破坏锁的安全性;如果设置太长,一旦某个持有锁的客户端释放锁失败,就会导致一段时间内其它客户端都无法获到取到锁。

WLock 服务端限制锁的过期时间最多为 5 分钟,但是对客户端设置的锁过期时间则不做限制,对于过期时间超过 5 分钟的加锁请求,通过自动定时续约来延长锁的过期时间,客户端发送到服务端的加锁和续约锁请求中,携带的锁过期时间最大也只能为 5 分钟。自动续约周期默认为 Math.min(锁过期时间,5 分钟)的 1/3(最小为 1 秒),这样在锁过期前,允许客户端至少有两次容错机会。

WLock 计算过期时间戳的方式是[加锁成功的起始时间/续约锁成功的起始时间+锁过期时间],锁过期检测严格依赖机器时钟,为了避免集群服务节点间的时钟差异,导致不同节点计算锁过期触发时机不一致,WLock 限制服务端只有 Master 节点主动检测锁的过期状态,检测到锁过期时,由 Master 发起 Propose 请求同步 Slave 节点对过期锁进行删除处理,并将 ExpireEvent 通知到客户端,降低了时钟不一致对锁服务的影响。由于锁过期检测 Task 仅存储在 Master 内存,当 Master 发生漂移时,新的 Master 需要从 RocksDB 中重新加载分组下所有 Lockkey 信息,生成锁过期检测 Task 分发到根据过期时间排序的优先级队列,并恢复检测任务,这个过程通常在几秒内完成,这期间可能会导致服务端检测锁过期存在延迟。为了不受服务端或网络异常影响,WLock 客户端同时也开启有锁的过期检测任务,锁过期时间比服务端延迟了加锁或续约请求 Response 网络返回时间,客户端对于过期回调的执行做了幂等判断,避免并发重复执行。

大神 Martin Kleppmann 在文章《How to do distributed locking》中对分布式锁原理进行论证时,指出 Redlock 可能存在的安全问题:当客户端获取到锁后发生 GC pause 或者服务端出现时钟回退,有可能在锁持有者释放锁之前,锁就发生过期,此时如果另外一个客户端抢占到锁,锁的互斥性会被破坏。WLock 同样是引入了锁版本号(fencing token)来解决这个问题,但是需要用户自己在访问共享资源时,携带并比较当前资源更新操作的锁最大版本号(类似于乐观锁机制),再结合事务机制保证数据操作的一致性。


  • 续约机制

WLock 支持主动和自动两种续约机制,主动续约机制可通过客户端调用续约接口触发,自动续约机制可在加锁时通过参数配置启动。由于续约线程与锁持有线程通常为两个线程,自动续约机制存在一定的安全风险,业务在加锁处理逻辑的外层 finally 逻辑中一定要释放锁(远程释放锁前,会先停止自动续约任务),否则锁持有线程异常退出时,锁自动续约还会一直执行,导致锁永远不过期,出现死锁。


  • 事件补偿

WLock 通过[Host, Pid,ThreadID]来定义一个锁 Owner,ThreadID 为-1 时锁为进程粒度,否则为线程粒度。锁 owner 不会随着客户端与服务端连接状态的更新而变化,但锁状态发生变更时,服务端会向锁 owner 或者锁监听者主动推送一些事件如 WatchEvent、ExpireEvent,为此,服务端需要维护锁 owner 或锁监听者与客户端连接的对应关系。

当 WLock 服务端 Master 发生漂移或者网络连接异常重连时,客户端连接绑定关系会同时发生变更,此时就需要客户端主动进行事件补偿,补偿类型包括两种:

1)WatchEvent 事件补偿,重新向服务端注册监听事件,服务端更新 WatchEvent 与客户端连接绑定关系。

2)AcquireEvent 事件补偿,客户端对已持有的锁进行续约,服务端收到请求后不更新锁的过期时间,只更新锁 owner 与客户端连接的对应关系。


2.3. 高并发优化

前面提到,WLock 通过引入多 Paxos 分组,多节点互为主备对等部署并行同步数据的集群架构,已经一定程度上提升了系统的吞吐能力。此外,Wlock 还充分利用了 WPaxos 组件批量 Propose 的功能,对单分组的锁操作实现做了以下优化,进一步提升了系统的并发处理能力:

1)单个 Paxos 分组采用多线程并行处理锁请求,相同 Lockkey 的锁请求哈希到同一个线程串行处理;

2)对单个 Paxos 分组下不同线程阻塞的锁请求进行合并,批量发起 Propose 同步数据,降低了网络传输与多任务调用切换成本。

3. 为什么选择 WLock

在分布式领域中,分布式锁已经是一种比较成熟的技术,现有的实现方案已有很多,为什么还开发 WLock,优势又有哪些?接下来我们从功能、服务特性、性能三个维度,介绍下常见的几种分布式锁的差异点。

  • 功能

  •  服务特性

  • 性能 

测试运行环境

机器配置:CPU:20 x Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz  

内存:192 GB  

硬盘:SSD

网卡:万兆网卡  

服务端集群机器个数:3 台


测试结果

1. 单客户端 qps:

2. 相同并发下,请求响应延迟(单位 ms)

说明:以上对比测试的中数据,Redis、ZK、Etcd 相关非官方数据,均由我们在相同环境下实际压测得到。其中,对于 QPS 的统计,客户端请求一次加锁再请求一次释放锁合并为一次计数,更详细的压测数据及压测条件可查看开源对比文档。


通过以上几个维度的测试分析,WLock 的优势在于可靠性与系统吞吐量比较高,处理延迟略低于 Redis,但明显高于 Zookeeper 与 Etcd,为此,对于分布式锁选型有以下建议:

  • 对可靠性要求不高,响应延迟比较敏感的场景,锁并发低于 3W 时可使用 Redis,高于 3W 建议用 WLock;

  • 对可靠性要求比较高,同时锁并发高于 500 的场景,可使用 WLock;

4. 未来规划

  • 提供 GO、PHP、Node 等多语言 SDK

  • 开源 WEB 管控中心、监控模块

  • 支持分布式信号量机制

参考资料

  1. WPaxos 源码地址:https://github.com/wuba/WPaxos

  2. 开源|WPaxos:一致性算法 Paxos 的生产级高性能 Java 实现:https://mp.weixin.qq.com/s/bydpMwTWAamS3u8Ko57iYg

  3. How to do distributed locking:

    https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

如何贡献 &问题反馈

诚挚邀请对分布式锁感兴趣的同学一起参与 WLock 项目的开发建设,提出宝贵意见和建议,可在https://github.com/wuba/WLock.git开源社区提交 issue 与 Pull Request 反馈给我们。也可以扫描微信号,备注 WLock,加入微信交流群。


用户头像

关注

还未添加个人签名 2018.11.19 加入

还未添加个人简介

评论

发布
暂无评论
开源 | WLock:高可用分布式锁设计实践_分布式_溜_InfoQ写作社区