替代MySQL半同步,Meta技术团队推出MySQL Raft共识引擎

来源: CSDN 2023-05-19 09:17:11

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

  翻译 | ChatGPT   责编 | 梦依丹

  出品 | CSDN(ID:CSDNnews)

  MySQL Raft是MySQL数据库中一种基于Raft协议的分布式一致性复制机制。近日,Meta技术团队分享了他们基于Raft协议在数据库基础设施方面的实践与创新,并打算取代当下使用的MySQL半同步数据库(原文是用semisynchronous databases)。本文借助ChatGPT,进行了编译整理。

  Meta运行着世界上最大规模的MySQL部署之一。该部署驱动着社交图谱以及许多其他服务,如消息、广告和动态。在过去几年中,他们实施了MySQL Raft,这是一个与MySQL集成的Raft共识引擎,用于构建复制状态机。目前已大部分部署迁移至MySQL Raft,并计划完全用其替代当前的MySQL半同步复制。这也给Meta的MySQL部署带来了显著的成效,包括更高的可靠性、可证明的安全性、故障转移时间的显著改善以及操作简便性,同时具有相等或可比较的写入性能。

  迁移背景

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

  以前,他们的复制解决方案是使用MySQL半同步(semisync)复制协议。这是一种仅限于数据路径的协议。MySQL主服务器会使用半同步复制将数据写入两个日志记录副本(logtailers),这些副本位于主要区域之外但不在主服务器故障域内。这两个 日志记录副本将充当半同步ACKer(ACK 是对主服务器发出的事务已被本地写入进行确认)。这将使得数据路径具有非常低延迟的(亚毫秒级)提交,并为写操作提供高可用性/耐久性。普通的MySQL主-从异步复制则用于向其他区域进行更广泛的分发。

  控制平面操作(例如晋升、故障转移和成员变更)由一组Python守护程序负责执行(以下简称自动化)。自动化将执行必要的编排工作,以在故障转移位置上晋升新的MySQL服务器作为主服务器。自动化还会将以前的主服务器和剩余的从服务器指向新的主服务器进行复制。成员变更操作由另一个名为MySQL池扫描器(MPS)的自动化程序编排执行。要添加新成员,MPS将把新副本指向主服务器并将其添加到服务发现存储中。故障转移是一项更复杂的操作,在此过程中,logtailers的尾部线程(半同步 ACKer)将被关闭以限制先前死亡的主服务器。

  为什么需要MySQL Raft?

  过去,为了确保安全并避免在复杂的晋升和故障转移操作期间数据丢失,几个自动化守护程序和脚本会使用锁定、编排步骤、围栏机制和SMC(服务发现系统)。这是一个分布式设置,并且很难以原子方式完成。随着越来越多的边角情况需要修补,自动化变得更加复杂和难以维护。

  基于此,Meta技术团队决定采取完全不同的方法。他们增强了MySQL并使其成为真正的分布式系统。意识到控制平面操作如晋升和成员更改是大多数问题的触发器,团队希望控制平面和数据平面操作成为相同复制日志的一部分。为此,他们使用了众所周知的共识协议Raft。这就意味着成员身份和领导力真实性来源于服务器(mysqld)内部。这是引入Raft最大贡献之一,因为它使得在MySQL服务器中进行晋升和成员更改时能够证明正确性(安全属性)。

  Raft库和MySQL Raft插件

  Meta团队基于Apache Kudu实现了MySQL Raft,针对MySQL和他们的部署需求进行了大量增强,并将这个分支作为开源项目kuduraft发布。

  kuduraft的一些核心功能包括:

  FlexiRaft - 支持两个不同交叉仲裁:数据仲裁和领导者选举仲裁

  代理(Proxying) - 能够使用代理中间节点来减少网络带宽

  压缩(Proxying ) - 在分发之前对二进制日志(事务)有效载荷进行一次压缩

  日志抽象化 - 支持不同的物理日志文件实

  主要禁止(Primary ban) -临时阻止某些实体成为主要实体的能力

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

  

  MySQL Raft复制拓扑结构

  在MySQL Raft的复制拓扑中,使用Raft协议的MySQL实例形成一个环状结构,每个实例在不同的地区。Raft环将由几个位于不同地区的MySQL实例(图中有四个)组成。这些地区之间的通信往返时间(RTT)在10到100毫秒之间。其中几个MySQL实例(通常是三个)充当主节点,处理写入操作,而其余的实例充当只读副本,用于处理读取操作。在Meta的MySQL部署中,对于提交操作的延迟要求非常低,因为使用MySQL作为存储的服务需要快速的写入能力。

  为了在FlexiRaft配置中为了满足低延迟提交要求,Meta采用了区域内提交的方式(即单区域动态模式)。为了实现该目标,每个具备主节点能力的区域将增加两个日志追随者(也称为见证者或仅记录实体)。写入操作的数据法定人数设为2/3,即需要从1个MySQL和2个日志追随者中获得2个ACK。Raft仍然负责管理和运行跨所有实体的复制日志,包括1个具备主节点能力的MySQL加上2个日志追随者的组合,乘以3个区域,再加上3个区域中的非主节点能力的MySQL,总共有12个实体。

  Raft角色:在Raft协议中,有三种角色:领导者(Leader)、追随者(Follower)和学习者(Learner)。领导者是在复制日志中担任领导者的角色,同时也是MySQL中的主节点,负责接收客户端的写入操作。追随者是环中的投票成员,从领导者那里被动地接收消息(AppendEntries)。从MySQL的角度来看,追随者是一个副本,会将事务应用到自己的引擎中。它不允许用户连接进行直接写入操作(设置为read_only=1)。学习者是环中的非投票成员,例如,在非主节点能力的区域中的三个MySQL实例。在MySQL的角度来看,学习者也是一个副本。

  

  复制日志

  MySQL一直以来都使用二进制日志格式进行复制。这种格式对于MySQL的复制非常重要,因此Meta团队决定保留该格式。从Raft的角度来看,通过对kuduraft进行日志抽象和改进,实现了将二进制日志作为复制日志的方式。MySQL的事务会被编码为一系列事件,比如Update Rows事件,每个事务都有开始和结束时间。二进制日志还包含适当的标头,并通常以结束事件(Rotate事件)作为结尾。

  为了满足Raft的需求,Meta不得不调整MySQL内部日志的管理方式。在主节点上,Raft会将日志写入binlog,与标准MySQL的操作方式几乎没有区别。而在副本中,Raft同样将数据写入binlog,而不是像标准MySQL那样写入单独的relay log。这种调整使得Raft更加简化,因为它只需要关注一个命名空间下的日志文件。如果某个副本被提升为领导者,它可以无缝地从自己的历史记录中获取事务,并向滞后的成员发送事务。副本的应用程序线程会从binlog中获取事务,并将其应用于数据库引擎。在此过程中,还会创建一个名为"apply log"的新日志文件,该文件在副本的崩溃恢复方面起着重要作用,但它并非可复制的日志文件。

  简言之:

  在标准的MySQL中:

  主节点将写入binlog并将其发送到副本。

  副本接收relay log并将事务应用于引擎。在应用期间,会创建一个新的仅限副本的binlog。

  在MySQL Raft中:

  主节点通过Raft写入binlog,并且Raft将binlog发送给跟随者/副本。

  跟随者/副本接收binlog并将事务应用于引擎。在应用期间创建一个apply log。

  从Raft角度来看,Binlog是复制日志。

  使用Raft在MySQL主节点上编写事务

  使用Raft在MySQL主节点上编写事务的过程如下:首先,事务将在数据库引擎中准备。这一过程将在用户连接的线程中进行。准备事务的行为涉及与存储引擎(如InnoDB或MyRocks)的交互,并生成用于该事务的内存中的binlog负载。

  在提交时,写入操作将通过组提交/有序提交流程传递。GTID(全局事务标识符)将被分配,然后Raft将为该事务分配一个OpId(term:index)。此时,Raft将对该事务进行压缩,并将其存储到其LogCache中,并通过binlog文件写入该事务。它将异步地开始向其他跟随者发送事务以获取ACK并达成共识。

  请注意,term和index是Raft中的概念,用于唯一标识和顺序化事务。

  当事务提交时,Raft会通过投票达成共识,并解除用户线程的阻塞。提交的事务将继续在引擎中执行,并返回结果给客户端。同时,Raft会异步地将提交标记发送给其他跟随者,以便它们也可以应用相同的事务。

  

  崩溃恢复

  崩溃恢复是与Raft协议无缝协作的关键部分。在事务的生命周期中,可能会发生崩溃,因此必须确保成员之间的一致性。以下是他们在崩溃恢复方面的关键思路:

  事务未刷新到binlog:如果事务在崩溃之前尚未刷新到binlog中,意味着该事务的内存负载(在mysqld进程内存中作为内存缓冲区)将丢失。当进程重新启动时,引擎中准备好的事务将被回滚。由于Raft日志中没有未提交的额外事务,因此不需要与其他成员进行调和。

  事务已刷新到binlog但未到达其他成员:在这种情况下,MySQL充当交易协调器,并在参与者之间运行两阶段提交协议,该协议介于引擎和复制binlog之间。在崩溃恢复期间,引擎中准备好的交易将被回滚(引擎尚未提交)。Raft协议将经历故障转移,并选出新的领导者。新领导者在其binlog中不会有此交易记录,因此在前任领导者加入环时,通过推送No-Op消息来截断该交易记录,以实现更高的任期。

  事务已刷新到binlog并传递给下一个领导者,但当前领导者在提交给引擎之前崩溃:与上述第2点类似,引擎中准备好的交易将被回滚。前任领导者将作为追随者加入Raft环。在这种情况下,新的领导者将在其binlog中具有此交易记录,并且不会发生截断,因为日志匹配。当新的领导者发送提交标记时,该事务将重新开始应用。

  这些措施确保了崩溃恢复的一致性,并保证了事务的正确性。通过与Raft协议的集成,MySQL在高可用性和容错性方面取得了显著的进展,使数据库能够在崩溃情况下保持稳定运行,并能够快速恢复。

  Raft状态机转换

  Raft启动状态机转换是在故障转移和常规维护操作中触发的一项重要任务。一旦Raft选举出新的领导者,MyRaft插件会尝试将相应的MySQL实例切换到主模式。为了完成这个转换,插件需要执行一系列协调步骤。

  这些步骤涉及从Raft到MySQL的回调操作。它们包括中止正在进行的事务,回滚GTID(全局事务标识符),从应用日志切换到binlog,并设置正确的read_only属性。这一过程相当复杂,并且目前没有开源的实现可用。

  FlexiRaft

  在传统的Raft协议中,只有一个全局仲裁者(leader)来处理数据路径决策。然而,在Meta这样的大规模环境中,环的规模很大,而数据路径决策需要更小的仲裁范围。由于Raft协议和Apache Kudu都只支持单个全局仲裁者,无法很好地适应Meta的需求。

  为了解决这个问题,Meta团队借鉴了Flexible Paxos的一些思路,并创新性地引入了FlexiRaft。FlexiRaft是对传统Raft协议的改进,以支持更灵活的仲裁机制。

  在高层次上,FlexiRaft在数据提交仲裁方面具有灵活性(较小的范围),同时对领导者选举仲裁产生较大的影响(更大的范围)。通过遵循仲裁交集的可证明保证,FlexiRaft确保了Raft协议中最长日志规则的执行和适当的仲裁交集,从而提供了可证明的安全性。

  FlexiRaft支持单区域动态模式,其中成员根据地理区域进行分组。在此模式下,当前的数据提交仲裁取决于当前的领导者(因此称为“单区域动态”)。数据仲裁由领导者所在地区的投票人数多数派决定。在选举过程中,如果术语连续,则候选人将与最后已知领导者所在地区相交。FlexiRaft还确保获得候选人所在地区的法定人数,否则随后的No-Op消息可能会被阻塞。在极少情况下,如果术语不连续,FlexiRaft会尝试找出一组需要与之相交以实现安全性增长的地区;或者在最坏情况下,回退到Flexible Paxos中的N个区域交叉点案例。由于预选和模拟选举的引入,术语间隙的发生非常罕见。

  这些机制使得FlexiRaft在数据提交仲裁和领导者选举仲裁方面具有灵活性和可证明的安全性,从而更好地适应大规模环境下的需求。它允许更准确地控制数据路径决策的范围,并提供了强大的一致性保证。

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

  为了将晋升(即领导者更改)和会员变更事件序列化到MySQL的二进制日志中,Meta团队对MySQL二进制日志格式的Rotate Event(旋转事件)和Metadata Event(元数据事件)进行了修改。这些事件被用来携带Raft协议中的No-Op消息(无操作消息)以及添加成员/删除成员操作的等效内容。由于Apache Kudu不支持联合共识(即同时处理多个成员变更),因此他们只允许逐个进行成员变更。在一轮中,只能通过一个实体来改变成员身份,以遵守隐式法定交集规则。

  通过修改MySQL的二进制日志格式,Meta团队成功地将Raft协议中的晋升和会员变更事件与MySQL的二进制日志进行了集成,以确保一致性和可靠性。这样可以将这些操作持久化并进行复制,从而在集群中保持一致的状态。

  自动化

  通过实施MySQL Raft,Meta为MySQL部署实现了非常清晰的职责分离。MySQL服务器负责通过Raft的复制状态机确保安全性,以提供无数据丢失的保证。这个责任被直接集成到服务器本身中。

  为了管理和监控集群的运行状况,他们还使用了自动化工具,如Python脚本和守护进程。这些工具负责启动控制平面操作,并监视集群的健康状态。它们能够在维护期间或在检测到主机故障时自动执行成员替换或升级等操作。有时,自动化工具还可以根据需要修改MySQL拓扑的区域布置方式。

  适应Raft协议所带来的变化,并将这些变化反映到自动化工具中,是一项庞大的工作,需要多年的开发和推出。这是一个长期的过程,需要不断地优化和改进,以确保自动化工具与MySQL Raft的要求保持一致,并能够可靠地管理和维护整个部署。

  在长时间维护事件期间,自动化工具会在Raft上设置领导禁止信息。Raft将阻止这些被禁止的实体成为领导者,或在意外选举时及时撤离它们。自动化工具还会将领导权转移到其他区域,远离被禁止的区域。这样做的目的是确保在维护期间,被禁止的实体不会干扰系统的正常运行,并将领导权转移到可靠的区域,以确保集群的稳定性和可用性。

  MySQL Raft 推出的经验与挑战

  在将Raft推广到整个机群的整个过程中,团队积累了大量宝贵的经验和教训。最初他们在MySQL 5.6上开发了Raft,随后迁移到MySQL 8.0。

  其中一个关键的经验教训是,尽管Raft协议可以相对容易地确保正确性,但它本身并不能很好地解决可用性问题。由于他们的MySQL数据仲裁非常小(区域内的三个成员中的两个),若区域内出现两个故障实体,将会破坏仲裁并导致可用性降低。MySQL机群每天都面临相当多的变动(如维护、主机故障、平衡操作),因此及时、正确地进行成员变更对于保持持续可用性至关重要。推出过程中,团队将主要精力放在及时进行日志传输和MySQL替换上,以确保Raft仲裁保持健康。

  Meta团队不得不增强kuduraft以使其更加健壮可用。这些改进并非核心协议的一部分,但可以视为工程附加组件。Kuduraft支持预选举,但仅在故障转移期间进行。在领导权的平稳转移期间,指定的候选人直接进入真正的选举,并提高任期。这就会给领导者带来困扰(kuduraft不会自动下台)。为解决此问题,Meta添加了模拟选举功能,类似于预先选举,但仅在领导权平稳转移时发生。由于这是异步操作,因此它没有增加晋升停机时间。模拟选举将消除真实选举部分成功并被卡住的情况。

  处理拜占庭故障:根据Raft协议,成员列表是由Raft自身认可的。然而,在引入新成员或因自动化竞争而导致两个不同的Raft环交叉的过程中,可能会出现一些异常情况。这些异常情况会导致出现僵尸成员节点,这些节点需要被清除并与其他成员节点停止通信。为了解决这个问题,团队实现了一个功能来阻止这些僵尸成员向环发送RPC请求,从而防止它们对系统造成干扰。这种处理方式可以被视为对拜占庭行为的一种应对方式。一旦团队注意到在他们的部署中发生了这些罕见事件,他们对Raft实现进行了改进,以增强系统的稳定性和可靠性。

  MySQL Raft推出后的监控

  推出MySQL Raft时,一个重要目标是减少on-call人员的操作复杂性,以便工程师可以更轻松地定位和解决问题。为此,团队建立了多个监控仪表板、CLI工具和scuba表来监视Raft的状态。在MySQL中增加了大量的日志记录,特别是在晋升和成员更改方面。团队还创建了用于环的法定人数和投票报告的CLI工具,可以快速识别环不可用(法定人数不足)的时间和原因。在工具化和自动化基础设施方面的投资与服务器变更的推出密切相关,并且可能比服务器变更本身更具挑战性。然而,这项投资带来了巨大的回报,并大大减少了运维和入职过程中遇到的困难。这样的投资使得团队能够更高效地管理和监控Raft,并及时解决潜在的问题。

  Quorum Fixer

  尽管并不希望发生这种情况,但有时自动化无法及时检测到环中的不健康实例或日志记录器,并进行及时替换。这可能是由于检测不足、工作队列过载或缺乏备用主机容量等原因造成的。尽管这种情况不常见,因为团队在部署过程中采取了适当的放置策略来隔离关键群集实体的故障域,但仍然可能发生意外情况。

  为此,Meta开发了Quorum Fixer,这是一个用 Python 编写的手动修复工具,旨在解决可能导致可用性下降的拆分情况。它在环中禁止写操作,并进行离线检查以确定最长日志实体。然后,它会强制更改 Raft 中的领导者选举的法定人数期望值,从而选择该实体作为领导者。一旦成功晋升,我们将法定人数期望值重置回原来的状态,通常可以使环恢复正常。

  我们有意决定不自动运行此工具,因为我们希望能够发现所有的群集损失情况并修复其中的错误,而不是让自动化默默地解决问题。因此,Quorum Fixer 提供了一种手动的方式来处理这些情况。通过使用该工具,我们能够更加积极地识别并解决生产环境中的问题。

  发布MySQL Raft

  推出MySQL Raft对于大规模部署来说确实是具有挑战性。为了应对这个问题,Meta团队还开发了一个名为"enable-raft"的工具,使用Python编写。该工具的目的是协调从半同步到Raft的过渡过程,并在每个实体上加载插件并设置适当的配置(例如MySQL系统变量)。这个过程中需要短暂的停机时间来完成环境的转换。

  通过多次改进,enable-raft已变得非常稳健,并且能够快速扩展Raft功能。他们已经成功地使用这个工具来安全地推出Raft,并将其应用于Meta的部署中。该工具在过渡过程中发挥了重要作用,它简化了从半同步到Raft的切换,并在整个部署中保持稳定性。它是Meta团队成功实施Raft的关键组成部分。

  在进行MySQL核心复制管道的更改过程中,测试是非常重要的,因为数据安全性是一个关键问题。为了建立信心并确保变更的正确性,我们采用了大量的影子测试和故障注入。通过影子测试和故障注入以及长时间运行的数据正确性检查,我们能够更好地评估和验证MySQL核心复制管道的更改,确保其在生产环境中的可靠性和安全性。

  性能表现

  通过优化Raft实现,他们实现了与semisync相当的写入路径延迟表现,尽管semisync机制稍微简单一些。团队确保在Raft中承担额外职责时不增加任何CPU负载,使其能够处理许多之前不属于服务器二进制文件的任务。

  Raft大大改善了晋升和故障转移的时间。在整个集群中,优雅的领导权转移占据了绝大部分比例,并且通常可以在300毫秒内完成。相比之下,在semisync设置中,由于服务发现存储将是真实来源,客户端需要更长的时间来注意到晋升的完成,从而导致用户在片段上的停机时间更长。

  在故障转移方面,Raft通常可以在2秒内完成。通过每500毫秒进行心跳检测和Raft健康状况,并在连续三个心跳失败后开始选举新的领导者。相比之下,在semisync环境中,这个过程需要进行编排并耗费20至40秒的时间。因此,在故障转移情况下,Raft可以将停机时间缩短10倍。

  总之,通过这些性能优化措施,团队成功地改进了Raft的写入路径延迟表现,缩短了晋升和故障转移的时间,使得在整个集群中的领导权转移更加快速和优雅。

  未来计划

  接下来,Raft将专注于增强MySQL服务功能,实现对MySQL一致性的无需干预管理。通过支持可配置法定人数的FlexiRaft,服务所有者可以选择在入门时是否需要特定地理位置的复制副本,权衡一致性和延迟。这将为MySQL用户提供更多灵活性和选择。

  Raft还计划利用代理功能来节省跨大西洋的网络带宽。通过仅在美国复制到欧洲一次,然后使用Raft的代理功能进行分发,虽然会增加一些延迟,但可以大大减少横跨大西洋的传输时间。此外,团队还在探索无领导协议(如Epaxos)和解耦日志与状态机的非集成日志设置,以进一步提升部署和服务的效能和灵活性。

0
收藏
0