速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

细谈分布式锁及在 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:242808

评论

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

2022年广州市等保测评公司新排名看这里!

行云管家

网络安全 等保 等保测评 广州 等保测评公司

解读分布式调度平台Airflow在华为云MRS中的实践

华为云开发者联盟

Python spark airflow 华为云MRS 大数据集群

许北林:我为什么加入OpenHarmony生态?又为什么要做“启航KP”开发套件?

OpenHarmony开发者

OpenHarmony 开发者故事

涛思数据与中天钢铁签署战略合作协议,加速钢铁行业的数字化发展

TDengine

数据库 tdengine

Google Guava中EventBus使用不当会导致什么故障?

BUG侦探

kafka Guava EventBus

AI简报-Image Colorization调研

AIWeker

深度学习 5月月更 AI简报 Image Colorization

Niobe开发板:基于OpenHarmony操作系统进行多线程(多任务)开发

拓维信息

OpenHarmony

音视频开发进阶课程|第一期:音频要素

ZEGO即构

RTC 音视频开发 音视频课程 音视频基础入门

520,解锁开发者的专属浪漫

葡萄城技术团队

情人节 520

520,用Python定制你的《本草纲目女孩》

华为云开发者联盟

Python 华为云 modelarts 本草纲目女孩 MoXing

跨平台应用开发进阶(八) :uni-app 实现Android原生APP-云打包集成极光推送(JG-JPUSH)详细教程

No Silver Bullet

uni-app 极光推送 5月月更 云打包

31点经验分享与吐槽

老白鹿

Tech Talk 活动预告丨云原生 DevOps 的 Kubernetes 技巧

亚马逊云科技 (Amazon Web Services)

云原生

科创人·智慧芽技术副总裁屠昶旸:技术之路是挑战之路,不愿在大厂空耗岁月

科创人

所谓测试报告

FunTester

Seata 企业版正式开放公测

阿里巴巴云原生

阿里云 开源 云原生 seata

如何在30分钟完成表格增删改查的前后端框架搭建

葡萄城技术团队

前端 前后端 系统搭建 表格系统

作为软件工程师,给年轻时的自己的建议(上)

禅道项目管理

程序员 工程师 职业成长

跨平台应用开发进阶(七) :uni-app 自定义 showToast

No Silver Bullet

uni-app 5月月更 吐司弹窗 跨终端

使用 jMeter 对需要 User Authentication 的 Restful API 进行并发负载测试

汪子熙

Java Jmeter 性能测试 SAP 5月月更

比渗透测试更有用,红队演练该如何开展?

青藤云安全

MySQL缓存策略分析

C++后台开发

MySQL 数据库 后端开发 Linux服务器开发 C++后台开发

【小知识】云管理平台与一般管理系统有什么区别?

行云管家

云计算 云管理平台 云管理

FlyFish|前端数据可视化开发避坑指南(一)

云智慧AIOps社区

JavaScript 前端 node,js 数据可视化工具

架构实战营 第 6 期 模块六课后作业

火钳刘明

#架构实战营 「架构实战营」

业务逻辑的灵魂在哪里?

清林情报分析师

数据分析 数据建模 数据可视化 分析软件 分析思维

数据分析软件有哪些分类?

清林情报分析师

数据分析 数据可视化 知识图谱 分析软件 分析工具

大数据培训在 Presto 中使用哈希改善动态集群缓存命中率

@零度

郑重声明

Authing

身份云 Idaas

飞书、钉钉和企微的三巨头之争下,其他厂商在移动平台赛道如何奋起直追?

WorkPlus

如何在 Web 应用里消费 SAP Leonardo 的机器学习 API

汪子熙

机器学习 前端开发 前端框架 SAP 5月月更

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