• MySQL Raft的用法
  • 发布于 2个月前
  • 327 热度
    0 评论

Meta 推出 MySQL Raft 的目的是最终取代当前的 MySQL 半同步数据库(semisynchronous databases)。MySQL Raft 的最大优势是简化了操作,使 MySQL 服务器能够处理提升和成员身份的问题。这一改进确保了 Raft 协议的可靠性,并极大地减轻了运维工作的负担。


Meta 运行着全球最大规模之一的 MySQL 部署。这个部署支撑着社交关系图谱以及许多其他服务,如消息传递、广告和信息流。在过去几年中,我们实施了 MySQL Raft,这是一个集成到 MySQL 中的 Raft 一致性引擎,用于构建复制状态机。大部分部署已经迁移到了 MySQL Raft,并计划完全用它替代当前的 MySQL 半同步数据库。这个项目为 Meta 的 MySQL 部署带来了巨大的好处,包括更高的可靠性、可证明的安全性、故障切换时间的显著改进以及操作的简便性,同时在写入性能方面保持了相等或接近的水平。

一.背景
为了实现高可用性、容错性和读取的扩展性,Meta 的 MySQL 数据存储是一个具有数百万个分片、持有多个 PB 级数据的大规模分片和地理复制部署。该部署涵盖了数千台机器,分布在多个地区和数据中心,跨越多个大陆。

在以前,我们的复制解决方案使用了 MySQL 的半同步(semisync)复制协议。这是一个仅涉及数据路径的协议。MySQL 主节点使用半同步复制将数据复制到主节点所在区域之外但不在主节点故障域内的两个仅记录日志的副本(logtailers)。这两个 logtailers 充当半同步的 ACKer(ACK 是向主节点确认事务已在本地写入)。


这样可以实现数据路径上非常低延迟(亚毫秒级)的提交,并为写入操作提供高可用性和耐久性。常规的 MySQL 主节点到副本的异步复制用于向其他区域进行更广泛的分发。


控制平面操作(例如晋升、故障转移和成员变更)由一组 Python 守护程序(以下简称自动化程序)负责处理。自动化程序负责进行必要的编排,将故障转移位置的新 MySQL 服务器晋升为主服务器。


同时,自动化程序还会将先前的主服务器和剩余的副本指向新的主服务器进行复制。成员变更操作由名为 MySQL 池扫描器(MPS)的另一个自动化程序进行编排。


当需要添加新的成员时,MPS 会将新的副本指向主服务器,并将其添加到服务发现存储中。故障转移是一个更为复杂的操作,需要关闭日志副本(半同步 ACKer)的尾随线程,以隔离先前故障的主服务器。


二.为什么需要 MySQL Raft?
过去,为了确保安全性并避免在复杂的晋升和故障转移操作中出现数据丢失,我们使用了多个自动化守护程序和脚本,利用锁定、编排步骤、隔离机制和服务发现系统(SMC)。这是一个分布式设置,但在原子级别上完成这些操作非常困难。随着需要修补越来越多的特殊情况,自动化变得越来越复杂且难以维护。

为了彻底改变这种情况,我们采用了完全不同的方法。我们增强了 MySQL,并将其打造成一个真正的分布式系统。我们认识到,控制平面操作(如晋升和成员变更)是大多数问题的根源,因此我们希望控制平面和数据平面操作成为同一个复制日志的一部分。为此,我们使用了被广泛认可的一致性协议 Raft。这也意味着成员和领导权的真实信息转移到了服务器内部(mysqld)。这是引入 Raft 的最大贡献,因为它使得 MySQL 服务器能够通过可证明的正确性(安全性属性)来处理晋升和成员变更。

三Raft 库和 MySQL Raft 插件
我们基于 Apache Kudu 实现了 MySQL 的 Raft 协议,并对其进行了显著增强以满足 MySQL 和我们的部署需求。我们将这个修改版本作为一个开源项目发布,命名为 kuduraft。

我们在 kuduraft 中添加了一些关键功能,包括:
FlexiRaft,支持两种不同的交叉仲裁:数据仲裁和领导者选举仲裁
代理功能,可以使用代理中间节点来减少网络带宽。
压缩功能,对二进制日志(事务)的有效载荷进行压缩后再进行分发。
日志抽象化,支持不同的物理日志文件实现。

主服务器禁止功能,临时阻止某些实体成为主服务器。


