cover_image

MySQL高可用服务的设计开发

申玉聪 北京顺丰同城科技技术团队
2024年06月18日 09:18

1.背景

1.1 现状

目前我们业务对MySQL集群的访问路径如图1-1所示,业务通过SFNS访问MySQL的代理层DBProxy,而DBProxy会将数据库请求转发到MySQL集群,MySQL集群是一主多从,其中写请求转发到MySQL集群主库,读请求转发到从库。
图片

图1-1 数据库访问路径

SFNS支持自动探活,DBProxy的部署是多节点的,当DBProxy一个节点故障,SFNS会将请求转发到其他运行正常的DBProxy,理论上不会对业务造成影响。DBProxy会自动剔除故障的从库,因此当MySQL集群从库故障时,DBProxy会将请求转发到其他运行正常的从库,保证业务正常访问。
但是,MySQL集群中只有一个主库,存在单点问题,当集群主库故障时会直接影响业务的写入,基于此,我们需要对MySQL集群配置高可用,解决主库单点问题,使得主库故障后,能够自动地进行故障切换,选举出新的主库,恢复集群写入,保证业务正常访问。

1.2 方案对比

目前高可用的方案中,MHA(Master High Availability)是使用比较多、并且比较成熟的方案,那么我们为什么会选择orchestrator呢?我们针对一些关键的功能对MHA(Master HighAvailability)和orchestrator进行对比,对比结果如图1-2所示。

图片

图1-2 高可用方案对比

MHA(Master High Availability)有一个很大的缺陷是其自身管理节点存在单点问题,而Orchestratort则通过Raft分布式一致性协议保证其自身管理节点的高可用,并且orchestrator相比于MHA(Master High Availability)来说在宕机判断和选主模式上都有比较大的优势,但不足是Orchestrator在某些场景下可能会出现丢数据的情况,数据补偿机制需要进行优化。
Orchestrator还有一些其他高可用方案不具备的优秀功能:
1. 自动发现MySQL复制拓扑,很方便管理多套集群。
2. 支持修改MySQL拓扑结构,变更复制关系。
3. 支持使用命令行工具、Http API、Web界面管理拓扑,如图1-3所示是其Web管理界面。
4. Go语言编写,方便二次开发。
图片

图1-3 Orchestrator Web管理界面

1.3 关键技术介绍

本文涉及到的数据库相关的关键技术主要包括DBProxy和DBProxyAdmin。

1.3.1 DBProxy介绍

在实际项目的开发过程中,连接池、失败重连、读写分离、负载均衡这些功能往往写在应用中,使代码变得复杂,并且增加了出错的机会,因此我们引入了DBProxy,作为业务和数据库之间的中间层,为业务访问数据库提供了独立、透明的高性能代理服务。DBProxy主要具备以下功能:
1.读写分离
2.负载均衡
3.安全认证
4.失败重连
5.连接池
6.配置热加载

1.3.2 DBProxyAdmin介绍

在我们实际应用DBProxy的过程中,对于DBProxy的部署是多节点的,手动对DBProxy的配置、进程管理容易出现失误,因此我们开发了DBProxy的管理工具DBProxyAdmin,其具体功能如下:
1.配置管理
    a) 配置上传(上传配置到云端)
    b) 配置修改
    c) 配置下发(从云端下发到各节点)
    d) 配置查看
2.进程管理
    a) 查看进程状态
    b) 启动DBProxy进程
    c) 重启DBProxy进程
    d) 停止DBProxy进程

2.Orchestrator原理

2.1 探测机制

Orchestrator默认每隔5秒去被监控的MySQL实例上拉取实例的状态,并将实例的状态信息存储至Orchestrator的后端元数据库,然后Orchestrator默认每隔5秒从元数据库中获取每个MySQL实例的状态,展示在Web界面。

2.2 故障检测

