写点什么

细谈分布式锁及在 OpenStack 上的应用:如何实现 Active/Active 高可用

  • 2017-05-18
  • 本文字数:6129 字

    阅读完需:约 20 分钟

本文首先介绍了分布式锁的概念,详细介绍了分布式锁多种实现方式以及它们的优缺点。然后介绍了 OpenStack Tooz 项目,该项目实现了分布式锁的通用框架,支持对接不同的 DLM。最后介绍了 OpenStack Cinder 实现高可用的痛点问题以及讨论了社区当前正如何使用分布式锁来实现 cinder-volume 服务的 AA 高可用模式。

一、分布式锁介绍

1.1 何谓“锁”?

锁(Lock),在我们生活当中再熟悉不过了,它的主要作用就是锁住某个物体或者某个区域空间,一方面阻止无锁者进入查看受隐私保护的东西,二来阻止恶意者实施破坏。比如我们都会对手机加锁来防止别人偷窥自己的隐私,我们通过锁门来阻止坏人进入家里偷窃等等。

在计算机的世界,锁同样重要,并且功能和我们生活当中的锁基本是一样的,只是管理的对象不一样而已。计算机的锁是为了锁住某个计算机资源或者数据,不允许其它用户或者进程随意访问读取,或者保护数据不被其它程序恶意篡改。除此之外,锁还具有同步的功能,即保证 A 任务完成之后才能执行 B 任务,而不能顺序颠倒,也不能在 A 完成到一半时,B 就开始执行。

我们的服务器几乎都是使用多用户分时操作系统,意味着同一时间有多个用户执行多个进程并且同时运行着,即使是同一进程,也有可能包含有多个线程跑着。如果我们不对一些互斥访问的资源进行加锁,势必会乱套,试想下同时多个程序不加控制的使用同一台打印机会造成什么恶果。

除了资源,对数据的加锁保护也同样重要,如果没有锁,数据就会各种紊乱,比如你的记账管理系统,开始总额有 5000 元,入账 5000 元,此时总额应该就有 10000 元了,但还没有来得及更新到数据库中去,此时,你的另一个进程开始读取总额,由于之前入账的 5000 块还没有更新,取出来的总额还是原来的 5000 元,显然读取的这 5000 元是过时的老数据,在操作系统术语中称为脏数据。更诡秘的是,此时你的第二个进程开始出账 3000 块,此时开始写入剩余总额 2000 元。此时你的两个进程可能同时在写数据,假设你的进程一先写完(写入 10000),你的第二个进程写入 2000 元覆盖掉了刚刚写入的 10000 元,最后你的总额为 2000 元,你刚刚入账的 5000 元莫名其妙地蒸发了。以上情况就是事务处理中 ACID 的数据不一致性问题。

阅读本文读者只需要知道锁的作用即可,更多关于并发控制的有关概念(比如幻读、不可重复读、原子性、持久性等)以及各种锁的概念(互斥锁、共享锁、乐观锁、悲观锁、死锁等)不是本文讨论的重点,感兴趣的读者请自行 Google 之。

1.2 锁的实现非权威解读

生活当中的锁原理其实很简单,传统的机械锁可以当作是一种机关,通过匹配形状齿轮触发机关某个部位而解锁。而现代的锁原理更简单,加锁其实就是设置一种状态,而要解除该状态只需要比对信息,比如数字密码、指纹等。

计算机中的锁,根据运行环境可以大体分为以下三类:

  • 同一个进程:此时主要管理该进程的多个线程间同步以及控制并发访问共享资源。由于进程是共享内存空间的,一个最简单的实现方式就是使用一个整型变量作为 flag,这个 flag 被所有线程所共享,其值为 1 表示已上锁,为 0 表示空闲。使用该方法的前提是设置 (set) 和获取 (get) 这个 flag 变量的值都必须是原子操作,即要么成功,要么失败,并且中间不允许有中断,也不允许出现中间状态。可幸的是,目前几乎所有的操作系统都提供了此类原子操作,并已经引入了锁机制,所以以上前提是可以满足的。
  • 同一个主机:此时需要控制在同一个操作系统下运行的多个进程间如何协调访问共享资源。不同的进程由于不共享内存空间,因此不能通过设置变量来实现。既然内存不能共享,那磁盘是共享的,因此我们自然想到可以通过创建一个文件作为锁标记。进程只需要检查文件是否存在来判断是否有锁。
  • 不同主机:此时通常跑的都是分布式应用,如何保证不同主机的进程同步和避免资源访问冲突。有了前面的例子,相信很多人都想到了,使用共享存储不就可以了,这样不同主机的进程也可以通过检测文件是否存在来判断是否有锁了。[机智表情]