此外,为了与 Raft 协议进行交互,我们还对 MySQL 复制功能进行了相当大的改动。我们创建了一个名为 MyRaft 的新的闭源 MySQL 插件。MySQL 通过插件 API 与 MyRaft 进行交互(类似的 API 也用于半同步复制),同时我们为 MyRaft 创建了一个单独的 API,以与 MySQL 服务器进行交互(回调)。


四.MySQL Raft 复制拓扑
Raft 环将包含位于不同地区的几个 MySQL 实例(图表中为四个)。这些地区之间的通信往返时间(RTT)将在 10 到 100 毫秒之间。其中几个 MySQL 实例(通常是三个)被允许成为主服务器,而其余的实例只能作为纯读取副本(非主服务器能力)。

在 Meta 的 MySQL 部署中,还对极低延迟的提交有着长期的需求。使用 MySQL 作为存储的服务(如社交图谱)需要或者已经设计成具有极快的写入能力。

为了满足这一需求,FlexiRaft 的配置将只使用区域内的提交(单区域动态模式)。为实现这一目标,每个具备主服务器能力的区域将额外拥有两个日志追随者(见证者或仅记录实体)。写入操作的数据法定人数将为 2/3(即 1 个 MySQL + 2 个日志追随者中的 2 个 ACK)。


Raft 仍然将在所有实体(1 个具备主服务器能力的 MySQL + 2 个日志追随者)* 3 个区域 + (非主服务器能力的 MySQL)* 3 个区域 = 12 个实体之间管理和运行复制日志。


Raft 角色:Leader,顾名思义,在复制日志的一个任期内担任领导者角色。在 Raft 中,领导者也是 MySQL 中的主服务器,负责接收客户端的写入请求。


追随者(Follower)是环中具有投票权的成员,被动地接收领导者发送的消息(AppendEntries)。在 MySQL 的角度看,追随者是副本,并将事务应用到其引擎上。它不允许来自用户连接的直接写入(设置为 read_only=1)。学习者(Learner)是环中无投票权的成员,例如,非主服务器能力区域中的三个 MySQL 实例(如上所述)。在 MySQL 的角度看,学习者是副本。



五.复制日志
对于复制,MySQL 历来使用二进制日志格式。这个格式对于 MySQL 的复制非常关键,我们决定保留它。从 Raft 的角度来看,二进制日志成为了复制日志。我们通过改进 kuduraft 的日志抽象来实现这一点。MySQL 的事务将被编码为一系列事件(例如,更新行事件),每个事务都有一个开始和结束。二进制日志也包含适当的头部信息,并且通常以结束事件(Rotate 事件)结尾。

我们需要调整 MySQL 内部对日志的管理方式。在主服务器上,Raft 会写入 binlog,这与标准 MySQL 的操作没有区别。在副本中,Raft 也会将数据写入 binlog,而不是像标准 MySQL 中那样写入独立的中继日志。


这样做简化了 Raft 的实现,因为 Raft 只需关注一个日志文件命名空间。如果一个追随者被提升为领导者,它可以无缝地回溯其日志历史,将事务发送给落后的成员。副本的应用程序线程会从 binlog 中提取事务,并将其应用到数据库引擎中。在此过程中,会创建一个新的日志文件,即应用日志。应用日志在副本的崩溃恢复中起着重要的作用,但它不是一个被复制的日志文件。    


简而言之。

在标准的 MySQL 中:

1.主服务器将数据写入 binlog,并将 binlog 发送给副本。

2.副本通过 relay log 接收数据,并将事务应用到引擎中。在应用过程中,会创建一个新的仅供副本使用的 binlog。

在 MySQL Raft 中:

1.主服务器通过 Raft 将数据写入 binlog,并将 binlog 发送给追随者 / 副本。

2.追随者 / 副本通过 binlog 接收数据,并将事务应用到引擎中。在应用过程中,会创建一个应用日志。

3.从 Raft 的角度来看,binlog 是复制日志。


六.使用 Raft 在 MySQL primary 上写入事务
该事务首先在引擎中进行准备。这将在用户连接的线程中完成。准备事务将涉及与存储引擎(如 InnoDB 或 MyRocks)的交互,并生成事务的内存中的 binlog 数据。

在提交时,写操作将通过组提交 / 有序提交流程进行处理。会分配 GTID,并由 Raft 为事务分配 OpId(term:index)。此时,Raft 将压缩事务并将其存储在其 LogCache 中,同时将事务写入 binlog 文件。它将异步开始将事务发送到其他追随者,以获取 ACK 并达成共识。

