ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL MGR高可用架构详解:从Paxos原理到三节点部署实战

MySQL MGR高可用架构详解:从Paxos原理到三节点部署实战

1. MGR到底解决什么问题

先聊一个场景。某天下午某公司的机房巡检,值班同事发现主库的磁盘报警,紧接着副本延迟从几十秒飙到几分钟,等你去查的时候已经快追上业务高峰了。更麻烦的是,这个节点一旦彻底挂掉,只读流量直接断,因为半同步虽然有副本,但主库故障切换还要靠脚本去提升备库,提升过程中写服务是停的。这套流程跑了一年多,中间出过两次切换失败,一次是因为备库回放落后太多,一次是脚本本身有bug,两次都折腾了大半个小时才恢复。

后来我在这套环境里引入了基于MySQL的组复制,也就是常说的MGR。MGR不是一个新的存储引擎,它是MySQL官方提供的一种高可用解决方案,建立在InnoDB存储引擎之上,用Paxos协议把多个MySQL实例组织成一个复制组。这个组里的数据是强一致的,任何事务只有在组内多数成员都确认之后才算提交成功,谁挂了、谁加进来、组里有多少个节点是健康的,这些信息由组自己维护,不需要外部脚本再去探测、抢锁、做角色切换。

简单说,MGR把高可用从"主备结构 + 外部调度"变成了"组成员自治 + 内部协商",系统自己知道谁是主、谁可以接管,天然规避了脑裂问题。适合用在那些希望读写不中断、切换不依赖人工脚本、又不想引入额外中间件的业务场景。对DBA来说,MGR需要理解的核心东西不多,一条Paxos链路加一套组管理机制,剩下的还是那些熟悉的MySQL参数和复制概念。

这篇文章就用三节点的例子,把MGR从选型、配置、启动到日常运维中踩过的坑过一遍,基于常见实践的思路,没有平台或版本上的绑定,纯数据库层面的实操经验。

2. 架构设计核心思路

2.1 从异步复制到MGR的演进逻辑

传统主从复制是单向的,binlog从主库流到从库,从库IO线程拉日志、SQL线程回放。这个模型最大的痛点是"延迟不可控"和"角色提升需要人工干预"。即便加了半同步,也只能保证事务提交时至少有一个备库收到了binlog,但备库回放多少、能不能马上接管,半同步管不了。

MGR在数据复制层面没有推翻binlog那一套,底层依然是基于binlog和relay log在流转数据。它真正改变的是"谁来做决策"这件事。MGR在组内引入了Paxos协议作为共识引擎,事务在组内成员之间的传播和确认走的是这条协议,而所有成员对组内的成员列表、主节点是谁、哪个节点加入或退出,都通过分布式的组编排机制来同步。

所以从架构设计的视角看,MGR并不是一个全新的数据库,它是给传统的MySQL复制加了一个"管理壳"和"一致性协议"。理解这一点对所有后续配置都有帮助——你需要关注的参数,一部分是为了让MySQL本身能够进入组复制模式(比如开启GTID、开启binlog),另一部分才是MGR自己的管理参数(比如组名、是否引导启动、单主还是多主)。

2.2 单主模式与多主模式的取舍

MGR支持两种模式:单主模式,即Single Primary Mode;多主模式,即Multi Primary Mode。生产环境绝大多数情况我会建议用单主。

单主模式下,整个组只有一个节点可写,也就是主节点,其他节点只提供读的扩展。这个主节点不是某个配置文件写死的,而是组根据成员角色动态选出来的,老主退出后剩余成员会自动选出新主。因为这个选举过程在组内部通过协议完成,不需要额外的监控程序。

多主模式下,组内所有节点都可写。听起来很诱人,但代价是每个节点都要开启行级冲突检测(detect conflicts),而且碰到并发更新同一行数据,后提交的事务会被回滚。业务端没有做好流量拆分和冲突规避的话,很容易一手奶一手毒。从我实际经验看,除非业务有明确的"分片写入"策略,否则多主模式只会增加排查问题的复杂度,单主模式已经把"具备自动切换能力"和"业务无需改造"这两点都满足了。

另外一个常见的架构问题是,MGR要不要和Proxy组合使用。MGR本身不提供连接层的能力,像读写分离、连接复用这类事情需要业务端配置或者引入中间层。在MGR之上套一层对外提供统一入口的中间件,应用只感知一个逻辑地址,写流量永远打到主节点,读流量可以分摊到各个节点,这个组合在生产里比让业务自己去感知多节点要实用得多。