以上介绍了锁的非权威实现,为了解释得更简单通俗,其实隐瞒了很多真正实现上的细节,甚至可能和实际的实现方式并不一致。要想真正深入理解操作系统的锁机制以及单主机的锁机制实现原理,请查阅更多的文献。本文接下来将重点讨论分布式锁,也就是前面提到的不同主机的进程间如何实现同步以及控制并发访问资源。

1.3 分布式锁以及 DLM 介绍

毫无疑问,分布式锁主要解决的是分布式资源访问冲突的问题,保证数据的一致性。前面提到使用共享存储文件作为锁标记,这种方案只有理论意义,实际上几乎没有人这么用, 因为创建文件并不能保证是原子操作。另一种可行方案是使用传统数据库存储锁状态,实现方式也很简单,检测锁时只需要从数据库中查询锁状态即可。当然,可能使用传统的关系型数据库性能不太好,因此考虑使用 KV-Store 缓存数据库,比如 redis、memcached 等。但都存在问题:

  • 不支持堵塞锁。即进程获取锁时,不会自己等待锁,只能通过不断轮询的方式判断锁的状态,性能不好,并且不能保证实时性。
  • 不支持可重入。所谓可重入锁就是指当一个进程获取了锁时,在释放锁之前能够无限次重复获取该锁。试想下,如果锁是不可重入的,一个进程获取锁后,运行过程中若再次获取锁时,就会不断循环获取锁,可实际上锁就在自己的手里,因此将永久进入死锁状态。当然也不是没法实现,你可以顺便存储下主机和进程 ID,如果是相同的主机和进程获取锁时则自动通过,还需要保存锁的使用计数,当释放锁时,简单的计数 -1,只有值为 0 时才真正释放锁。

另外,锁需要支持设置最长持有时间。想象下,如果一个进程获取了锁后突然挂了,如果没有设置最长持有时间,锁就永远得不到释放,成为了该进程的陪葬品,其它进程将永远获取不了锁而陷入永久堵塞状态,整个系统将瘫痪。使用传统关系型数据库可以保存时间戳,设置失效时间,实现相对较复杂。而使用缓存数据库时,通常这类数据库都可以设置数据的有效时间,因此相对容易实现。不过需要注意不是所有的场景都适合通过锁抢占方式恢复,有些时候事务执行一半挂了,也不能随意被其它进程强制介入。

支持可重入和设置锁的有效时间其实都是有方法实现,但要支持堵塞锁,则依赖于锁状态的观察机制,如果锁的状态一旦变化就能立即通知调用者并执行回调函数,则实现堵塞锁就很简单了。庆幸的是,分布式协调服务就支持该功能,Google 的 Chubby 就是非常经典的例子,ZooKeeper 是 Chubby 的开源实现,类似的还有后起之秀 etcd 等。这些协调服务有些类似于 KV-Store,也提供 get、set 接口,但也更类似于一个分布式文件系统。以 ZooKeeper 为例,它通过瞬时有序节点标识锁状态,请求锁时会在指定目录创建一个瞬时节点,节点是有序的,ZooKeeper 会把锁分配给节点最小的服务。ZooKeeper 支持 watcher 机制,一旦节点变化,比如节点删除 (释放锁),ZooKeeper 会通知客户端去重新竞争锁,从而实现了堵塞锁。

另外,ZooKeeper 支持临时节点的概念,在客户进程挂掉后,临时节点会自动被删除,这样可实现锁的异常释放,不需要给锁增加超时功能了。