故障检测的方式有很多种,比如当监控端探测到主库无法连接或查询,就认为故障已经发生,这种方式很容易因为网络状况不佳而造成误判,虽然可以通过一段时间内多次判断来减少误判的几率,但是却增加了真正故障的响应时间。
Orchestrator故障发现的过程如图2-1所示。Orchestrator很好地利用了集群实例间的复制关系,不仅会探测主库,也会对从库进行探测,实际情况下,如果所有从库都联系不上主库,说明集群的复制关系已经被损坏,我们是有理由进行故障切换的。在同时满足以下两个条件的前提下,Orchestrator才会判定主库故障:
1.联系主库失败。
2.可以联系到主库对应的从库,并且这些从库联系主库也失败。
图片

图2-1 Orchestrator故障发现

2.3 选主过程

Orchestrator判定主库故障后会进入选新主库的阶段,选主过程如图2-2所示,以下将详细介绍选主的整个过程
图片

图2-2 Orchestrator选主过程

2.3.1 副本排序

首先,Orchestrator判定主库故障后,会停止故障主库的副本和故障主库的复制关系,然后对所有副本进行排序操作。排序规则包括很多,这里只介绍在我们实际应用场景中比较重要的几种规则,其余就不一一介绍了。其中副本排序规则的优先级从高到低依次为:副本对故障主库Binlog的实际执行位置、数据中心(地域)、提升规则。依据排序规则的优先级,Binlog执行位置最新的副本优先,如果副本Binlog执行位置相同,则数据中心和故障主库相同的副本优先,如果Binlog执行位置也相同,则提升规则对应数值小的副本优先。其中提升规则的制定对集群来说是比较灵活的,用户可以自定义每个实例的提升规则,提升规则的选项及其对应的数值如下:
1.must:0
2.prefer(比较喜欢):1
3.neutral(中立):2
4.prefer_not(比较不喜欢):3
5.must_not(禁止提升):4

2.3.2 新主选择

根据排序规则对故障主库的所有副本排序完成后,Orchestrator不会直接选择排序结果优先级比较高的副本作为新主库,除了考虑排序结果外,Orchestrator还会考虑副本的版本、副本的Binlog格式、以及副本的提升规则是否设置为must_not(禁止提升),以此来判定某个副本是否合适作为新的主库,综合考虑以上条件后,最终会选择出一个副本作为集群的新主库。

2.3.3 副本划分

新主库初步选择完成后,Orchestrator会对故障主库的其余副本进行划分,划分的规则是副本对故障主库Binlog的实际执行位置,Binlog的执行位置代表同步数据的新旧程度。因此将其余副本划分为三类,分别是:比新主库数据还要新的副本(ahead)、和新主库一样新的副本(equal)、比新主库落后的副本(later)。

2.3.4 重构拓扑

