分布式读写锁的奥义:上古世代 ZooKeeper 的进击

简介: 本文作者将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。

在分布式系统中,通常会采用分布式锁来解决共享资源的互斥访问问题,而分布式读写锁则可以进一步提高读请求的并发。阿里云存储团队自研的分布式锁服务——女娲,凭借在性能、可扩展性和可运维性上的技术优势,使得阿里云存储在稳定、可靠、高性能等方面不断突破,进而为客户业务的永续运营提供了有力保障。在业界中,类似女娲的软件包括有 Apache ZooKeeper,Kubernetes Etcd,以及 Google Chubby,“他山之石,可以攻玉”,女娲的技术演进也在持续参考并吸收业界的最佳实践。本文,我们将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。


1.读写锁问题由来

Apache ZooKeeper (下文简称 ZK)是开源社区一款比较知名的分布式协同软件,通过提供基础原语语义,开发者可以较为容易地开发出分布式锁、服务发现、元数据存储等关键的分布式管控功能。作为 Hadoop 生态圈早期成员,ZK 对业界的影响不可谓不深远。


除了常见的常规文件(Normal File)与临时文件(Ephemeral File),ZK 还提供了一种特别的顺序文件(Sequential File),业界知名的开源库 - Apache Curaor在 ZK 的顺序文件语义之上封装出读写锁、公平锁、分布式队列、分布式 Barrier 等诸多优雅功能。那么 ZK 的顺序文件是怎样实现的呢?ZK 顺序文件的创建依赖于其父目录节点的 cversion 属性。每个目录节点都会记录其子节点的创建和删除次数,此即为 cversion,当 ZK 收到创建顺序文件请求的时候,其会使用该文件父目录的 cversion 作为文件名的后缀,那么就可以保证先后创建的顺序文件后缀值是单调增加的。所谓无心插柳柳成荫,当初为订阅目录下节点增删事件所设计的属性,今后居然演变成了 ZK 场景下某种逻辑时间戳 ,孵化出了类似读写锁这样重要的功能特性。


如下图所示,譬如我们发送路径为“/vm/vsp-xxxxx/_READ_”的顺序文件的创建请求,后端实际创建的文件节点名为“/vm/vsp-xxxxx/_READ_0000000001”,之后再创建名为“/vm/vsp-xxxxx/_WRIT_”的顺序文件,后端实际创建的文件节点名为“/vm/vsp-xxxxx/_WRIT_0000000002”。如果我们把前者认为是读锁,那么就订阅“/vm/vsp-xxxxx/”目录下所有的节点,并且把子节点按照最后 10 位的后缀排序。如果发现不存在序号小于自身的写锁,那么自己就算是持锁成功了;如果我们把后者认为是写锁,那么就订阅“/vm/vsp-xxxxx/”目录下所有的节点,并且把子节点按照最后 10 位的后缀排序。如果发现不存在序号小于自身的读锁,也不存在序号小于自身的写锁,那么自己就算是持锁成功了。这样就实现了简单且优雅的分布式读写锁。类比单机系统中使用的读写锁,分布式读写锁对于业务运行性能上提升的意义,同样是不言而喻的。

image.png


行文至此,大家应该也意识到,基于 ZK 实现的分布式读写锁,关键之处在于顺序文件(Sequential File)的后缀序号顺序性必须得到保障,即使在 failover 等场景下该序号顺序性也依旧严格维持。想象一下,ZK 系统当前认为“/vm/vsp-xxxxx”目录节点对应的 cversion 值为 5,然后 failover 之后,新当选的 Leader 认为 cversion 值为 4,那就糟糕了,可能就有新旧两个客户端自认为自己创建的顺序文件的序号都是 5,那么基于此特性实现的选主、抢锁等功能属性就会陷入「双主」、「一锁多占」等极其危险的境地,分布式存储的应用场景下,甚至会导致数据被写坏。


这个也正是本文要探讨的重点,ZK 顺序节点的后缀序号的顺序性是如何得到保障的,特别是在 failover 等场景下是如何实现的?“谁能书阁下,白首太玄经”,通过对这个精巧案例的分析,也希望大家能够体会到女娲在分布式协调领域可谓“补天”般的苦心经营。



2.Failover 的隐患


2.1幂等的陷阱