以上提供锁服务的应用我们通常称为 DLM(Distributed lock manager),对比以上提到的三种类型的 DLM:

注: 以上支持度仅考虑最简单实现,不涉及高级实现,比如传统数据库以及缓存数据库也是可以实现可重入的,只是需要花费更多的工作量。

二、OpenStack Tooz 项目介绍

2.1 Tooz 为何而生?

前面介绍了很多实现分布式锁的方式,但也只是提供了实现的可能和思路,而并未达到拿来即用的地步。开发者仍然需要花费大量的时间完成对分布式锁的封装实现。使用不同的后端,可能还有不同的实现方式。如果每次都需要重复造轮子,将浪费大量的时间,并且质量难以保证。

你一定会想,会不会有人已经封装了一套锁管理的库或者框架,只需要简单调用 lock、trylock、unlock 即可,不用关心底层内部实现细节,也不用了解后端到底使用的是 ZooKeeper、Redis 还是 Etcd。curator 库实现了基于 ZooKeeper 的分布式锁,但不够灵活,不能选择使用其它的 DLM。OpenStack 社区为了解决项目中的分布式问题,开发了一个非常灵活的通用框架,项目名为 Tooz,它实现了非常易用的分布式锁接口,本文接下来将详细介绍该项目。

2.2 Tooz

Tooz 是一个 python 库,提供了标准的 coordination API。最初由 eNovance 几个工程师编写,其主要目标是解决分布式系统的通用问题,比如节点管理、主节点选举以及分布式锁等,更多 Tooz 背景可参考 Distributed group management and locking in Python with tooz。Tooz 抽象了高级接口,支持对接十多种 DLM 驱动,比如 ZooKeeper、Redis、Mysql、Etcd、Consul 等,其官方描述为:

The Tooz project aims at centralizing the most common distributed primitives like group membership protocol, lock service and leader ?election by providing a coordination API helping developers to build distributed applications.

使用 Tooz 也非常方便,只需要三步:

  1. 与后端 DLM 建立连接,获取 coordination 实例。
  2. 声明锁名称,创建锁实例
  3. 使用锁

官方给出了一个非常简单的实例,如下:

coordinator.stop() 由于该项目最先是由 Ceilometer 项目 core 开发者发起的,因此 tooz 最先在 Ceilometer 中使用,主要用在 alarm-evaluator 服务。目前 Cinder 也正在使用该库来实现 cinder-volume 的 Active/Active 高可用,将在下文重点介绍。

三、分布式锁在 Cinder 中的应用

3.1 Cinder 之伤

Cinder 是 OpenStack 的核心组件之一,为云主机提供可扩展可伸缩的块存储服务,用于管理 volume 数据卷资源,类似于 AWS 的 EBS 服务。cinder-volume 服务是 Cinder 最关键的服务,负责对接后端存储驱动,管理 volume 数据卷生命周期,它是真正干活的服务。

显然 volume 数据卷资源也需要处理并发访问的冲突问题,比如防止删除一个 volume 时,另一个线程正在基于该 volume 创建快照,或者同时有两个线程同时执行挂载操作等。cinder-volume 也是使用锁机制实现资源的并发访问,volume 的删除、挂载、卸载等操作都会对 volume 加锁。在 OpenStack Newton 版本以前,Cinder 的锁实现都是基于本地文件实现的,该方法在前面已经介绍过,只不过并不是根据文件是否存在来判断锁状态,而是使用了 Linux 的 flock 工具进行锁管理。Cinder 执行加锁操作默认会从配置指定的 lockpath 目录下创建一个命名为 cinder-volume_uuid-{action}的空文件,并对该文件使用 flock 加锁。flock 只能作用于同一个操作系统的文件锁,即使使用共享存储,另一个操作系统也不能判断是否有锁,一句话说就是 Cinder 使用的是本地锁。