副本划分完成后,进入重构拓扑的步骤,其中重构拓扑包括两个步骤,分别是重置副本的复制关系、分离不可用副本。
首先对equal、later副本进行重置,将它们的主库从之前的故障主库变为目前的新主库,并启动它们和新主库之间的复制关系,和新主库建立复制关系后,later副本缺失的数据也会自动得到补偿。除此之外,很关键的一点是在这个阶段新主库还没有进行重置,还保留着和故障主库的复制信息,并且在这一阶段新主库重新开启了了这段复制关系,也就是说,如果这个阶段故障主库被重新拉起来,恢复正常,业务继续向旧主库写入的数据也会同步到新的拓扑中。
在变更复制关系的过程中,可能有一些副本和新主库产生复制错误,这些副本会被Orchestrator从新集群中分离出去,除此之外,需要分离的副本还包括ahead副本,也就是比新主库数据还要新的副本,根据以上的选主策略,在我们实际的应用场景下,我们初次选出的新主库一般是所有副本中拥有最新数据的,除非最新副本被加入到了提升黑名单中被禁止提升。分离副本的方式是可逆的,通过修改副本的MasterHost实现(change master to “//master”)。

2.3.5 二次选主-理想主库替换新主

新集群拓扑产生后,选主过程并没有结束,Orchestrator会判断当前选出的新主库是否是一个理想的主库,如果不是,Orchestrator会继续尝试从当前新主库的所有副本中选择一个理想的主库来替换当前的新主库,替换的过程相当于两个实例互换角色,不会造成数据丢失。
对于理想主库的判定,不得不说Orchestrator是非常人性化的,它会从用户的角度出发,尊重用户对于新主库的选择意愿,理想主库的判定和选择包括以下几个规则,这些规则都是用户自定义的选项:
1.提升规则是否为must或者prefer
2.DataCenter是否和故障主库相同
3.PhysicalEnvironment是否和故障主库相同
Orchestrator两次选主的过程,不仅保证了新集群能够最大程度地拥有最新的数据,并且还保障了用户的实际提升意愿。假设故障主库是广州机房,用户希望提升的新主库也是在广州机房,如果在第一次选主的过程中不幸选中的最新主库是上海机房的,该新主库一定会判定为非理想新主库,在第二次选主的过程中新主库会被理想的广州机房的副本所替换。

2.4 新集群恢复写入

新主选择完成后,新集群拓扑还处于只读状态,所以需要恢复新集群的写入,使数据能够正常写入新主库,并且要避免继续写入旧主库。如图2-3所示是新集群写入恢复的过程。
图片

图2-3 Orchestrator写入恢复

首先,在新主库上执行Reset操作来清除新旧主库间的复制关系,然后将新主库设置为可写状态,同时Orchestrator会尝试将故障主库设置为只读状态,防止故障主库重新被拉起造成脏数据的写入。该过程还定义了多个Hook,方便用户进行自定义开发。至此,关于Orchestrator的故障切换过程就完成了。

3. Orchestrator优化

本节将对Orchestrator在实际应用过程中面临的问题及其解决方案进行详细地阐述。

3.1 问题分析

虽然Orchestrator的功能强大,但是在我们实际的生产环境中对其进行使用,还是会面临一些问题,以下将主要阐述两个关键的问题:DBProxy的拓扑变更、数据补偿。

3.1.1 关于同机房切主

我们MySQL集群的节点分布在两个机房,我们希望实现主库能在同机房实现故障切换。

3.1.2 关于DBProxy的拓扑变更

Orchestrator只完成了MySQL层面的故障切换,选择了新主库,并且对整个集群拓扑进行了重构,保证了新集群的可用性,但是MySQL的上一层,也就是业务直接访问的DBProxy层并不知道MySQL在故障切换后集群的拓扑已经发生了变化,因此业务访问的还是旧主库,会直接导致写入失败。
为了解决以上问题,我们必须在Orchestrator完成故障切换后对DBProxy的拓扑结构进行变更,使业务能够正常访问新集群。其中DBProxy在进行拓扑变更的过程中需要注意以下几个问题:
1.如何避免修改DBProxy过程中出现新旧主库同时写入的情况?
2.DBProxy修改和数据补偿的先后问题。

3.1.3 关于数据补偿

虽然在初次选主阶段,Orchestrator会优先选择最新副本作为新主库,但是最新的副本并不一定是数据最完整的副本,某些情况下,我们仍然需要进行数据补偿:
1.主库故障前存在主从延迟,可能最新副本的数据也存在缺失。
2.新旧主库清除复制信息之后,DBProxy全部修改完成之前,如果故障主库被重新拉起来,旧主库会有重新写入数据的风险。
基于以上两点,我们认为数据补偿的过程还是必须的,补偿过程中需要注意以下几个问题:
1.什么时候开始补偿?
2.从什么位置开始补偿?
3.数据补偿的耗时问题。
4.数据补偿完成之前应尽量保证新主库只读,避免造成主键冲突。

3.2 设计与实现

本节主要介绍DBProxy拓扑变更、数据补偿的设计与实现过程,并针对上一节提出的问题提出解决方案。

3.2.1 总体设计

基于以上问题分析的结果,我们对Orchestrator的功能补充包括DBProxy的拓扑变更、数据补偿。功能的总体设计如图3-1所示,DBProxy拓扑的变更和数据补偿全部在Orchestrator的故障切换完成后进行。
图片

图3-1 总体设计

DBProxy在修改过程中如果出现新旧主库同时写入数据的情况,在数据补偿阶段极易造成逐渐冲突,因此我们在开始对DBProxy进行修改之前要先设置新主库为只读状态,保证在DBProxy完全修改完成之前数据不会写入到新主库。如果这个过程中数据写入了旧主库,我们可以在之后的数据补偿阶段将这些数据补偿到新主库,不会造成数据的丢失。
对于DBProxy的修改和数据补偿的时机问题,我们考虑一种极端的情况:在尝试设置旧主库为只读状态时,旧主库由于故障无法连接因此设置只读失败,随后DBProxy又不幸修改失败了,其拓扑的主库仍旧是旧主库,如果这时旧主库重新恢复正常,业务数据持续写入旧主库,这种情况下如果进行数据补偿,从旧主库获取到的补偿数据可能不是最新的,因此理论上我们考虑先进行DBProxy的变更,并且在DBProxy修改成功的前提下再进行数据补偿。
关于数据补偿,我们首先考虑从什么位置进行补偿,选主过程中在重构复制拓扑之后会重启新旧主库的复制关系,但在新集群的恢复阶段我们将解除这段复制关系,因此数据补偿的起始位置应该是新旧主库停止复制时新主库对旧主库的同步位置。
其次考虑数据补偿的耗时问题,我们会设置一个阈值,如果补偿日志小于该阈值,则进行自动补偿,然后恢复新主库的写入,如果超过阈值或者补偿过程中发生错误,那么将不会再自动进行新主库的写入恢复,而是需要人工决策先恢复业务写入还是手动进行数据补偿完成后再恢复新主库的写入。需要注意的是在自动的数据补偿过程中,在数据完全补偿完成之前,需要保证新主库为只读状态,否则会造成主库冲突。

3.2.2 同机房切主

主要通过DetectPromotionRuleQuery参数实现,该参数定义了一个可以在拓扑实例上执行的可选查询,并返回实例的提升规则。在我们的场景中,主机名包含了地域信息,我们希望根据实例的地域信息来定义提升规则,理想的状态是选择的新主和原主库在相同地域。
如下图所示,我们使用自定义SQL对hostname进行了判断,如果hostname包含“bk”字段,提升规则设置为must_not,如果hostname包含“gz”字段,提升规则设置为prefer,其余情况提升规则设置为prefer_not。这样就保证了备库(bk)永远不会被提升为新主库,地域为gz的实例总是会被优先提升为主库。
图片

3.2.3 DBProxy拓扑变更

DBProxy的修改大致分为以下几个步骤:首先获取新集群的拓扑信息,然后对云端的DBProxy配置进行修改,修改完成后将新的配置下发到MyPrxoy的各个节点,最后对所有DBProxy进行重启,使DBProxy能够将业务请求转发到新的集群。
关于DBProxy的拓扑变更,我们主要使用DBProxyAdmin工具实现整个变更的过程,其具体实现步骤如下:
1.获取新集群拓扑信息
2.备库信息过滤(线上请求不转发的备库)
3.云端配置变更
4.向各节点下发修改后的云端配置
5.DBProxy进程重启

3.2.4 数据补偿

如图3-2所示为数据补偿的实现过程,主要分为两个阶段,分别是补偿日志的获取和补偿日志的应用。
图片

图3-2 数据补偿

1.Orchestrator向数据补偿服务发起数据补偿的请求。
2.数据补偿服务从故障主库获取差异Binlog。
3.将获取到的差异的Binlog应用到新主库,并向Server返回数据补偿结果。
4.Orchestrator从数据补偿Server获取最终数据补偿的结果。

3.2.5 Hook调用时机

我们对orchestrator的源码进行了部分修改,获取到了补偿日志的起始点位,以及新的拓扑信息。使我们可以在自定义hook中获取这些参数。
Proxy修改、数据补偿的过程被打包成Golang二进制包,并在orchestrator的自定义hook中进行调用,调用的hook为PostFailoverProcesses,即在新主恢复成功时执行。如下图所示,红框标出的是我们修改源码后获取到的自定义参数。
图片

4. 收益

使用Orchestrator为生产环境中的MySQL集群进行高可用的部署,很大程度上保障了MySQL集群的可用性以及可靠性。
高可用上线后,已实现了生产集群的全量覆盖,并且成功完成多次线上故障演练。平均切换时间为5秒,最长切换时间为20s内,成功避免了多次线上实际故障。
图片
END

继续滑动看下一个
北京顺丰同城科技技术团队
向上滑动看下一个