3. 关键参数和部署前置条件

3.1 底层参数为什么这么重要

在配置MGR之前,有个特别容易忽略的问题:MySQL实例当前的初始状态。你的MySQL版本、数据目录里已有的数据、binlog有没有开启,都会直接影响MGR能不能顺利启动。MGR要求组内成员的GTID必须完全一致,如果只是装了个单机实例再改了配置就想直接加入组,大概率会卡在恢复阶段报错。

搭建MGR的基本前提包括下面这些条件,每一项都需要在配置里明确体现:

  • 存储引擎必须为InnoDB,MGR不支持MyISAM作为组内表的存储引擎
  • 开启GTID,gtid_mode=ON,且enforce_gtid_consistency=ON
  • 开启binlog,log_bin参数和log_slave_updates都要配置
  • binlog格式建议使用ROW,事件校验和设置为NONE
  • 事务隔离级别建议使用READ COMMITTED,避免间隙锁引发的不必要冲突
  • 组内所有成员的server_id必须唯一且不能为0

这些条件看着是基础配置,但每一条都对应一个潜在的坑。比如说binlog_checksum如果设置成CRC32,部分版本在恢复阶段会报日志校验不一致;比如在没有开启GTID的实例上直接做组复制,报错日志里会出现一堆关于GTID不一致的提示。这部分往往是最花时间的,因为不是MGR本身的问题,而是你的MySQL还没有达到可以被组复制接管的状态。

3.2 MGR基座参数的解读

确认基础条件满足后,下面的参数是真正和组复制直接相关的部分。

plugin-load-add=group_replication.so group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" group_replication_start_on_boot=off group_replication_local_address= "192.168.1.1:33061" group_replication_group_seeds= "192.168.1.1:33061,192.168.1.2:33061,192.168.1.3:33061" group_replication_bootstrap_group=off

group_replication_group_name是组复制的唯一标识,格式上必须是一个UUID,不能乱写。同一个组里所有成员的组名必须一致。很多人会在这里踩坑——三个节点写三个不同的UUID,启动时互相找不到对方,报错信息却不会直接告诉你是组名错了,只会提示连接失败或者超时。

group_replication_local_address是当前成员和其他成员通信时的内部地址,格式是IP:端口,但这个端口不是说MySQL实例的3306端口,而是组复制专用的通信端口。这个端口专门跑Paxos通信和成员间的消息复制,必须和数据库服务端口区分开。防火墙只放通了3306,漏了33061,节点之间在服务层是通的,但组复制层永远起不来。

group_replication_group_seeds是组内成员的初始接入列表。注意seeds只承担引导加入组时联系其他成员的作用,一旦成员成功加入组,组内成员名单就会变成动态维护的视图,后续加入的新成员不再依赖seeds列表。

group_replication_bootstrap_group这个参数极其危险,它只应该在一个组第一次创建、且没有任何成员在线时使用。如果每次启动都在所有节点上配置成ON,会导致多个节点各自引导出不同的组,大家以为在同一个组里,实际互相不在一个视图。生产规范里我的要求是:引导完第一个节点后立刻把bootstrap_group关闭,或者只在单独的命令行里执行引导语句。

还有一个重要参数是group_replication_single_primary_mode,它在MySQL 5.7.17及以后版本默认值是ON,也就是单主模式。如果想用多主,需要显式设置成OFF。但前面说了,多主意味着更多冲突检测的负荷,单主是更稳妥的默认选项。

3.3 恢复阶段存储引擎和事务层面的要求

很多新人以为MGR只要参数配置对了就行,其实还有一个被藏得很深的环节——分布式恢复阶段。当一个节点加入组时,它并不是拿着binlog从头开始读,而是要先和组内已存在的成员建立连接,获取缺失的事务数据。这个过程分为两类数据:一类是在线事务日志的增量同步,一类是通过状态传输完成全量初始化。

这个阶段对存储层面的兼容性有要求。MGR在事务执行时会在组内做冲突检测,行数据对应的主键必须存在,否则在反向扫描检测阶段会出问题。同时,表上必须有主键,否则在冲突检测或回放时可能报"Table has no primary key"之类的错误。虽然某些场景下能容忍无主键表,但MGR的运行机制决定了它对无主键表的态度是零容忍的。