我们知道 OpenStack 的大多数无状态服务都可以通过在不同的主机同时运行多个实例来保证高可用,即使其中一个服务挂了,只要还存在运行的实例就能保证整个服务是可用的,比如 nova-api、nova-scheduler、nova-conductor 等都是采用这种方式实现高可用,该方式还能实现服务的负载均衡,增加服务的并发请求能力。而极为不幸的是,由于 Cinder 使用的是本地锁,导致 cinder-volume 服务长期以来只能支持 Active/Passive(主备)HA 模式,而不支持 Active/Active(AA,主主) 多活,即对于同一个 backend,只能同时起一个 cinder-volume 实例,不能跨主机运行多个实例,这显然存在严重的单点故障问题,该问题一直以来成为实现 Cinder 服务高可用的痛点。

因为 cinder-volume 不支持多实例,为了避免该服务挂了导致 Cinder 服务不可用,需要引入自动恢复机制,通常会使用 pacemaker 来管理,pacemaker 轮询判断 cinder-volume 的存活状态,一旦发现挂了,pacemaker 会尝试重启服务,如果依然重启失败,则尝试在另一台主机启动该服务,实现故障的自动恢复。该方法大多数情况都是有效的,但依然存在诸多问题:

  • 在轮询服务状态间隔内挂了,服务会不可用。即不能保证服务的连续性和服务状态的实时性。
  • 有时 cinder-volume 服务启动和停止都比较慢,导致服务恢复时间较长,甚至出现超时错误。
  • 不支持负载均衡,极大地限制了服务的请求量。
  • 有时运维不当或者 pacemaker 自身问题,可能出现同时起了两个 cinder-volume 服务,出现非常诡秘的问题,比如 volume 实例删不掉等。

总而言之,cinder-volume 不支持 Active/Active HA 模式是 Cinder 的一个重大缺陷。

3.2 Cinder 的"进化"

玩过三国杀的都知道,很多武将最开始都比较脆弱,经过各种虐杀后会触发觉醒技能,又如 pokemon 完成一次进化,变得异常强大。cinder-volume 不支持 AA 模式一直受人诟病,社区终于在 Newton 版本开始讨论实现 cinder-volume 的 AA 高可用,准备引入分布式锁替代本地锁,这可能意味着 Cinder 即将完成一次功能突发变强的重大进化。

Cinder 引入分布式锁,需要用户自己部署和维护一套 DLM,比如 ZooKeeper、Etcd 等服务,这无疑增加了运维的成本,并且也不是所有的存储后端都需要分布式锁。社区为了满足不同用户、不同场景的需求,并没有强制用户部署固定的 DLM,而是采取了非常灵活的可插除方式,使用的正是前面介绍 Tooz 库。当用户不需要分布式锁时,只需要指定后端为本地文件即可,此时不需要部署任何 DLM,和引入分布式锁之前的方式保持一致,基本不需要执行大的变更。当用户需要 cinder-volume 支持 AA 时,可以选择部署一种 DLM,比如 ZooKeeper 服务。

Cinder 对 Tooz 又封装了一个单独的 coordination 模块,其源码位于 cinder/coordination.py,需要使用同步锁时,只需要在函数名前面加上 @coordination.synchronized 装饰器即可 (有没有突然想到 Java 的 synchronized 关键字 ),方便易用,并且非常统一,而不像之前一样,不同的函数需要加不同的加锁装饰器。比如删除 volume 操作的使用形式为:

为了便于管理多存储后端,Cinder 同时还引入了 cluster 的概念,对于使用同一存储后端的不同主机置于一个 cluster 中,只有在同一个 cluster 的主机存在锁竞争,不同 cluster 的主机不存在锁竞争。不过截至到刚刚发布的 Ocata 版本,cinder-volume 的 AA 模式还正处于开发过程中,其功能还没有完全实现,还不能用于生产环境中部署。我们期待 cinder-volume 能够尽快实现 AA 高可用功能,云极星创也会持续关注该功能的开发进度,并加入社区,与更多开发者一起完善该功能的开发。

