<返回更多

在 Meta 构建和部署 MySQL Raft

2023-05-22  今日头条  闪念基因
加入收藏

 

作者:Anirban Rahut、Abhinav Sharma、Yichen Shen、Ahsanul Haque

在 Meta,我们运行着世界上最大的 MySQL 部署之一。该部署为社交图谱以及许多其他服务提供支持,例如消息、广告和提要。在过去的几年里,我们实现了 MySQL Raft,这是一个 Raft 共识引擎,与 MySQL 集成以构建复制状态机。我们已经将大部分部署迁移到 MySQL Raft ,并计划用它完全替换当前的 MySQL半同步数据库。该项目为 Meta 的 MySQL 部署带来了显着的好处,包括更高的可靠性、可证明的安全性、故障转移时间的显着改进以及操作的简单性——所有这些都具有相同或可比的写入性能。

背景

为了实现高可用性、容错和扩展读取,Meta 的 MySQL 数据存储是一个大规模分片、地理复制部署,具有数百万个分片,保存 PB 级数据。部署包括在多个地区和多个大洲的数据中心运行的数千台机器。

以前,我们的复制解决方案使用的是 MySQL半同步(semisync)复制协议。这是一个仅数据路径协议。MySQL 主节点将使用半同步复制到主区域内但在主节点故障域之外的两个仅记录副本 (logtAIlers)。这两个 logtailer 将充当半同步 ACKer(ACK 是对事务已在本地写入的主节点的确认)。这将允许数据路径具有非常低的延迟(亚毫秒)提交,并为写入提供高可用性/持久性。常规的 MySQL 主副本异步复制用于更广泛地分布到其他区域。

控制平面操作(例如,升级、故障转移和成员更改)将由一组 Python/ target=_blank class=infotextkey>Python 守护程序(以下称为自动化)负责。自动化将进行必要的编排,以将故障转移位置的新 MySQL 服务器提升为主要服务器。自动化还将指向先前的主副本和剩余的副本,以从新的主副本进行复制。成员更改操作将由另一个称为MySQL 池扫描器(MPS)的自动化来协调。要添加新成员,MPS 会将新副本指向主副本并将其添加到服务发现存储。故障转移将是一个更复杂的操作,其中 logtailer(半同步 ACKer)的尾线程将被关闭以隔离之前死掉的主线程。

为什么需要 MySQL Raft?

过去,为了在复杂的升级和故障转移操作期间帮助保证安全并避免数据丢失,一些自动化守护进程和脚本会使用锁定、编排步骤、防护机制和服务发现系统 SMC。这是一个分布式设置,很难以原子方式完成。随着越来越多的极端情况需要修补,随着时间的推移,自动化变得越来越复杂和难以维护。

我们决定采取完全不同的方法。我们增强了 MySQL 并使其成为真正的分布式系统。意识到升级和成员变更等控制平面操作是大多数问题的触发因素,我们希望控制平面和数据平面操作成为同一复制日志的一部分。为此,我们使用了众所周知的共识协议Raft。这也意味着成员资格和领导权的真实来源移动到服务器 (mysqld) 内部。这是引入 Raft 的最大贡献,因为它在 MySQL 服务器的促销和成员更改之间实现了可证明的正确性(安全属性)。

Raft 库和 MySQL Raft 插件

我们的 Raft for MySQL 实现基于Apache Kudu。我们根据 MySQL 和部署的需要对其进行了显着增强。我们将此分支作为开源项目kuduraft发布。

我们添加到 kuduraft 的一些关键特性是:

我们还必须对 MySQL 复制进行相对较大的更改以与 Raft 接口。为此,我们创建了一个名为 MyRaft 的新闭源 MySQL 插件。MySQL 将通过插件 API 与 MyRaft 接口(类似的 API 也已用于半同步),而我们为 MyRaft 创建了一个单独的 API 以与 MySQL 服务器接口(回调)。

 

MySQL Raft 复制拓扑

一个 Raft 环将由不同区域的多个 MySQL 实例(图中有四个)组成。这些区域之间的通信往返时间 (RTT) 范围为 10 到 100 毫秒。这些 MySQL 中的一些(通常是三个)被允许成为主要的,而其余的只允许成为纯读取副本(不具备主要能力)。Meta 的 MySQL 部署也对极低延迟提交有着长期的需求。使用 MySQL 作为存储的服务(例如,社交图谱)需要或已经设计为如此极快的写入。

为了满足这一要求,FlexiRaft 的配置将仅使用区域内提交(单区域动态模式)。为了实现这一点,每个主要的有能力的区域将有两个额外的 logtailer(见证或仅日志实体)。写入的数据法定人数为 2/3(1 个 MySQL + 2 个 logtailer 中的 2 个 ACK​)。Raft 仍然会管理和运行跨所有实体的复制日志(1 个具有主功能的 MySQL + 2 个 logtailers)* 3 个区域 +(不具有主功能的 MySQL)* 3 个区域 = 12 个实体。