处于事务 “提交” 状态的用户线程将被阻塞,等待 Raft 的共识。当 Raft 获得区域内三分之二的投票时,将达成共识提交。Raft 还会将事务发送到所有区域外的成员,但会忽略它们的投票,这是由 FlexiRaft 算法(下文有描述)决定的。
在达成共识提交后,用户线程将解除阻塞,事务将继续进行并提交到引擎中。引擎提交后,写查询将完成并返回给客户端。随后,Raft 将异步向下游的追随者发送提交标记(当前提交的 OpId),以便它们也可以将事务应用到其数据库中。


七.崩溃恢复
我们需要对崩溃恢复进行一些更改,以使其与 Raft 无缝配合。事务可能在任何时候发生崩溃,因此协议必须确保成员的一致性。以下是我们使其正常工作的一些关键想法:
事务未刷新到 binlog:在这种情况下,事务在内存中的数据(作为内存缓冲区)将丢失,并且在进程重新启动时,引擎中的准备事务将被回滚。由于 Raft 日志中没有额外的未提交事务,因此无需与其他成员进行协调。

事务已刷新到 binlog 但未达到其他成员:Mysqld 充当事务协调器,并在引擎和复制 binlog 之间运行两阶段提交协议。在崩溃恢复时,由于引擎尚未达到提交状态,因此会回滚引擎中的准备事务。Raft 将进行故障转移,并选举出新的领导者。新的领导者的 binlog 中没有该事务,因此当以前的领导者重新加入环时,通过向其推送 No-Op 消息来将该事务从以前的领导者的 binlog 中截断。

事务已刷新到 binlog 并传递给下一个领导者,但当前领导者在提交到引擎之前发生崩溃:与情况 2 类似,引擎中的准备事务将被回滚。以前的领导者将作为跟随者加入 Raft 环。在这种情况下,新的领导者的 binlog 中包含该事务,因此不会发生截断,因为日志是匹配的。当新的领导者发送提交标记时,事务将再次从头开始应用到引擎中。

八.Raft 启动的状态机转换
故障转移和常规维护操作可能会触发 Raft 中的领导权更改。选举出领导者后,MyRaft 插件将尝试将相应的 MySQL 服务器转换为主模式。为此,插件会协调执行一系列步骤。

从 Raft 到 MySQL 的回调将中止正在进行的事务、回滚正在使用的 GTID,将引擎端的日志从应用日志切换为 binlog,并最终设置适当的 read_only 设置。这个机制非常复杂,目前不是开源的。

九.FlexiRaft
由于 Raft 论文和 Apache Kudu 仅支持单个全局仲裁机制,这在 Meta 这样的环境中并不适用,因为环数较大但数据路径的仲裁机制需要较小的规模。

为了解决这个问题,我们对 FlexiRaft 进行了创新,借鉴了 Flexible Paxos 的思想。
在高层次上,FlexiRaft 允许 Raft 具有不同的数据提交仲裁机制(较小的规模),但会对领导者选举仲裁机制(较大的规模)产生相应影响。通过遵循仲裁交叉的可证明保证,FlexiRaft 确保 Raft 的最长日志规则和适当的仲裁交叉将保证可证明的安全性。
FlexiRaft 支持单区域动态模式。在此模式下,成员根据其地理区域进行分组。当前 Raft 的仲裁机制取决于当前的领导者(因此称为 “单区域动态”)。
数据提交仲裁机制是领导者所在区域的多数投票者。在进行领导者切换时,如果任期连续,候选者将与最后已知领导者的区域交叉。FlexiRaft 还确保候选者所在区域的仲裁机制也得到满足,否则后续的无操作消息可能会被阻塞。
在极少数情况下,如果任期不连续,FlexiRaft 将尝试确定一组需要与之交叉的区域,以确保安全性;否则,在最坏情况下,它将回退到 Flexible Paxos 的 N 区域交叉情况。通过预选举和模拟选举,任期间隔的发生非常罕见。

十.控制平面操作(促销和会员变更)
为了在 binlog 中串行化升级和成员变更事件,我们利用了 MySQL 二进制日志格式中的 Rotate Event 和 Metadata event。这些事件将携带 Raft 中的 No-Op 消息和添加成员 / 移除成员操作。

由于 Apache Kudu 不支持联合共识,因此我们只允许逐个进行成员变更(每次只能更改一个实体的成员资格),以遵守隐式仲裁交叉规则。