参考文献

  1. Building a Distributed Lock Revisited: Using Curator’s InterProcessMutex
  2. Distributed lock manager
  3. 并发控制
  4. Ocata Series Release Notes
  5. Distributed group management and locking in Python with tooz
  6. Cinder Volume Active/Active support - Manager Local Locks
  7. etcd API: Waiting for a change
  8. The Chubby lock service for loosely-coupled distributed systems
  9. OpenStack 中 tooz 介绍及实践
  10. A Cinder Road to Active/Active HA

作者介绍

付广平,云极星创研发工程师,从事 Nova、Cinder 研发工作,Openstack 代码贡献者。北京邮电大学硕士毕业,曾先后在华为北研所 2012 实验室、英特尔中国研究院,以及阿里巴巴技术保障部实习,从事大数据和云计算相关工作。


感谢木环对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-05-18 17:242725

评论

发布
暂无评论
发现更多内容

anyRTC 6月SDK更新迭代

anyRTC开发者

音视频 WebRTC 实时通讯sdk

降低网络拥塞,追求美好体验——对话拍乐云首席科学家章琦

拍乐云Pano

模块二作业-微信朋友圈复杂度分析

babos

#架构实战营

神奇的Duff's device

实力程序员

字节跳动面试:来自阿里巴巴佛系安卓程序员的指南

欢喜学安卓

android 程序员 面试 移动开发

Flink + Iceberg + 对象存储,构建数据湖方案

Apache Flink

flink

IPFS一台矿机的成本多少钱?IPFS矿机收益如何?

HarmonyOS开发者创新大赛作品《智能农场》相关开发技术分享

科技汇

决定中国SaaS成败的三个关键问题

ToB行业头条

SaaS

质量基础设施一站式服务平台建设,NQI平台解决方案

颠覆传统经营模式,区块链助力餐饮行业数字化革新

旺链科技

数字化 区块链技术 餐饮

架构实战营 模块八作业

冬天的树

字节跳动技术总监自爆:大牛带你直击优秀开源框架灵魂

欢喜学安卓

android 程序员 面试 移动开发

Selenium4前线快报

FunTester

软件测试 自动化测试 测试开发 selenium

上手后才知道,这套仪表盘系统用起来是真的爽!

尔达Erda

开源 微服务 运维 APM msp

关于数据库时区,这么多奥秘你都知道么?

华为云开发者联盟

数据库 操作系统 时间 时区 GaussDB(DWS)

模块8 作业

Chris Cheng

架构训练营

RAID-0-1-5-10 搭建及使用-删除 RAID 及注意事项

学神来啦

云计算 Linux linux运维 raid

我是一个请求,我是如何被发送的?

华为云开发者联盟

注解 流程 CSE 请求 RestTemplat

大型团队的敏捷项目管理实践与思考

万事ONES

项目管理 敏捷开发 ONES 开发管理

字节跳动技术总监自爆:万字Android技术类校招面试题汇总

欢喜学安卓

android 程序员 面试 移动开发

字节跳动技术总监自爆:看完你还觉得算法不重要

欢喜学安卓

android 程序员 面试 移动开发

详解SQL优化必备:并行执行框架和执行计划

华为云开发者联盟

sql SQL优化 执行计划 GaussDB(for openGauss) 并行执行框架

听说过对 Go map 做 GC 吗?

万俊峰Kevin

map Go 语言

架构师之于团队的作用和其能力体现是什么?

happlyfox

话题讨论

5G消息盛事来袭|2021中国移动创客马拉松大赛5G消息专题赛即将启动!

5G消息

开发者 创客开发 开发者大赛 5G消息

那些必须要掌握的Hive数据倾斜与调优手段

云祁

7月日更

我发现了Chrome的一个bug

wzx

JavaScript chrome

IPFS最新消息是什么?IPFS官网最新资讯是什么?

IPFS

【源码篇】Flutter GetX深度剖析 | 我们终将走出自己的路(万字图文)

小呆呆666

flutter ios android 大前端

Python OpenCV 霍夫(Hough Transform)直线变换检测原理,图像处理第 33 篇博客

梦想橡皮擦

7月日更

细谈分布式锁及在OpenStack上的应用:如何实现Active/Active高可用_语言 & 开发_付广平_InfoQ精选文章