ZK 是一个复制状态机模型,如下图所示,不同于存储 I/O 依赖链路上的 LSM-Tree 模型,ZK 的复制状态机是完全的内存模型,这样提供了最佳的访问性能(当然,这个也限制了 ZK 的存储容量,不过其定位即为状态管理,容量很难成为其瓶颈)。此外,复制状态机系统在使用共识协议保证事务日志一致性的基础上,工程实现时通常引入快照机制对事务日志进行必要的垃圾回收,以此加速 failover 恢复速度。对于 ZK 而言,为了不影响前台业务访问,其打快照过程必然得是异步的,即递归地遍历内存中 DataTree 的数据结构并持久化到磁盘上的快照文件。在打快照过程中,ZK 继续处理事务请求并更新到内存的 DataTree。换言之,实际持久化到磁盘上的快照文件,记录的是一个“混合”的系统状态,其并不真实存在于任何一个历史时刻。

image.png

再以下图为例,在 ZK 打快照过程中,实际上系统也在处理 op1、op2、op3 事务日志,这个带来了一个问题,当我们快照持久化了某个父目录节点(包括其 cversion 值),其下面的子节点在后续的持久化时刻,可能已经处理了新的 CREATE 或者 DELETE 等事务请求,导致快照中该父目录下实际孩子节点状态并非其记录父目录 cversion 所对应时刻的状态。相应地,在回放事务日志时候,状态机会先加载最新的快照文件到内存中作为初始版本的 datatree,而后根据快照中维护的开始打快照时刻对应的事务日志序号,往后一条一条地回放事务日志,对应图中即为从 op1 开始,逐条日志回放。所以,即使持久化的快照中可能已经包含了执行完 op1、op2、op3 之后的状态,在回放的时候仍然会重新再执行一遍这些事务日志。这是大部分异步打快照的系统普遍面临的问题,对于 ZK 来说,需要在回放事务日志重建内存状态过程中合理应对。

image.png

因为 ZK 是个幂等的系统设计,因此 failover 场景下在回放事务日志时候,即使存在部分重合的日志应用(APPLY)至状态机,这里是没有数据正确性风险的。但是对于 failover 场景下重新计算出最新的 cversion 值则存在大挑战。考虑如下图的这种情况,快照在 index:2 执行完才开始序列化父目录,此时记录的父目录 cversion 值为 2,后继时刻 3 开始持久化子节点,此时快照记录的子节点只有“/root/B”。

image.png

那么这个例子下 failover 恢复的步骤是怎么样的呢?请看下表,最终恢复出来的目录 cversion 值正确,并且孩子节点信息也是正确的。实际上,按照这个步骤最终恢复回来的 cversion 不会小于 failover 之前的 cversion,因为无论如何 cversion 总是会在快照的起始 index 之后才被写入的快照文件,而后继回放事务日志时,每次创建和删除也都使 cversion 递增,所以最终的 cversion 值一定大于等于 failover 之前内存所维护的 cversion 值。

image.png


2.2顺序不顺序

在 ZK 早期的 及其之前的版本,由于回放事务日志的逻辑没有与正常处理事务日志的逻辑分离开,所以遇到删除节点 A 而 A 不存在的这种场景(即图中标红的步骤),会按照事务执行失败进行处理,不再更新 cversion,由此会导致 cversion 值相比 failover 之前会有回退。

image.png

这个问题当然不难修复。这个算是社区里面第一次意识到了这个问题,并正式揭开了“ failover 场景下目录 cversion 值一致性的保卫战”。下图即为当初社区针对这个问题的讨论。

image.png

3.社区的一次出手

在 ZK 开源版本 之前的版本,都是使用的上面表 1 中的修复方案。即将「正常处理事务请求」 和 「failover 后回放事务请求」这两种场景区分开,回放过程处理删除事务请求遇到上面所说的 NoNodeException,同样更新父目录的 cversion 即可。

image.png

可以看到另开一路实现了回放事务日志的逻辑,「已存在的节点再次被创建」,以及「不存在的节点再次被删除」,这两种情形都可以触发目录 cversion 的更新,使得 cversion 一定比 failover 之前大。总算可以松一口气,不担心顺序文件序号回退触犯「双主」或者「双锁」的天条了,但这种修复方案可能会令读者很自然地生起疑问,这样做单调性是可以保证了,但是幂等性呢?failover 前执行时更新的 cversion 是不是在回放过程中会被再更新一次?这样不优美的修复会遗留潜在风险吗?


4.社区的二次出手

社区的开发者们当然也有这些顾虑,既然处理事务日志的同时更新、维护 cversion 的过程不是幂等的,这就可能会导致 ZK 后端不同节点上,关于同一个目录文件的 cversion 值可能不一致,所以终极版本的修复需要将目录 cversion 值直接跟着事务日志一起持久化,即应该在创建和删除的事务日志中记录父目录的 cversion 值。

image.png