Raft roles:leader,顾名思义,就是复制日志的一个term中的leader。Raft 中的领导者也将是 MySQL 中的主要领导者,并且接受客户端写入。follower 是环中的投票成员,被动接收来自 leader 的消息( AppendEntries )。从 MySQL 的角度来看,跟随者将是一个副本,并将事务应用于其引擎。它不允许从用户连接直接写入(设置了 read_only=1)。学习者将是环中的非投票成员,例如,非主要功能区域中的三个 MySQL(上图)。从 MySQL 的角度来看,它将是一个副本。

 

复制日志

对于复制,MySQL 历来使用二进制日志格式。这种格式是 MySQL 复制的核心,我们决定保留它。从 Raft 的角度来看,二进制日志变成了复制日志。这是通过对 kuduraft 的日志抽象改进完成的。MySQL 事务将被编码为一系列事件(例如,Update Rows 事件),每个事务都有开始和结束。二进制日志也有适当的标题,通常以结束事件(轮换事件)结束。

我们不得不调整 MySQL 在内部管理其日志的方式。在主节点上,Raft 会写入二进制日志。这与标准 MySQL 中发生的情况没有什么不同。在副本中,Raft 还会写入二进制日志,而不是标准 MySQL 中的单独中继日志。这为 Raft 创造了简单性,因为 Raft 只关心一个日志文件的命名空间。如果一个追随者被提升为领导者,它可以无缝地返回到它的日志历史记录中,将交易发送给落后的成员。副本的应用程序线程将从二进制日志中获取事务,然后将它们应用到引擎。在此过程中,将创建一个新的日志文件,即应用日志。此应用日志将在副本的崩溃恢复中发挥重要作用,但在其他方面是非复制日志文件。

所以,总结一下:

在标准 MySQL 中:

在 MySQL 筏中:

使用 Raft 在 MySQL primary 上写入事务

事务将首先在引擎中准备好。这将发生在用户连接的线程中。准备交易的行为将涉及与存储引擎(例如InnoDB或MyRocks)的交互,并为交易生成内存中的二进制日志有效负载。在提交时,写入将通过group commit /ordered_commit 流程。将分配 GTID,然后 Raft 将分配一个 OpId (term:index) 给交易。此时,Raft 会将事务压缩,存储在自己的 LogCache 中,并通过事务写入一个 binlog 文件。它将异步开始将交易发送给其他追随者以获得 ACK 并达成共识。

处于事务“提交”状态的用户线程将被阻塞,等待来自 Raft 的共识。当 Raft 获得三分之二的区域投票时,就会达成共识提交。Raft 还会将交易发送给所有区域外的成员,但会因为称为 FlexiRaft 的算法(如下所述)而忽略他们的投票。在一致提交时,用户线程将被解除阻塞,事务将继续并提交给引擎。引擎提交后,写入查询将完成并返回给客户端。不久之后,Raft 也会异步发送一个提交标记(当前提交的 OpId)给下游的追随者,这样他们也可以将事务应用到他们的数据库中。

 

崩溃恢复

必须对崩溃恢复进行更改,以使其与 Raft 无缝协作。在交易的生命周期中,崩溃随时可能发生,因此协议必须确保成员的一致性。以下是有关我们如何使其发挥作用的一些重要见解。

  1. Transaction was not flushed to binlog:在这种情况下,内存中的事务负载(仍然在 mysqld 进程内存中作为内存缓冲区)将丢失,并且引擎中准备好的事务将在进程重新启动时回滚。由于 Raft 日志中没有多余的未提交事务,因此不需要与其他成员进行对账。
  2. 事务被刷新到 binlog 但从未到达其他成员:Mysqld 充当事务协调器并作为参与者在引擎和复制的 binlog 之间运行两阶段提交协议。在崩溃恢复时,引擎(例如 InnoDB 或 MyRocks)中准备好的事务将被回滚(引擎尚未提交)。Raft 将进行故障转移,并选举出新的领导者。该领导者不会在其 binlog 中包含此事务,并且此后将从前领导者的 binlog 中截断此事务,因为当前任领导者重新加入环时(通过推送 No-Op 消息)。
  3. 事务被刷新到 binlog 并到达下一个领导者。Current leader 在提交给引擎之前就死了:类似于 no。2 上面,引擎中准备好的事务将被回滚。以前的领导者将作为追随者加入 Raft 环。在这种情况下,新领导者将在其二进制日志中包含此事务,因此不会发生截断,因为日志会匹配。当新领导者发送提交标记时,事务将从头开始重新应用。

Raft 启动的状态机转换