此外,由于分布式恢复阶段会用到多种通道,如果之前手动清理过binlog文件,或者gtid_purged被设置得不正常,会导致新节点无法追平主节点的GTID集合,报错里会出现"ERROR 1777"或类似的GTID缺失提示。这种情况下,与其去修补GTID,不如让新节点以空实例身份加入组,再通过组内成员的备份进行初始化,思路会清晰很多。

4. 三节点MGR部署实操

4.1 环境准备

我用三个节点做演示,各自配置如下。

节点A:192.168.1.11,端口3306,组通信端口33061 节点B:192.168.1.12,端口3306,组通信端口33061 节点C:192.168.1.13,端口3306,组通信端口33061

三台机器的MySQL版本保持一致,这里用8.0也就是目前生产环境常见的版本,MGR在8.0里已经比较稳定,很多在5.7里需要额外处理的权限模型问题也得到了改善。操作系统层面需要保证的是节点间网络互通,3306用于MySQL服务协议连接,33061用于组内成员通信,防火墙或者安全组策略里必须显式放行。

另外要提醒一下,MGR节点之间的网络延迟和稳定性直接决定了组复制的表现。组复制是强一致的,每次事务提交都要等待组内成员确认,这个确认过程在网络抖动时会被放大成可感知的延迟,甚至会触发成员疑似失败和重新选举。部署MGR的网络要求比普通主从要高,同一机房内网是底线,跨机房部署需要仔细评估延迟对写性能的影响。

4.2 MySQL配置文件核心段

三台节点的my.cnf核心内容一致,只有server_id和report_host不同。

[mysqld] server_id=11 gtid_mode=ON enforce_gtid_consistency=ON binlog_format=ROW binlog_checksum=NONE log_bin=binlog log_slave_updates=ON transaction_write_set_extraction=XXHASH64 loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" loose-group_replication_start_on_boot=OFF loose-group_replication_local_address="192.168.1.11:33061" loose-group_replication_group_seeds="192.168.1.11:33061,192.168.1.12:33061,192.168.1.13:33061" loose-group_replication_bootstrap_group=OFF loose-group_replication_single_primary_mode=ON loose-group_replication_enforce_update_everywhere_checks=ON

这里解释几个为什么。

transaction_write_set_extraction设置成XXHASH64是为MGR的冲突检测服务的,它会把每行事务的写集合提取成哈希信息,然后和其他成员同步,用于判断是否存在写冲突。不设置这个参数,启动组复制会直接报错。

loose前缀主要是为了在MySQL实例里加载插件但还没有配置完整时,相关参数不会被当成未知系统变量导致启动失败,这个细节在5.7时代非常有价值,8.0里我依然保留它,作为一种防御性写法。

4.3 创建复制账号

MGR成员之间的数据复制和恢复,都是通过复制通道来做的。这个复制账号既不能太弱,又不能只给最小权限而漏掉恢复阶段所需的能力。常见的授权方式如下。

CREATE USER 'repl'@'%' IDENTIFIED BY 'REPL_PASSWORD'; GRANT BACKUP_ADMIN, REPLICATION SLAVE ON *.* TO 'repl'@'%';

注意,BACKUP_ADMIN权限在分布式恢复阶段是必须的,因为新成员加入时需要请求远程数据并应用传输快照,这比传统主从复制要求更高的权限。很多人在这一步只授予REPLICATION SLAVE,结果新节点启动组复制时卡在恢复阶段,报错提示无法执行某些Binlog相关操作。

4.4 插件安装与组创建

所有节点都需要加载组复制插件。

INSTALL PLUGIN group_replication SONAME 'group_replication.so';

然后依次执行启动流程。先启动节点A,并在节点A上执行引导命令。

SET GLOBAL group_replication_bootstrap_group=ON; START GROUP_REPLICATION; SET GLOBAL group_replication_bootstrap_group=OFF;

节点A执行完START GROUP_REPLICATION后,它会以第一个成员的身份初始化一个组,并且因为组里只有自己一个成员,它会给自己分配主节点角色。然后节点B和节点C执行普通的启动命令,不需要引导。

START GROUP_REPLICATION;

