上周又有人问我 MGR 的事,说测试环境跑通了,一上生产就各种节点加不进去、自动退出、主库切完业务直接报错。这套路我太熟了。MySQL MGR(MySQL Group Replication,组复制)单主高可用集群部署这条路我自己走过很多遍,从最初照着官方文档一行行敲命令,到后来看见报错码大概能猜出是哪一层出的问题,中间交的学费不算少。这篇文章想干的事很简单:把散落在手册、社区帖子和深夜调试记录里的东西,按一条完整的部署链路重新串一遍,让你照着做能跑起来,出了问题知道往哪儿看。
MGR 说白了就是 MySQL 官方自带的一套基于多数派投票的复制机制,节点之间互相通信、互相确认事务,多数派确认了才算提交。它解决的是传统一主多从那套方案里最难受的几件事:主库挂了谁来接、切换时数据会不会丢、脑裂怎么防。单主模式指的是集群里同一时刻只有一个节点可写,其余节点只读,主库故障时由组内成员自动选出新的主库。这套东西适合谁?适合那些业务体量在单机 MySQL 能扛住的范围内、又实在不能接受几分钟以上停机、同时不想引入太复杂外部组件的团队。如果你正被 MHA 的切换脚本折磨,或者预算买不起商业数据库,那这篇文章基本就是给你写的。
1. 方案选型:MGR 单主到底适合什么样的场景
动手之前先把选型这件事聊透,比上来就敲命令重要得多。我见过太多团队是因为"听说 MGR 是高可用方案"就直接上了,结果架构跟业务特征完全不匹配,后期运维成本比原来还高。
1.1 老式主从加切换脚本,真正的问题在哪
传统的做法是一主两从,加一个 Keepalived 做 VIP 漂移,或者上 MHA、 orchestrator 这类外部切换工具。这套组合在上一个十年里确实是主流,但它有几个绕不过去的硬伤。
第一个是数据丢失。异步复制下,主库把 binlog 发给从库就返回客户端成功,从库可能还没收到。主库这时候宕机,从库上就缺了最后那部分事务。你可能会说半同步能解决,半同步确实能减少丢数据的窗口,但它带来的是主库提交延迟上升,而且半同步在超时后会退化回异步,很多人根本没注意这个降级行为。
第二个是脑裂。Keepalived 靠心跳判断对端存活,网络抖动一下就可能误判,两个节点同时认为自己是主,VIP 飘来飘去,业务写两边,数据直接分裂。事后修数据的过程极其痛苦,我见过一次修了三天的。
第三个是切换逻辑的复杂度。什么时候切、切到哪个从库、切换后其他从库怎么重新指向新主、GTID 差集怎么补,每一个环节都要写脚本处理,脚本本身还可能出 bug。MGR 把这些东西全部收进内核里了,事务提交需要多数派确认,天然不存在脑裂窗口,主库失效由协议自己选新主,不需要任何外部脚本参与决策。
1.2 单主模式和多主模式的取舍逻辑
MGR 支持两种模式,单主和多主。多主模式下每个节点都能接受写入,听起来很美好,实际用起来坑不少。
多主的核心问题是冲突检测。每个事务提交前都要把写集(write set)广播给所有节点做认证,判断有没有跟别的事务冲突。如果两个节点同时更新同一行,后提交的那个会被回滚,客户端收到的是死锁或者回滚错误。业务侧如果不做重试处理,就会看到莫名其妙的失败。而且热点行更新在电商、计费这类场景里非常常见,多主模式下冲突概率会急剧上升,实际吞吐反而不如单主。
另一个问题是多主对自增主键、外键、触发器的限制,官方文档里列了一长串,配起来非常别扭。单主模式下这些限制基本都不存在,因为只有一个节点在写,冲突检测几乎不会被触发,认证流程的开销也低得多。
我的建议很直接:除非你有明确的写扩展需求,并且已经做好了业务侧的重试和分片设计,否则一律上单主。单主扛不住的写入量,靠加节点是解决不了的,那得上分库分表,那是另一个维度的工程。单主模式的写能力上限基本就是单机 MySQL 的上限,能扛住多少取决于你的硬件和 SQL 质量,MGR 本身不会让它变慢太多,组播和认证带来的额外开销通常在 5% 到 15% 之间。
1.3 集群规模、网络时延与硬件的最低门槛
节点数量这块,MGR 用的是多数派算法,容错能力和节点数是这个关系:3 节点容忍 1 个故障,5 节点容忍 2 个。偶数节点没有任何意义,4 个节点的容错能力和 3 个一样,但多了一份复制开销。所以要么 3 要么 5,官方上限是 9 个。
大多数团队 3 节点就够了,成本可控,容错能力也够用。金融类或者对可用性要求极高的场景可以上 5 节点,但要注意节点越多,事务提交需要等待的确认也越多,延迟会上升。
网络这块是很多人忽略的。MGR 的成员之间要保持长连接,事务认证走的是组通信。官方建议成员之间的网络往返时延控制在 5ms 以内,超过这个值,认证队列容易堆积,流控会被触发,写入性能断崖式下跌。同机房部署基本没问题,同城双活要看实际链路质量,跨城基本别想,除非你能接受写入性能砍半。
带宽也要算一笔账。每个事务的写集都要广播给所有成员,粗略估算,网络出口带宽大约是写入量的 (节点数 - 1) 倍。如果业务写入峰值为 50MB/s,5 节点集群主库出口至少要 200MB/s,也就是 1.6Gbps,这个量级得上万兆网卡了。
硬件方面,磁盘的 fsync 性能直接影响提交延迟,因为 MGR 不能靠关闭innodb_flush_log_at_trx_commit来换性能,那样会在故障时丢数据。建议用 SSD,最好是 NVMe。CPU 核数决定了组复制线程和应用线程的并行度,4 核起步,8 核以上比较舒服。
提示:千万不要在已经有大量数据的单机 MySQL 上直接开 MGR,先做一次全量备份,然后在新集群上恢复。在存量实例上直接改配置启组复制,失败的姿势千奇百怪。
2. 部署前的环境准备:这一层做不好后面全是坑
部署失败的原因里,有相当一部分根本不是 MGR 本身的问题,而是主机名解析、时钟、防火墙这些基础层没弄好。这一层我建议花半小时认真过一遍,能省掉后面几小时的排查。
2.1 操作系统与网络层的四件事
第一件是主机名解析。MGR 的group_replication_local_address和group_replication_group_seeds用的是主机名加端口,如果解析不到或者解析不一致,节点之间根本连不上。最稳妥的做法是在每台机器的/etc/hosts里把所有节点的映射都写死,不要依赖 DNS,DNS 出问题的时候排查会让你怀疑人生。
# 三台机器上都要有这三行 192.168.10.11 db1 192.168.10.12 db2 192.168.10.13 db3第二件是时间同步。MGR 本身不强制要求时钟完全一致,但慢查询日志、错误日志、binlog 的时间戳对不上,排查问题时会非常难受。用 chrony 或者 ntpd 都行,确保三台机器的时间偏差在毫秒级。
# chrony 配置示例,指向内网时间源 server ntp.internal iburst第三件是防火墙。除了 MySQL 服务端口 3306,MGR 还需要一个专用的组通信端口,习惯上配成 33061。这个端口只在成员之间使用,不需要对业务网开放,但成员之间必须互通。很多人只开了 3306,然后看着ERROR 3096发呆。
第四件是文件和进程限制。MySQL 在高并发下会打开大量文件描述符,默认的 1024 肯定不够。
# /etc/security/limits.conf mysql soft nofile 65535 mysql hard nofile 65535 mysql soft nproc 65535 mysql hard nproc 65535SELinux 这块,如果团队没有专门的安全策略维护能力,测试环境直接设成 permissive,生产环境按需配置策略。我遇到过好几次因为 SELinux 拦截导致数据目录初始化失败的情况,报错信息还特别隐晦。
2.2 参数预调:写在配置文件里比动态设置靠谱
MGR 相关的参数有一部分必须在配置文件里写好,因为实例启动时就要加载插件。动态SET GLOBAL只适合调优类参数,不适合初始化类参数。下面这份是我常用的配置模板,你可以直接抄,改一下路径和 IP 就行。
[mysqld] server_id = 1 # 每个节点必须不同 port = 3306 datadir = /data/mysql/3306/data socket = /data/mysql/3306/mysql.sock log_error = /data/mysql/3306/error.log pid_file = /data/mysql/3306/mysql.pid # ---------- 复制与 GTID ---------- gtid_mode = ON enforce_gtid_consistency = ON binlog_format = ROW binlog_checksum = NONE log_bin = /data/mysql/3306/binlog/mysql-bin binlog_expire_logs_seconds = 604800 sync_binlog = 1 # 8.0.26 之前叫 log_slave_updates,之后推荐用新名字 log_replica_updates = ON # ---------- 事务与刷盘 ---------- innodb_flush_log_at_trx_commit = 1 innodb_buffer_pool_size = 8G innodb_flush_method = O_DIRECT innodb_redo_log_capacity = 4G # 8.0.30 及以上;更早版本用 innodb_log_file_size # ---------- MGR ---------- plugin_load_add = 'group_replication.so' plugin_load_add = 'mysql_clone.so' group_replication_group_name = "8f3c1a76-2b4d-11ef-9c8a-005056a1b2c3" group_replication_start_on_boot = OFF group_replication_local_address = "db1:33061" group_replication_group_seeds = "db1:33061,db2:33061,db3:33061" group_replication_bootstrap_group = OFF group_replication_single_primary_mode = ON group_replication_enforce_update_everywhere_checks = OFF group_replication_member_expel_timeout = 10 group_replication_autorejoin_tries = 3 group_replication_exit_state_action = READ_ONLY group_replication_consistency = BEFORE_ON_PRIMARY_FAILOVER # ---------- 其他 ---------- max_connections = 1000 character_set_server = utf8mb4几个参数单独解释一下为什么这么配。
binlog_checksum = NONE是 MGR 的硬性要求。组复制的认证机制需要读取 binlog 事件的内容做校验,如果开启了 checksum,事件里会带上校验和字段,解析会出问题。这个参数不设对,节点加入时会直接报错。
group_replication_start_on_boot = OFF是故意设的。如果设成 ON,实例一启动就自动尝试加入组,但首次搭建时第一个节点需要用 bootstrap 方式启动,自动加入会导致启动失败或者组状态异常。建议整个集群稳定运行一段时间之后,再考虑改成 ON,让实例重启后能自动归队。
group_replication_member_expel_timeout = 10把默认的 5 秒延长到 10 秒,目的是容忍短时间的网络抖动。默认值太激进,一次 GC 停顿或者网络瞬断就可能把节点踢出去,然后触发一轮不必要的分布式恢复。
group_replication_autorejoin_tries = 3让被踢出的节点自动尝试重新加入三次,每次间隔 5 分钟。没有这个参数的话,节点被踢出后需要人工介入。
group_replication_consistency = BEFORE_ON_PRIMARY_FAILOVER是我比较推荐的一个值。它保证主库切换后,新主库上的读请求会等待积压事务应用完再返回,避免业务读到旧数据。设成AFTER一致性更强,但每个事务都要等所有节点确认,性能损失比较明显,除非业务对一致性要求极高,否则不建议。
group_replication_enforce_update_everywhere_checks在单主模式下必须是 OFF,这个参数是给多主模式用的。设错了节点根本起不来。
注意:
group_replication_group_name必须是一个合法的 UUID 格式,可以用SELECT UUID()生成一个,整个集群用同一个值。千万不要用全零 UUID,也不要每个节点配不一样的值,后者会导致节点之间根本不认为自己在同一个组里。
2.3 账号、目录与端口规划表
部署之前把规划表拉出来,比边做边想要高效得多。下面这张表是我按三节点最小集群写的,你可以直接改 IP 用。
| 项目 | db1 | db2 | db3 | 说明 |
|---|---|---|---|---|
| 内网 IP | 192.168.10.11 | 192.168.10.12 | 192.168.10.13 | 同一网段,RTT 小于 5ms |
| server_id | 1 | 2 | 3 | 全局唯一,不能重复 |
| MySQL 端口 | 3306 | 3306 | 3306 | 业务访问端口 |
| 组通信端口 | 33061 | 33061 | 33061 | 仅成员间通信 |
| 数据目录 | /data/mysql/3306/data | 同左 | 同左 | 建议独立磁盘 |
| 初始角色 | PRIMARY | SECONDARY | SECONDARY | 由 db1 引导 |
账号方面需要准备两个。一个是复制账号,MGR 内部用它建立成员间的复制通道;另一个是管理账号,用来执行 MGR 的管理命令比如切换主库。
-- 复制账号,三个节点都要建,且必须一致 CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl_Str0ng_Pwd'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; -- 克隆账号,用于节点重建时拉取全量数据 GRANT BACKUP_ADMIN, CLONE_ADMIN ON *.* TO 'repl'@'%'; -- 管理账号 CREATE USER 'mgr_admin'@'%' IDENTIFIED BY 'Mgr_Adm1n_Pwd'; GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'mgr_admin'@'%'; GRANT GROUP_REPLICATION_STREAM ON *.* TO 'mgr_admin'@'%';GROUP_REPLICATION_STREAM这个权限容易被漏掉,它允许账号使用组复制专用的复制通道。
目录结构我一般按端口号分,这样一台机器跑多个实例时不会打架。数据目录、binlog 目录、错误日志目录分开建,binlog 单独放一块盘有助于排查 IO 问题。
3. 逐步实操:三节点单主集群从零搭起来
准备工作做完,接下来就是真刀真枪的部署。这段我按实际执行的顺序写,每一步都说明它在干什么,而不是单纯甩命令。
3.1 实例初始化与基础配置落地
三台机器上的操作完全一样,只有server_id和group_replication_local_address不同。先建目录,再初始化数据目录。
# 建目录 mkdir -p /data/mysql/3306/{data,binlog,log,tmp} chown -R mysql:mysql /data/mysql # 初始化实例,注意 --initialize-insecure 会生成空密码 root mysqld --defaults-file=/etc/my.cnf --initialize-insecure --user=mysql # 启动 mysqld_safe --defaults-file=/etc/my.cnf &初始化完成后登录进去,先把 root 密码改掉,再建前面说的那几个账号。这里有个细节:复制账号必须在每个节点上单独创建,因为 MGR 加入组的时候不会同步账号信息,账号是实例级别的对象。很多人以为主库建了账号从库就有,然后卡在ERROR 1045上。
然后把group_replication_start_on_boot保持 OFF 的前提下,手工加载插件并检查。
-- 确认插件已加载 SELECT PLUGIN_NAME, PLUGIN_STATUS FROM information_schema.PLUGINS WHERE PLUGIN_NAME IN ('group_replication', 'clone'); -- 确认关键参数 SHOW VARIABLES LIKE 'group_replication%'; SHOW VARIABLES LIKE 'gtid_mode'; SHOW VARIABLES LIKE 'binlog_checksum';gtid_mode必须是 ON,binlog_checksum必须是 NONE,这两个不满足,后面START GROUP_REPLICATION会直接失败。
3.2 复制通道与 MGR 插件的准备
MGR 在内部用的是标准的复制通道,但通道名不是默认的,而是固定为group_replication_recovery。这个通道负责新成员加入时的增量或全量数据同步。
在引导组之前,需要在每个节点上先配好这个通道的凭据,让它在需要的时候能自动连上捐赠者。
-- 停掉可能存在的组复制 STOP GROUP_REPLICATION; -- 配置恢复通道的凭据 CHANGE REPLICATION SOURCE TO SOURCE_USER='repl', SOURCE_PASSWORD='Repl_Str0ng_Pwd' FOR CHANNEL 'group_replication_recovery';这里我用的是 MySQL 8.0.23 之后的新语法CHANGE REPLICATION SOURCE TO。老版本对应的是CHANGE MASTER TO ... FOR CHANNEL,如果版本更早,语法要相应调整。
另外,加入组之前要把本地 binlog 清干净。因为一个全新的节点如果带着本地事务的 GTID 集合加入,组会认为它有额外的数据,直接拒绝。
-- 8.0.21 及以上推荐用新语法 RESET BINARY LOGS AND GTIDS; -- 老版本用 RESET MASTER;这一步只在首次搭建或者节点重建时做,已经在组里的节点千万别执行,那会清掉正在复制的 binlog。
还有一个前置检查是server_uuid必须唯一。克隆出来的实例、从虚拟机模板复制的机器,auto.cnf里的 UUID 可能是一样的,这种情况下节点加入会被拒绝,报的错还特别含糊。检查方法是:
SELECT @@server_uuid;三台机器输出必须不一样,一样的话删掉datadir/auto.cnf重启,会自动重新生成。
3.3 引导首个节点:bootstrap 只做一次
这是整个部署过程中最容易出事的一步。引导(bootstrap)的意思是让第一个节点自己组建一个只有它自己的组,其他节点再往里加。关键在于:整个集群生命周期内,bootstrap 只能有一次。如果第二个节点也去 bootstrap,就会形成两个独立的组,两边都认为自己是正统,数据从此分裂。
在第一台机器 db1 上执行:
SET GLOBAL group_replication_bootstrap_group = ON; START GROUP_REPLICATION; SET GLOBAL group_replication_bootstrap_group = OFF;三行必须按顺序执行,最后一行尤其不能忘。忘了关这个开关,之后实例重启时会再次尝试引导,那就真的分裂了。
验证一下状态:
SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;正常情况下会看到只有一行,MEMBER_STATE为ONLINE,MEMBER_ROLE为PRIMARY。如果状态是RECOVERING那说明还在初始化,等几秒再看;如果直接是ERROR,去看错误日志,通常是参数没配对。
提示:如果执行完发现状态一直卡在
RECOVERING,先看group_replication_local_address的端口能不能 telnet 通,很多时候是防火墙没放行 33061。
3.4 其余节点逐个加入与状态验证
db1 稳定运行后,去 db2 上执行加入。加入的时候要指定种子节点,这些种子只是用来发现组的,加入成功后种子列表就不重要了。
-- db2 上执行 START GROUP_REPLICATION;因为配置文件里已经写好了group_replication_group_seeds,直接 START 就行。如果种子节点里有任何一个是可达的,就能成功加入。
加入过程分两个阶段:先做分布式恢复,把 db1 上已有的数据同步过来;然后进入认证阶段,开始接收新的事务。数据量大的话,恢复阶段会持续比较久,这段时间节点的状态是RECOVERING。
-- 在任意已加入的节点上查看 SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;db2 加入成功后,同样的步骤在 db3 上做一遍。全部完成后,三行都应该是ONLINE,其中一行是PRIMARY,两行是SECONDARY。
想看得更细一点,可以看成员统计表:
SELECT MEMBER_ID, COUNT_TRANSACTIONS_IN_QUEUE AS queue_len, COUNT_TRANSACTIONS_CHECKED AS checked, COUNT_CONFLICTS_DETECTED AS conflicts, TRANSACTIONS_WAITING_CERTIFICATION_QUEUE AS wait_cert FROM performance_schema.replication_group_member_stats;COUNT_CONFLICTS_DETECTED在单主模式下应该一直是 0,一旦有增长说明配置有问题,比如不小心开了多主或者有节点被误认为可写。
3.5 客户端入口:把读写请求导向正确的地方
集群搭起来了,但业务怎么连?单主模式下只有主库能写,从库默认是super_read_only = ON,直连从库写会报错。这就需要一层路由。
最省事的方案是用 MySQL Router,它跟 MGR 配合得很好,能自动感知拓扑变化。配置里定义一个读写端口和一个只读端口,Router 会定期查询组信息,自动把流量指向正确的主库。
[routing:rw] bind_address = 0.0.0.0 bind_port = 6446 destinations = metadata-cache://mycluster/?role=PRIMARY routing_strategy = first-available [routing:ro] bind_address = 0.0.0.0 bind_port = 6447 destinations = metadata-cache://mycluster/?role=SECONDARY routing_strategy = round-robin另一条路是用 ProxySQL,功能更复杂,能做读写分离、查询缓存、连接池,代价是配置和维护成本高一些。如果业务规模不大、只需要一个自动漂移的写入口,MySQL Router 就够了。
还有一种做法是应用层自己做,连接配置里配三个地址,启动时探测哪个是主库。这种做法我不太推荐,因为主库切换后应用需要重连,重连逻辑写不好会引发一连串超时。用中间件把这件事挡在外面会清爽很多。
注意:不管用哪种方案,一定要把主库地址从应用配置里彻底拿掉。我见过好几次集群本身切换成功了,但因为应用里硬编码了 db1 的 IP,业务还是挂了好几分钟。
4. 故障切换、节点维护与日常运维
集群跑起来只是开始,真正的考验在故障发生的那一刻。这部分我想把 MGR 在故障时的行为讲清楚,这样你看到日志和状态变化时不会慌。
4.1 主节点意外宕机时会发生什么
假设 db1 突然断电。组里剩下 db2 和 db3 两个节点,多数派是 2,刚好满足。它们会在几秒内检测到 db1 失联,触发 expelled 计时器,超时后把 db1 从成员列表里踢掉,然后发起新一轮选举。
选举的依据包括成员的group_replication_member_weight(默认都是 50,可以调高某个节点的值让它优先当选)、成员 UUID 的字典序等。整个过程通常在 10 到 30 秒内完成,具体取决于网络和member_expel_timeout的设置。
切换完成后,新的主库会自动将super_read_only关掉,开始接受写入。老的主库 db1 恢复后,group_replication_start_on_boot如果是 ON 会自动尝试归队,OFF 的话需要手工 START。
这里有个非常关键的坑:旧主库归队时,如果它本地还有未提交的事务或者 GTID 差集过大,分布式恢复会自动选择全量克隆(clone)方式,把整个数据目录覆盖掉。这是正常行为,但如果旧主库上有还没来得及归档的本地 binlog,就会被清掉。所以故障后如果想保留现场做分析,先把 binlog 备份出来再让节点归队。
如果想控制这个行为,可以调整group_replication_clone_threshold。默认值是 1000000,意思是差距超过 100 万个事务才走 clone。设得太小会导致频繁全量克隆,设得太大则增量恢复会非常慢。
4.2 计划内主节点切换的标准动作
计划内切换(比如主库要升级硬件、打补丁)比故障切换简单得多,用一条命令就能完成。
-- 先查出目标节点的 UUID SELECT MEMBER_ID, MEMBER_HOST FROM performance_schema.replication_group_members; -- 指定新的主库 SELECT group_replication_set_as_primary('目标节点UUID');执行后,当前主库会先把所有未完成的事务推送完,然后降级为只读,目标节点升级为主库。整个过程是原子的,不会出现两个主库同时可写的窗口。
切换完成后要立刻验证:
SELECT MEMBER_HOST, MEMBER_ROLE FROM performance_schema.replication_group_members; SHOW VARIABLES LIKE 'super_read_only'; SHOW VARIABLES LIKE 'read_only';新主库上的super_read_only和read_only都应该是 OFF,从库上都是 ON。
如果集群原本是多主模式,想改成单主,用SELECT group_replication_switch_to_single_primary_mode();,反过来用SELECT group_replication_switch_to_multi_primary_mode();。切换过程中会有短暂的不可写窗口,选在业务低峰做。
4.3 节点重新加入与克隆式分布式恢复
节点掉了之后想加回来,流程跟首次加入差不多,但多几个检查点。
先看本地数据目录是不是干净的。如果节点是因为磁盘故障重建的,数据目录是空的,直接 START 就会走全量恢复。如果节点只是网络抖动被踢出,本地数据还在,那要判断 GTID 差集有多大。
-- 被踢出的节点上查看 SELECT @@gtid_executed;如果差距不大,MGR 会自动走增量恢复,从捐赠者那边拉 binlog 补上。这种情况下不需要任何额外操作,直接:
START GROUP_REPLICATION;如果差距很大,或者说错误日志里提示Clone process has been initiated,那就是走全量克隆了。克隆过程会自动停实例、复制数据、重启实例,你只需要等。
有时候会遇到节点卡在ERROR状态起不来,这时候要先STOP GROUP_REPLICATION清理掉错误状态,再重新 START。如果还是不行,可能需要RESET BINARY LOGS AND GTIDS把本地数据清空,强制走全量克隆。
STOP GROUP_REPLICATION; RESET BINARY LOGS AND GTIDS; START GROUP_REPLICATION;注意:执行
RESET BINARY LOGS AND GTIDS之前一定要确认这个节点的数据可以被覆盖。如果这个节点是组里唯一还存活的节点,清空它等于清空整个集群。
4.4 必须盯住的监控指标
MGR 的监控重点和传统主从不太一样,除了常规的 QPS、连接数、慢查询,还有几个组复制特有的。
| 指标来源 | 关键字段 | 健康阈值 | 异常含义 |
|---|---|---|---|
| replication_group_members | MEMBER_STATE | 全部 ONLINE | 有 RECOVERING/ERROR 需关注 |
| replication_group_members | MEMBER_ROLE | 恰好 1 个 PRIMARY | 多个 PRIMARY 说明模式错乱 |
| replication_group_member_stats | COUNT_TRANSACTIONS_IN_QUEUE | 小于 1000 | 认证队列堆积 |
| replication_group_member_stats | COUNT_CONFLICTS_DETECTED | 恒为 0 | 非 0 说明有写入冲突 |
| replication_group_member_stats | TRANSACTIONS_WAITING_CERTIFICATION_QUEUE | 小于 100 | 认证压力过大 |
| 错误日志 | 关键字 EXPELLED / UNREACHABLE | 无 | 网络或节点异常 |
COUNT_TRANSACTIONS_IN_QUEUE这个值最能反映集群健康度。它表示认证队列里等待处理的事务数,正常应该是个位数。持续增长说明某个节点应用事务的速度跟不上,触发了流控。这时候要看该节点的 CPU 和磁盘 IO,往往是磁盘写不动了。
流控参数在group_replication_flow_control_*这一组里。默认的group_replication_flow_control_mode = QUOTA,根据队列长度动态调整。如果业务对延迟敏感,可以把阈值调高一点让流控晚点触发,但代价是节点之间的数据延迟会变大。
还有一个容易忽略的点是磁盘空间。binlog、redo log、克隆过程中的临时文件都会吃空间。克隆一个几百 G 的库,临时文件可能也要几百 G,空间不够会直接失败。建议数据目录所在分区保留 40% 以上的余量。
5. 常见报错与排查实录
这部分是我压箱底的东西,基本上你能遇到的错误都在这里了。
5.1 加入集群阶段的经典错误码
ERROR 3092 (HY000): The server is not configured properly to be an active member of the group,这个是最常见的。它本身是个笼统的错误,具体原因要看紧跟着的错误日志。我总结下来八成是这几种:server_id冲突、gtid_mode没开、binlog_checksum不是 NONE、log_replica_updates没开、group_replication_group_name不是合法 UUID。按顺序检查一遍基本能定位。
ERROR 3096 (HY000): The START GROUP_REPLICATION command failed,这个通常是网络问题。检查group_replication_local_address配置的端口有没有被监听,成员之间能不能互通。在任意节点上telnet db1 33061试一下,连不通就查防火墙。
ERROR 3098 (HY000): The server cannot join the group because it has a different group name,组名不一致。三台机器上这个参数必须完全一样,包括大小写和连字符。
ERROR 3099和ERROR 3100通常跟本地事务有关。节点本地有未提交的事务或者 GTID 集合不为空,需要RESET BINARY LOGS AND GTIDS。
还有一个报错码是 3092 的变种,提示This member has more executed transactions than those present in the group,意思是你这个节点的数据比组里还多,这几乎必然是之前 bootstrap 过或者从别的组迁移过来的实例。处理办法就是清空本地 GTID 后重新加入。
| 错误码 | 大概率原因 | 快速处置 |
|---|---|---|
| 3092 | 参数不合规(server_id/gtid/checksum) | 逐项核对配置 |
| 3096 | 组通信端口不通 | 查防火墙与监听 |
| 3098 | group_name 不一致 | 统一为同一 UUID |
| 3099/3100 | 本地残留事务或 GTID | 清 binlog 与 GTID 后重试 |
| 1045 | 复制账号不存在或密码错 | 每个节点单独建账号 |
| 2003 | 种子节点地址解析失败 | 检查 /etc/hosts |
5.2 网络、认证与时钟类问题
账号问题我单独拎出来说,因为它太常见了。ERROR 1045 (28000): Access denied for user 'repl'@'db2',原因是复制账号只在主库建了,新加入的节点上没有这个账号。MGR 的账号是实例级别的,不随复制同步。解决办法就是每个节点都执行一遍建账号的 SQL,密码必须一致。
还有一种情况是账号建了但还是连不上,看错误信息里的主机名部分。MySQL 的账号是user@host形式,如果建的是'repl'@'192.168.10.%',而连接时用的是主机名,匹配不上就会拒绝。建议统一用'repl'@'%',或者把主机名和 IP 两种都建一遍。
时钟问题不会直接导致加入失败,但会让排查变得极其困难。有一次我排查一个认证队列堆积的问题,看了半天日志时间对不上,最后发现某台机器的时钟慢了两分钟,日志顺序完全乱了。定位问题前先date一下三台机器,能省很多事。
5.3 流控与性能抖动
业务侧反馈的现象通常是"偶尔有几秒钟写入特别慢",但查慢查询又查不出什么。这种情况八成是流控。
流控被触发的标志是错误日志里出现Flow control相关的信息,或者replication_group_member_stats里的COUNT_TRANSACTIONS_IN_QUEUE突然飙高然后回落。
根因通常是某个从库应用事务太慢,认证队列越积越多,主库为了不把从库甩太远,主动降速。要解决这个问题,得先弄清楚从库为什么慢。常见原因包括:从库的innodb_buffer_pool_size设得比主库小、从库磁盘性能差、从库上有大查询在抢资源。
还有一种情况是大事务。单个事务几百万行更新,在 MGR 里会被当成一个整体广播和认证,耗时非常长。这种大事务还会导致 binlog 传输时间过长,进而触发member_expel_timeout把节点踢出。所以业务上要尽量避免一次性大批量 update,改成按主键分批。
-- 分批更新的写法示例 UPDATE t_order SET status = 'DONE' WHERE status = 'PENDING' AND id > 0 ORDER BY id LIMIT 2000; -- 循环执行直到影响行数为 0分片大小我一般建议 1000 到 5000 行一批,具体看单行大小。每批之间 sleep 几十毫秒,给复制留点喘息时间。
5.4 几条用血换来的实操心得
第一,永远不要在生产环境的第一个节点上直接 bootstrap。先把整个流程在测试环境跑两遍,包括模拟主库宕机、节点被踢出、节点重建这些场景,确认业务侧的重连逻辑没问题,再动生产。MGR 的第一次 bootstrap 如果搞错了,代价是重来一遍。
第二,记录 bootstrap 序列号。我在配置里会加一个注释,写明这个集群是什么时间、由哪个节点 bootstrap 的。听起来很傻,但当你面对一堆环境时,能快速确认哪个是原始组,比翻日志强。
第三,克隆阈值要根据数据量调。默认 100 万事务对小库来说是合适的,但对每天几百万事务的库,可能一天就触发一次全量克隆。这种场景下应该把group_replication_clone_threshold调大,让增量恢复承担更多工作。
第四,从库的硬件不要省。很多人觉得从库只读,配低一点没事。但 MGR 的从库要应用所有事务,硬件差了就会拖累整个集群。从库的规格至少跟主库持平吧,我是这么配的。
第五,定期做一次演练。挑个低峰期,主动 kill 掉主库进程,观察切换时间和业务表现。演练过的团队和没演练过的团队,真出事时的反应完全是两个物种。
第六,别把备份交给 MGR。MGR 保证的是节点间的数据一致和高可用,它不是备份方案。误删表这种逻辑错误,MGR 会忠实地把删除操作同步到所有节点。物理备份和逻辑备份该做还得做,而且要定期验证可恢复性。
第七,注意版本一致性。三台机器的 MySQL 版本必须完全一致,包括小版本号。滚动升级的时候要先升级从库,最后升级主库,每升级一个节点都要确认它成功归队并且状态是 ONLINE 再动下一个。
这套东西配下来,一个能用的单主高可用集群就跑起来了。剩下的事情就是慢慢观察、慢慢调,把参数调到跟你的业务特征匹配。每个团队的业务形态都不一样,别人那套跑得很稳的配置搬过来可能水土不服,还是得自己在真实负载下跑一段时间才能定型。