故障转移和定期维护操作可以触发 Raft 中的领导层变更。选出领导者后,MyRaft 插件将尝试将伴随的 MySQL 转换为主要模式。为此,该插件将编排一组步骤。这些来自 Raft → MySQL 的回调将中止正在进行的事务,回滚正在使用的 GTID,将引擎端日志从应用日志转换为二进制日志,并最终设置正确的只读设置。这个机制比较复杂,目前还没有开源。

灵活筏

由于Raft 论文和 Apache Kudu 仅支持单个全局仲裁,因此它在 Meta 上效果不佳,因为环很大但数据路径仲裁需要很小。

为了规避这个问题,我们在 FlexiRaft 上进行了创新,借鉴了Flexible Paxos 的思想。

在高层次上,FlexiRaft 允许 Raft 有不同的数据提交法定人数(小),但在领导者选举法定人数(大)上采取相应的命中。通过遵循群体交集的可证明保证,FlexiRaft 确保 Raft 的最长日志规则和适当的群体交集将保证可证明的安全性。

FlexiRaft 支持单区域动态模式。在这种模式下,成员按其地理区域分组在一起。Raft 的当前法定人数取决于当前领导者是谁(因此称为“单区域动态”)。数据法定人数是领导者所在地区的大多数选民。在晋升期间,如果任期是连续的,则候选人将与最后一个已知领导者的区域相交。FlexiRaft 还会确保也达到 Candidate 区域的法定人数,否则后续的 No-Op 消息可能会卡住。如果在极少数情况下项不连续,Flexi Raft 将尝试找出一组不断增长的区域,为了安全需要与之相交,或者在最坏的情况下,将退回到 Flexible Paxos 的 N 区域相交情况. 由于预选和模拟选举,

控制平面操作(促销和会员变更)

为了序列化binlog中的promotion和membership change事件,我们劫持了MySQL二进制日志格式的Rotate Event和Metadata事件。这些事件将携带相当于 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 quorums 健康。

我们必须增强 kuduraft 以使其在可用性方面更加稳健。这些改进不是核心协议的一部分,但可以被视为它的工程附加组件。Kuduraft 支持预选,但预选仅在故障转移期间进行。在优雅的领导权交接过程中,指定的候选人直接进入真正的选举,连任。这会导致领导者卡住(kuduraft 不会自动降压)。为了解决这个问题,我们添加了一个模拟选举功能,它类似于预选,但只有在优雅地交接领导权时才会发生。由于这是一个异步操作,因此不会增加促销停机时间。模拟选举将排除真正的选举会部分成功并陷入困境的情况。

Handling byzantine failures:Raft 的成员列表被认为是 Raft 自己加持的。但是在提供新成员期间,或者由于自动化竞赛,可能会出现两个不同的 Raft 环相交的奇怪情况。这些僵尸成员节点必须被淘汰,并且不能相互通信。我们实施了一项功能来阻止从此类僵尸成员到环的 RPC。在某些方面,这是对拜占庭演员的处理。在注意到部署中发生的这些罕见事件后,我们增强了 Raft 实施。

监控 MySQL Raft 部署

在启动 MySQL Raft 时,目标之一是降低随叫随到的操作复杂性,以便工程师能够找到根本原因并缓解问题。我们构建了几个仪表板、CLI 工具和scuba表来监控 Raft。我们向 MySQL 添加了大量日志记录,尤其是在促销和成员变更方面。我们为环上的法定人数和投票报告创建了 CLI,这有助于我们快速确定环何时以及为何不可用(破碎的法定人数)。对工具和自动化基础设施的投资是齐头并进的,可能比服务器变更的投资更大。这项投资获得了丰厚的回报,减少了运营和入职培训的痛苦。

法定人数修复者

虽然这是不可取的,但仲裁确实时不时地被打破,导致可用性损失。典型的情况是自动化没有检测到环中不健康的实例/logtailer 并且没有快速替换它们。发生这种情况的原因可能是检测不力、工作队列过载或缺少备用主机容量。当法定​​人数中的多个实体同时宕机时,相关故障不太常见。这些不会经常发生,因为部署会尝试通过适当的放置决策来隔离仲裁中关键实体的故障域。长话短说:尽管有现有的保障措施,但在规模上,意想不到的事情还是会发生。需要有可用的工具来缓解生产中的这种情况。我们基于对这一点的预期构建了 Quorum Fixer。

Quorum Fixer 是一种用 Python 编写的手动修复工具,可以抑制环上的写入。它进行带外检查以找出最长的日志实体。它强行改变了 Raft 内部领导者选举的法定人数期望,以便被选中的实体成为领导者。成功升级后,我们重置法定人数期望值,环通常会变得健康。