这里有一个非常容易踩的坑:启动顺序。如果节点B在节点A还没有成功引导时就执行START GROUP_REPLICATION,节点B会去尝试连接seeds列表,但由于组还没有创建出来,连不上或者等待超时。正确流程是必须保证第一个节点彻底完成引导并进入ONLINE状态之后,其他节点再逐个加入。

4.5 验证组状态

通过performance_schema可以查看组复制视图。

SELECT * FROM performance_schema.replication_group_members;

期望看到三个成员都显示为ONLINE,对于单主模式,其中有一个成员的MEMBER_ROLE为PRIMARY,其余为SECONDARY。再检查一下复制通道状态:

SELECT channel_name, service_state FROM replication_group_member_stats;

这套验证下来正常,就可以把业务写流量逐渐切到PRIMARY节点上了。

5. 常用运维操作和注意事项

5.1 组复制日常维护的正确姿势

MGR日常运维中,最需要适应的一点是:你不能再以传统主从的思路去"操作"节点,比如手动改read_only、手动执行change master。组内角色的切换是自动协商的,外部干预反而容易扰乱视图一致性。

节点需要重启时,正确的步骤是先停业务流量,再在该节点上STOP GROUP_REPLICATION,然后停止MySQL实例。重启后START GROUP_REPLICATION即可重新加入组。不要直接kill -9,除非你想测试副本故障恢复的极限场景。

需要移除一个节点时,不要直接从组里剔除,而是在该节点上执行STOP GROUP_REPLICATION,它会先通知组内其他成员自己正在离开,组内视图会自动更新。如果节点故障到已经无法通信,其他成员会在一定时间后通过多数派机制把故障节点从视图里移除,这是MGR自动完成的。

5.2 成员加回和数据追平

分布式恢复机制允许节点在短暂离线后重新追平数据。假设节点B因为断电离线,恢复后启动MySQL,再执行START GROUP_REPLICATION,它会通过组内通信通道从其他成员获取离线期间缺失的事务日志并回放。只要离线期间的binlog没有被清理掉,就能顺利追平。如果离线太久,其他节点的binlog已经被清理,节点B就需要重新全量初始化。

全量初始化的思路是:选择一个在线成员,用备份工具备份后恢复到离线节点上,恢复时注意保留GTID信息,然后重新启动组复制。干净的备份和明确的GTID范围,是MGR处理长时间离线节点回来的最好方式。

5.3 MGR和备份策略的组合

MGR节点和传统主从一样需要备份,而且备份建议打到SECONDARY节点上,避免主节点在承担业务写入的同时还要承受备份带来的额外I/O压力。

比较实用的方式是,选一个SECONDARY节点,用开源的备份工具基于物理备份方式做全量备份,这个备份可以用于任意节点的恢复,也可以用于长时间离线节点的追平。

恢复时的要点是要保留备份中的GTID执行信息,MGR节点重新加入组时依赖这个集合判断自己缺哪些事务。如果不保留或丢失了GTID,节点恢复后会出现一种很尴尬的状态:数据文件可以正常启动,但组复制一直认为这个成员的GTID集合和组内不一致,拒绝让它进入ONLINE状态。

5.4 监控指标关注清单

MGR的监控和传统主从有很大区别。传统主从主要看seconds_behind_master,MGR则看这些:

指标说明关注原因
MEMBER_STATE成员是否ONLINE一旦变为RECOVERING或ERROR需要立即介入
MEMBER_ROLE是主还是备单主模式下必须只有一个主
group_replication_primary_member当前主节点切换后业务连接感知需要更新
count of transactions committed组内提交事务数各节点差异过大说明回放有问题
flow control状态是否被流控流控抑制会导致慢查询和写入超时

MGR在很多环节会出现流控,比如某个节点严重落后时,组会限制新事务提交来让落后节点追平。这个设计是保证一致性的,但很多人会遇到写入变慢,不是因为数据库慢,而是被流控压制了。排查时需要看是否有成员状态异常。

6. MGR常见问题排查实录

6.1 启动组复制时报错"Can't start group replication"

这个报错是范围最大、原因最多的一类。我遇到过的原因有下面几种,排查时可以按顺序走:

  • 插件没有正确加载,确认INSTALL PLUGIN已经执行
  • 配置了binlog_checksum=CRC32,改回NONE再重启实例
  • 没有开启GTID,检查gtid_mode和enforce_gtid_consistency
  • server_id为0或者重复
  • 本机数据目录里已经有非GTID环境遗留的旧事务数据