比如下图这种情况,由于打快照是异步的过程,不同节点(这里的节点指的是 ZK 服务器)上快照的状态并不相同。有相同起始 index 的「可能状态 1 」 和「可能状态 2 」的快照 ,都回放了全部事务日志之后,由于「可能状态 2」的起始 cversion 更大,所以最终计算出的 cversion 也更大。节点间对同一个目录 cversion 的值判断不一致,不发生重新选举的时候还好,总归 Quorum Leader 来分配顺序节点的序号能保证单调,一旦切主就会面临切到一个 cversion 更小的 ZK 节点的困扰,归根结底各个节点依赖自己打异步快照不可靠,快照无法在节点间达成一致。

image.png

因此自然的解决方法有两种,一种是想办法使得 snapshot 在节点间标准一致;另一种是改为依赖本身各个节点间就一致的事务日志,靠一致性协议来保证。调整打 snapshot 方式的改动会比较大,核心点在于 snapshot 本身能与某一个时刻的事务日志 index 保持严格的一对一映射,但这样势必需要引入像是 LSM-Tree 模型的本地存储引擎,替换掉内存中的 datatree 才可以做到,不然必须停服等待内存 dump 到磁盘。


所以社区最终采用了更轻量的改动,按照使用事务日志的思路进行的修复,在具体实现上,ZK 之父 Flavio 提了一个建议,即不需要在创建和删除的事务日志中都维护 cversion 字段,只在创建中维护即可,因为 cversion 为目录下所有创建删除操作调用的次数,用创建事务日志记录的 parentCVersion 减去当前目录下子节点数便可以间接计算出删除操作的次数,这样可以进一步减少改动量。

image.png

只在 CreateTxn 中增加了 parentCVersion 的字段,回放事务日志过程遇到 CREATE 事务便计算,作为最终重建内存 DataTree 的 cversion 来确保涵盖所有删除操作。

image.png

image.png

5.纸上得来终觉浅

自 2009 年飞天建立之初,女娲便在分布式协调领域不断躬行摸索,相较于基于异步复制的分布式系统(如 mysql,tair,redis),基于 Paxos/Raft/ZAB 等共识协议的分布式协调系统可以更严格地保证数据安全,而在上述一致性系统的基础之上结合 Lease 机制便可以提供安全的粗粒度锁(这种提法,首见于 Google Chubby 的经典论文),女娲提供的分布式锁的机理也不外如是。


对于女娲这类最基础的服务而言,由于服务着大量的业务,支持着丰富的场景,最是需要切磋琢磨的功夫,在寻常处见功力,细微处见真章。比如支持分布式读写锁功能的过程中,针对 cversion 这样很小的一个细节,如果没有从社区踩坑记录中汲取经验,没有完善的测试进行检查,那就会在线上惹出大祸。




来源  |  阿里云开发者公众号
作者  |  僧泉、云锋

相关文章
|
1天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
18天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
21天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
13天前
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
|
6天前
|
人工智能 自然语言处理 前端开发
100个降噪蓝牙耳机免费领,用通义灵码从 0 开始打造一个完整APP
打开手机,录制下你完成的代码效果,发布到你的社交媒体,前 100 个@玺哥超Carry、@通义灵码的粉丝,可以免费获得一个降噪蓝牙耳机。
3502 13
|
25天前
|
缓存 监控 Linux
Python 实时获取Linux服务器信息
Python 实时获取Linux服务器信息
|
11天前
|
人工智能 自然语言处理 前端开发
什么?!通义千问也可以在线开发应用了?!
阿里巴巴推出的通义千问,是一个超大规模语言模型,旨在高效处理信息和生成创意内容。它不仅能在创意文案、办公助理、学习助手等领域提供丰富交互体验,还支持定制化解决方案。近日,通义千问推出代码模式,基于Qwen2.5-Coder模型,用户即使不懂编程也能用自然语言生成应用,如个人简历、2048小游戏等。该模式通过预置模板和灵活的自定义选项,极大简化了应用开发过程,助力用户快速实现创意。
|
13天前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
5651 10
|
7天前
|
人工智能 C++ iOS开发
ollama + qwen2.5-coder + VS Code + Continue 实现本地AI 辅助写代码
本文介绍在Apple M4 MacOS环境下搭建Ollama和qwen2.5-coder模型的过程。首先通过官网或Brew安装Ollama,然后下载qwen2.5-coder模型,可通过终端命令`ollama run qwen2.5-coder`启动模型进行测试。最后,在VS Code中安装Continue插件,并配置qwen2.5-coder模型用于代码开发辅助。
576 4
|
10天前
|
云安全 人工智能 自然语言处理