十一.自动化
通过实施 MySQL Raft,我们为 MySQL 部署实现了非常清晰的职责分离。MySQL 服务器通过 Raft 的复制状态机负责安全性,确保了不会丢失数据。自动化(使用 Python 脚本和守护程序)负责启动控制平面操作并监控群集的健康状况。

在维护期间或检测到主机故障时,自动化会通过 Raft 进行成员替换或升级。有时,自动化还可以更改 MySQL 拓扑的区域布置。将自动化适应 Raft 是一项庞大的任务,需要多年的开发和推广工作。

在长时间维护事件中,自动化会在 Raft 上设置领导者禁止信息。Raft 会阻止被禁止的实体成为领导者,或者在意外选举时迅速撤离它们。自动化还会将领导权转移至其他区域,远离被禁止实体所在的区域。

十二.部署过程的挑战与经验
对于团队来说,将 Raft 推广到整个群集是一次完美的学习过程。最初我们在 MySQL 5.6 上开发 Raft,然后不得不迁移到 MySQL 8.0 上。

其中一个关键的教训是,虽然使用 Raft 可以更容易地确保正确性,但 Raft 协议本身对于可用性并没有太多帮助。由于我们的 MySQL 数据仲裁组非常小(在区域内的三个成员中需要两个),如果该区域中有两个错误的成员,就会破坏仲裁组并降低可用性。


MySQL 群集每天都会经历相当大的变动(由于维护、主机故障、平衡操作),因此及时而正确地进行成员更改对于保持持续可用性至关重要。在推广过程中,我们主要关注及时更换 logtailer 和 MySQL 实例,以确保 Raft 仲裁组的健康状态。


为了增强可用性,我们对 kuduraft 进行了改进。这些改进不属于核心协议,可以看作是工程上的额外功能。Kuduraft 本身支持预选举,但只在故障转移期间进行。在领导权的平稳移交过程中,指定的候选者会直接进行真正的选举,并增加任期。这可能导致出现 “卡住的领导者” 的情况(kuduraft 不会自动下台)。


为了解决这个问题,我们添加了模拟选举功能,类似于预选举,但仅在领导权平稳移交时进行。由于这是一个异步操作,它不会增加升级时间。模拟选举可以解决真正选举部分成功但卡住的情况。


处理拜占庭故障:Raft 本身认可 Raft 的成员列表。但在添加新成员时或自动化过程中可能存在两个不同的 Raft 环相交的异常情况。这些僵尸成员节点需要被清理,并且它们之间不应该能够相互通信。我们在部署中发现这些罕见事件后,对 Raft 的实现进行了改进。我们实现了一个功能来阻止这些僵尸成员向环发送远程过程调用(RPC)。从某种程度上说,这是处理拜占庭故障的方法。

十三.监控
在推出 MySQL Raft 时,我们的目标之一是减少运维人员的操作复杂性,以便工程师能够迅速发现和解决问题。我们构建了多个仪表板、CLI 工具和 Scuba 表来监控 Raft。特别是在晋升和成员变更的领域,我们在 MySQL 中增加了大量的日志记录。

我们还开发了用于查看环的法定人数和投票报告的 CLI 工具,这有助于我们迅速确定环为何不可用(法定人数破裂)的原因。在工具和自动化基础设施方面的投资与服务器端的更改同步进行,这可能是一个比服务器更大的投资。这项投资取得了巨大的回报,减少了运维和入职过程中的痛苦。

十四.Quorum Fixer
尽管不希望发生,但法定人数偶尔会被破坏,导致可用性降低。典型情况是当自动化无法及时检测到环中的不健康实例或日志记录器并快速替换它们时。

这可能是由于检测不准确、工作队列过载或缺乏备用主机容量等原因引起的。相互关联的故障,即法定人数中的多个实体同时发生故障,这种情况并不经常发生。

尽管已经采取了适当的放置决策以在关键实体的法定人数中隔离故障域,但在规模化环境中仍会发生意外情况。因此,在生产环境中需要可用的工具来应对这些情况。我们开发了 “Quorum Fixer” 作为手动纠正工具。

“Quorum Fixer” 是用 Python 编写的手动修复工具,它会暂停环上的写操作。它会通过离线检查找出日志实体中最长的部分。然后,它会强制更改 Raft 内部的领导者选举中的法定人数预期,以确保选择的实体成为领导者。成功进行晋升后,我们会将法定人数预期重置回原始值,通常情况下环会恢复正常。