不自动运行这个工具是一个有意识的决定,因为我们想要找到根本原因并确定所有仲裁丢失的情况,并在此过程中修复错误(而不是让它们默默地被自动化修复)。

推出 MySQL Raft

在大规模部署中从半同步过渡到 MySQL Raft 是很困难的。为此,我们创建了一个名为 enable-raft 的工具(在 Python 中)。Enable-raft 通过加载插件并在每个实体上设置适当的配置(mysql sys-vars)来协调从半同步到 Raft 的转换。此过程涉及环的一小段停机时间。随着时间的推移,该工具变得健壮,可以非常快速地大规模推出 Raft。我们已经用它来安全地推出 Raft。

测试和影子工作流程

不用说,改变MySQL的核心复制管道是一个非常困难的工程。由于数据安全受到威胁,因此测试是信心的关键。我们在项目期间显着利用了影子测试和故障注入。在每次 RPM 包管理器推出之前,我们会在测试环上注入数千次故障转移和选举。我们将触发测试资产的替换和成员更改以触发关键代码路径。

具有数据正确性检查的长期运行测试也很关键。我们有每晚在分片上运行的自动化,确保主副本和副本的一致性。我们会收到任何此类不匹配的警报,并对其进行调试。

表现

Raft 写路径延迟的性能相当于半同步。半同步机制稍微简单一些,因此预计会更精简,但是我们优化了 Raft 以获得与半同步相同的延迟。我们优化了 kuduraft 以不再向队列中添加任何 CPU,尽管它引入了以前在服务器二进制文件之外的更多职责。

Raft 对提升和故障转移时间进行了数量级的改进。优雅的晋升,这是车队领导层变动的主要部分,得到了显着改善,我们通常可以在 300 毫秒内完成一次晋升。在半同步设置中,由于服务发现存储将成为事实来源,客户端注意到升级完成的时间会长得多,从而导致最终用户在分片上的停机时间增加。

Raft 通常会在 2 秒内进行故障转移。这是因为我们每 500 毫秒为 Raft 健康跳一次心跳,并在连续三个心跳失败时开始选举。在半同步世界中,这一步是编排繁重的,需要 20 到 40 秒。因此,Raft 将故障转移案例的停机时间缩短了 10 倍。

下一步

Raft 通过提供可证明的安全性和简单性,帮助解决了 Meta 中 MySQL 的操作管理问题。我们的目标是不干涉 MySQL 一致性的管理,并拥有针对罕见的可用性损失情况的工具,这些目标已基本实现。Raft 现在在未来开辟了重要的机会,因为我们可以专注于增强对使用 MySQL 的服务的提供。我们的服务所有者提出的其中一项要求是具有可配置的一致性。可配置的一致性将允许所有者在入职时选择服务是否需要 X 区域仲裁或在某些特定地区(例如欧洲和美国)要求副本的仲裁。FlexiRaft 对这种可配置的仲裁有无缝支持,我们计划在未来开始推出这种支持。PACELC 定理)。

由于代理特性(使用多跳分布拓扑发送消息的能力),Raft 还可以节省跨大西洋的网络带宽。我们计划使用Raft只从美国复制一次到欧洲,然后使用Raft的代理特性在欧洲内部分发。这将增加延迟,但考虑到大部分延迟发生在跨大西洋传输中并且额外的跃点要短得多,这将是名义上的。

Meta 的数据库部署和分布式共识空间中一些更具推测性的想法是关于探索无领导协议,如Epaxos。我们当前的部署和服务已经在强大的领导者协议附带的假设下工作,但我们开始看到一些需求,其中服务将受益于 WAN 中更统一的写入延迟。我们正在考虑的另一个想法是将日志从状态机(数据库)中分离出来,形成一个分解的日志设置。这将允许团队将日志和复制问题与数据库存储和 SQL 执行引擎的问题分开管理。

致谢

在元规模上构建和部署 MySQL Raft 需要大量的团队合作和管理支持。我们要感谢以下人员在使该项目取得成功方面发挥的作用。Shrikanth Shankar、Tobias Asplund、Jim Carrig、Affan Dar 和 David Nagle 在这次旅程中为团队成员提供了支持。我们还要感谢这个项目的干练项目经理 Dan O 和 Karthik Chidambaram,他们让我们走上正轨。

工程工作涉及几位现任和前任团队成员的重要贡献,包括 Vinaykumar Bhat、Xi Wang、Bartlomiej Pelc、Chi Li、Yash Botadra、Alan Liang、Michael Percy、Yoshinori Matsunobu、Ritwik Yadav、Luqun Lou、Pushap Goyal、Anatoly Karp 和伊戈尔·波兹加。

作者:Anirban Rahut、Abhinav Sharma、Yichen Shen、Ahsanul Haque

出处
:https://engineering.fb.com/2023/05/16/data-infrastructure/mysql-raft-meta/

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>