排查时可以查看MySQL错误日志,或通过SHOW GROUP_REPLICATION STATUS相关字段定位到具体阶段。大部分情况在日志里的提示都比较明确,只是很多人懒得看日志。

6.2 节点状态一直在RECOVERING

这是MGR部署中最常见的卡壳环节。节点加入组后,需要经历分布式恢复,如果恢复异常或失败,就会一直显示RECOVERING。

第一步看错误日志,常见的是无法连接复制源、复制账号没有权限、GTID缺失。第二步检查各节点之间的33061端口连通性。第三步确认节点B的复制账号是否和主节点一致——不同的MGR节点实际上用的是同一个复制账号连接彼此,授权必须一致。

还有一个隐藏较深的情况:新节点原本是一个独立运行的MySQL实例,数据目录里残留着和组内完全无关的事务,此时就算GTID已经开启,也无法和组内的GTID集合对齐,会一直处于RECOVERING直到选择重新初始化。

6.3 网络抖动引发的成员自动退出

MGR通过组内成员间的通信消息来判断其他节点是否存活。如果某个节点在设定的时间内没有响应,会被其他成员标记为可疑,进而从组成员视图中移除。注意这个判定基于的是Paxos协议的通讯超时,和MySQL层面的连接超时不是一回事。

遇到成员自动退出的情况,先不要急着拉起节点,先检查这段时间网络是否有丢包、CPU是否被打满、磁盘I/O是否有尖刺。MGR对网络和硬件资源的敏感程度远高于普通主从,生产环境里我见过不少次因为网络瞬时抖动导致成员被误判退出,最后靠STOP和START组复制操作让节点重新加回。

6.4 单主模式下切换到新主后业务连不上

单主模式的主节点是动态选举的。老主故障后,新的主节点会被选出来。如果业务端连接的是固定IP,此时就会出现"连不上"的情况,因为连接串指向的还是老主。

这不是MGR的问题,而是连接层配套问题。解决办法是在前面放一个软路由、域名切换或代理中间层,让业务永远指向一个逻辑地址。MGR负责底层的数据一致性和角色切换,外部这一层负责让业务感知不到切换。把这两层混淆,就会误以为MGR没有做好高可用。

6.5 常见问题速查表

现象可能原因处理方式
启动报错无法加载插件插件未安装或路径不对检查INSTALL PLUGIN
状态RECOVERING复制账号权限不足或GTID不匹配核对复制账号、重置GTID
成员反复加入退出网络抖动或通信端口阻塞放行组通信端口、检查网络
写入变慢流控触发或节点落后查看流控状态、定位落后节点
事务被杀多主模式冲突检测单主模式下一般不会发生;多主需检查业务并发冲突
无法引导组组名不一致或bootstrap未关闭统一UUID、按顺序引导

7. 实操体会和一点扩展建议

MGR上手最困难的地方不在配置项多,而在于需要转变一个观念:它不再是一个"手动喊口令"的高可用方案,而是一个自带共识协议的自治系统。适应这一点后,日常运维会轻松很多。

我个人的习惯是把MGR当成一个"带自愈能力的同步复制集群"来用,它不适合去解决所有问题。比如它不能帮你解决SQL慢查询,不能挽救因为业务设计缺陷造成的死锁风暴,也不会自动处理无主键大表的性能灾难。它是数据高可用层面的底座,不是业务性能的救世主。

再分享一个小技巧:在MGR的标准配置里加一条监控规则,重点盯MEMBER_STATE和流控状态。具体来说,在监控系统里对所有节点做指标采集,发现非ONLINE状态持续超过30秒就告警,流控超过50%持续5分钟也告警。这样做看起来简单,实际上能规避掉大多数"成员已离线但没人注意"的情况。

从长期维护角度看,后续如果业务增长导致某个SECONDARY节点的读压力过大,可以考虑给该节点额外增加传统的异步从库,把只读流量继续往下甩。MGR成员和它下挂的异步从库共存没有原则性冲突,但要注意下挂从库的binlog配置保留完整,以便从库和MGR成员之间的延迟可控。

这个方向如果是刚从主从复制迁移过来,建议小范围试点几个业务再逐步铺开。MGR本身的调试周期通常不会超过一天,真正需要时间的是让团队习惯它的运维方式。把"三节点、单主、自动切换"这套标配跑稳了,再考虑多主或者跨机房扩展,就会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进