有意决定不自动运行该工具,因为我们希望通过追踪原因并识别所有法定人数丧失的情况来修复问题,而不是让自动化默默地解决这些问题。

十五.发布 MySQL Raft
在大规模部署中从半同步复制过渡到 MySQL Raft 是一项具有挑战性的任务。为此,我们开发了一个名为 enable-raft 的工具(使用 Python 编写),用于协调从半同步复制到 Raft 的过渡过程。该工具会在每个实体上加载插件并设置相应的配置(MySQL 系统变量)。

这个过程会导致环境短暂的停机时间。随着时间的推移,我们不断改进这个工具,使其能够快速而安全地在大规模环境中实施 Raft 部署。enable-raft 工具的开发使得我们能够顺利地完成 Raft 的部署。

十六.测试
毋庸置疑,对 MySQL 核心复制流程进行更改是一个非常困难的项目。由于涉及数据安全,测试对于建立信心至关重要。我们在项目中广泛使用阴影测试和故障注入。在每次 RPM 软件包管理器发布之前,我们会在测试环境中注入数千次故障转移和选举。我们会触发测试环境中的替换和成员变更,以触发关键的代码路径。

长期运行的测试和数据正确性检查也至关重要。我们拥有每晚在分片上运行的自动化测试,以确保主节点和副本节点的一致性。一旦发现任何不匹配,我们会立即收到警报并进行调试。

十七.性能
Raft 的写入路径延迟性能与半同步写(semisync)相当。尽管半同步机制稍微简单一些,因此预期更为高效,但我们对 Raft 进行了优化,使其能够达到与半同步写相同的延迟性能。我们优化了 kuduraft,尽管它承担了许多之前在服务器二进制文件之外的责任,但我们成功地保持了整个集群中的 CPU 负载不增加。

Raft 极大地提升了晋升和故障转移的速度。在集群中,优雅的晋升时间显著缩短,通常只需 300 毫秒即可完成。相比之下,在半同步设置中,由于服务发现存储是真实情况的来源,客户端察觉晋升完成的时间要更长,这导致分片上用户的停机时间增加。
Raft 通常在 2 秒内完成故障转移。这是因为我们每 500 毫秒发送一次 Raft 心跳以检测健康状态,并在连续三次心跳失败后发起选举。而在半同步环境中,这个过程需要进行繁重的编排,并且需要 20 到 40 秒的时间。因此,Raft 在故障转移情况下显著提升了 10 倍的停机时间。
#18

未来计划
Raft 通过提供可证明的安全性和简化操作,帮助解决了 Meta 在 MySQL 管理方面的问题。我们的目标是在管理 MySQL 一致性时减少干预,并为罕见的可用性丧失情况提供工具。

目前,我们已经在很大程度上实现了这些目标。Raft 为未来带来了重大机遇,我们可以专注于增强使用 MySQL 的服务。服务所有者之一的要求是实现可配置的一致性。可配置的一致性使得服务所有者在服务接入时可以选择是否需要跨区域的法定人数或特定地理区域(如欧洲和美国)的复制副本。

FlexiRaft 无缝支持这种可配置的法定人数,我们计划在未来逐步推出这一功能。这种可配置的法定人数将导致较高的提交延迟,但使用案例需要在一致性和延迟之间进行权衡(例如,PACELC 定理)。

由于 Raft 具备代理功能(能够使用多跳分布拓扑发送消息),它还可以节省跨大西洋的网络带宽。我们计划利用 Raft 将数据从美国复制到欧洲一次,然后利用 Raft 的代理功能在欧洲内部进行分发。


虽然这将增加延迟,但考虑到大部分延迟来自跨大西洋传输,额外的跳跃距离将大大缩短,因此额外的延迟将是可以接受的。在 Meta 的数据库部署和分布式共识领域,我们正在探索一些更具前瞻性的想法,例如研究无领导协议(如 Epaxos)。尽管我们当前的部署和服务是基于强领导协议的假设进行的,但我们开始看到一些需求,其中一些服务可以从更统一的 WAN 写入延迟中获益。


另一个我们正在考虑的想法是将日志与状态机(即数据库)进行解耦,采用分离式日志设置。这样做可以让团队能够分别处理日志和复制问题,而与数据库存储和 SQL 执行引擎的问题分开管理。

用户评论