1. 先想清楚:MGR 和 KeepAlived 在这套方案里到底各自扮演什么角色
1.1 MySQL 8.0 MGR 单主模式解决了什么问题
MySQL 8.0 的 Group Replication,简称 MGR,本质上是一套基于组通信协议的高可用复制方案。它和传统异步主从复制最大的区别在于:成员之间通过 Paxos 类共识协议交换消息,组内所有节点对“谁还能写入”“谁已经故障”能达成一致,而不是靠半同步、MHA 这类外部脚本去猜。
生产上用 MGR 通常都是用单主模式。在这个模式下,同一时间只有一个节点是 PRIMARY,可以接收写事务,其他节点都是 SECONDARY,只同步和提供只读能力。PRIMARY 节点发生故障时,剩余成员会协商出一个新 PRIMARY,不需要人工介入。
MGR 对“丢数据”这件事的控制比传统异步复制严密得多。传统主从里,主库 binlog 刷盘了,从库可能还没拿到,一旦主库断电,切到从库就会丢这一段;MGR 则会要求事务在组内多数成员上确认后才提交,多数派活着数据就在。用一句大白话说,主从复制像是把账本交给徒弟抄写,徒弟抄到哪页算哪页;MGR 更像几个记账员同时对账,至少一半以上的人确认这笔账没问题,才正式落账。
这套方案适合对数据一致性要求较高、又不希望引入太重的外部协调组件的场景。很多公司数据库规模并不大,三台物理机或虚拟机就能组成一套高可用集群,应用连接一个虚拟 IP,剩下的故障切换逻辑交给集群和 VIP 脚本处理。
1.2 为什么还要在 MGR 上面加 KeepAlived
MGR 解决了数据库内部的主节点选举和数据一致问题,但它不给客户端提供“入口漂移”能力。应用层不可能每次写 SQL 前先查一下当前哪个节点是 PRIMARY,总要有一个固定的访问地址。这个地址就是 VIP,虚拟 IP。
KeepAlived 在这套方案里承担的就是 VIP 漂移工作。它用 VRRP 协议在多个节点之间维持一个虚拟 IP,正常情况下 VIP 落在 PRIMARY 节点上,节点故障或角色变化后,VIP 自动挪到新的 PRIMARY 节点。应用层看到的是同一个 IP,不需要改连接串,数据库内部的主从切换对业务透明。
很多人会问,既然 MySQL 官方有 InnoDB Cluster + MySQL Router,为什么还要自己拼 MGR + KeepAlived?我的看法是,InnoDB Cluster 适合愿意接受全套官方生态、统一管理工具链的环境;但不少线上环境已经有自己的监控、备份、发布体系,只想把数据库复制层和高可用入口层拆开管理。KeepAlived 轻量、配置直接、故障点少,和 MGR 组合起来足够应付大部分单主读写场景。
另外一个常见困惑是“Haproxy 和 KeepAlived 到底用哪个”。KeepAlived 主要管 VIP 漂移,本身不处理七层转发;Haproxy 主要做 TCP/HTTP 负载均衡。如果你的业务需要多个只读节点分担读流量、需要按端口转发,可以在 MGR 前面放 Haproxy,再用 KeepAlived 给 Haproxy 机器做高可用。但如果你只需要一个写入口加几个只读备份节点,直接把 VIP 绑在 MGR PRIMARY 节点上更简单,少一层代理就少一层故障面。
1.3 方案边界和使用前提
MGR 并不是万能的。它对网络质量要求比较高,节点之间一般建议同机房或低延迟专线,跨城域网的组复制会频繁出现延迟和成员踢出问题。同时,单主模式只适合写多读少的业务,如果读压力很大,要么扩展 MGR 成员数量,要么在 MGR 上层再做读写分离。
还有一个常见误区是把 MGR 当成“自动故障转移全家桶”。MGR 只保证数据库组件内达成共识,对外暴露持续可用的连接入口是 KeepAlived 要做的事;监控告警、应用重试、数据备份这些还得自己搭。本文后面的配置也全部围绕“生产可用”这四个字展开,不是只搭个实验环境跑通就结束。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的节点规划与 MySQL 8.0 安装
2.1 主机规划、端口规划与版本选择
一套三节点 MGR 集群至少需要三台主机,原因是组复制要避免“双脑”,多数派协议决定了集群必须能容忍一台机器宕机,至少三台才能形成多数派。两台节点的 MGR 没有意义,一台挂掉就达不到多数派,整个组会阻塞或解体。
下面是我常用的规划方式。假设三台机器 IP 分别为 192.168.56.101、192.168.56.102、192.168.56.103,MySQL 实例端口统一用 3306,组复制内部通信端口用 33061。注意 33061 这个端口很多第一次搭 MGR 的人会忘记放防火墙,后面节点加入时永远卡在 RECOVERING,问题排查非常痛苦。
| 节点 | IP | 操作系统 | MySQL 版本 | 组复制本地通信端口 |
|---|---|---|---|---|
| mgr-node1 | 192.168.56.101 | CentOS 7.9 / 8.x | 8.0.32 | 33061 |
| mgr-node2 | 192.168.56.102 | CentOS 7.9 / 8.x | 8.0.32 | 33061 |
| mgr-node3 | 192.168.56.103 | CentOS 7.9 / 8.x | 8.0.32 | 33061 |
MySQL 版本建议选 8.0.27 以上的稳定版。8.0.27 之后 Group Replication 的很多消息通信机制有优化,8.0.18 之前的老版本对 IPv6、部分系统配置兼容性差。这里用 8.0.32 举例,生产上选你验证过的同一个大版本即可,三台节点小版本尽量保持一致,避免出现协议兼容问题。
2.2 MySQL 8.0 二进制安装与基础初始化
生产环境我个人更推荐用官方 tar 包或 RPM 安装,尽量不用 Docker 跑数据库。Docker 本身不是不能用,但 MGR 对网络和端口非常敏感,容器网络的 NAT 会把 report_host、group_replication_local_address 这些地址搞得很复杂,排查成本远高于直接装在三台独立主机上。
以 tar 包方式为例,三台机器操作一致:
bash复制groupadd mysql
useradd -r -g mysql -s /bin/false mysql
cd /usr/local
tar xf mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz
mv mysql-8.0.32-linux-glibc2.12-x86_64 mysql
mkdir -p /data/mysql
chown -R mysql:mysql /data/mysql /usr/local/mysql
初始化数据目录时,用 --initialize 会生成一个临时 root 密码,写到日志文件里;如果你不想记临时密码,也可以先用 --initialize-insecure,初始化完成后直接空密码登录,再立刻修改 root 密码。两种方式都行,关键是初始化后必须马上加固账号,不能让空密码或弱密码暴露在生产网段。
bash复制/usr/local/mysql/bin/mysqld --initialize --user=mysql \
--basedir=/usr/local/mysql --datadir=/data/mysql \
--log-error=/data/mysql/error.log
grep "temporary password" /data/mysql/error.log
接下来把 MySQL 配置写入 /etc/my.cnf,启动服务。三台机器的配置文件大部分相同,只有几个跟节点身份有关的参数不同。关于这些关键参数,下一节单独拆开讲。
2.3 为 MGR 准备的公共参数和节点差异参数
MGR 对基础配置有硬性要求:必须开启 GTID、必须使用 ROW 格式的 binlog、必须把 binlog_checksum 设为 NONE,因为组复制成员之间做二进制日志传输时自己会做校验,不会依赖 binlog 自带的 checksum。下面是一份可以直接用的参考配置:
ini复制[mysqld]
server_id = 101
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_checksum = NONE
log_bin = mysql-bin
binlog_format = ROW
log_slave_updates = ON
master_info_repository = TABLE
relay_log_info_repository = TABLE
transaction_write_set_extraction = XXHASH64
report_host = 192.168.56.101
default_authentication_plugin = mysql_native_password
loose-group_replication_group_name = "eeeeeeee-aaaa-1111-2222-333333333333"
loose-group_replication_start_on_boot = OFF
loose-group_replication_local_address = "192.168.56.101:33061"
loose-group_replication_group_seeds = "192.168.56.101:33061,192.168.56.102:33061,192.168.56.103:33061"
loose-group_replication_single_primary_mode = ON
loose-group_replication_enforce_update_everywhere_checks = OFF
每个节点需要改的地方只有三个:server_id、report_host、group_replication_local_address。report_host 一定要填能真正互通的外网地址,如果漏掉这项,组复制成员在展示节点地址时可能会拿主机名去解析,导致其他节点连不上。group_replication_group_seeds 三台机器可以配置一致,因为它只是给新成员一个种子列表,不代表所有 IP 必须是本地地址。
group_replication_start_on_boot 我建议一律设为 OFF,不要让 MySQL 进程一启动就自动加入组。生产运维中经常需要先拉起实例做检查,确认数据、网络都正常后再手动启动组复制,自动加入虽然省事,但会在误启动或数据不一致时把问题放大。
这里多说一句关于 default_authentication_plugin 的坑。MySQL 8.0 默认认证插件是 caching_sha2_password,组复制分布式恢复的 channel 在传输密码时对加密通道要求比较挑剔。如果你不想在处理 SSL 公钥问题上折腾,可以在早期版本里先用 mysql_native_password,等整套链路跑顺后再评估升级。MySQL 8.0.34 之后 mysql_native_password 默认被禁用,这种情况建议直接用 caching_sha2_password,同时给复制账号配置 SSL。
3. MGR 集群从零搭建实录
3.1 在第一个节点初始化组
三台机器的 MySQL 都启动成功后,先把第一台节点 192.168.56.101 作为引导节点。引导节点做的事情是“创建出一个全新的复制组”,所以必须手动开启 group_replication_bootstrap_group,只有它启动成功并成为 PRIMARY 后,另外两台才能加入进来。
登录第一台节点,执行:
sql复制-- 给复制通道创建专用账号
SET SQL_LOG_BIN=0;
CREATE USER 'repl_user'@'%' IDENTIFIED WITH mysql_native_password BY 'REPL_Password#2024';
GRANT REPLICATION SLAVE, BACKUP_ADMIN, GROUP_REPLICATION_STREAM ON *.* TO 'repl_user'@'%';
GRANT SELECT ON performance_schema.* TO 'repl_user'@'%';
SET SQL_LOG_BIN=1;
创建账号时要包在 SET SQL_LOG_BIN=0 里面,避免这个账号被当成普通事务写入 binlog,再同步到其他节点。REPLICATION SLAVE 是分布式恢复必需权限,BACKUP_ADMIN 是给 Clone 插件准备的最低权限,GROUP_REPLICATION_STREAM 是组复制 8.0.x 新增的流控制相关权限,建议一起授权。
然后安装组复制插件并配置恢复通道:
sql复制INSTALL PLUGIN group_replication SONAME 'group_replication.so';
CHANGE MASTER TO
MASTER_USER='repl_user',
MASTER_PASSWORD='REPL_Password#2024',
MASTER_AUTO_POSITION=1
FOR CHANNEL 'group_replication_recovery';
你可能会有疑问,CHANGE MASTER TO 不是传统复制才用的吗?其实 MGR 新成员加入时也需要走一个“分布式恢复”流程,它会启动一个名为 group_replication_recovery 的内部复制通道,从现有成员拉取数据。这条命令就是提前把这个通道的账号信息写好。
接着执行引导启动:
sql复制SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
启动完成后检查成员状态:
sql复制SELECT member_id, member_host, member_state, member_role
FROM performance_schema.replication_group_members;
理想状态下第一台节点应该显示 ONLINE,角色是 PRIMARY。bootstrap_group 这个开关必须用完立刻关掉,否则万一节点重启,MySQL 又把它当成新组引导,会产生双组脑裂。
3.2 第二、三节点如何加入组
第二台、第三台节点加入时需要做的账号初始化和第一台完全一样,但不要再执行 group_replication_bootstrap_group=ON,直接 START GROUP_REPLICATION 即可。流程如下:
sql复制SET SQL_LOG_BIN=0;
CREATE USER 'repl_user'@'%' IDENTIFIED WITH mysql_native_password BY 'REPL_Password#2024';
GRANT REPLICATION SLAVE, BACKUP_ADMIN, GROUP_REPLICATION_STREAM ON *.* TO 'repl_user'@'%';
GRANT SELECT ON performance_schema.* TO 'repl_user'@'%';
SET SQL_LOG_BIN=1;
INSTALL PLUGIN group_replication SONAME 'group_replication.so';
CHANGE MASTER TO
MASTER_USER='repl_user',
MASTER_PASSWORD='REPL_Password#2024',
MASTER_AUTO_POSITION=1
FOR CHANNEL 'group_replication_recovery';
START GROUP_REPLICATION;
第二台启动后,第一台上再次查看组成员,正常情况下会看到两行记录,其中一个可能是 RECOVERING,几秒到几十秒后变为 ONLINE。如果一直卡在 RECOVERING,优先检查 33061 端口是否互通、账号权限是否正确、错误日志里有没有分布式恢复失败记录。
第三台也是同样的流程。全部加入后,任意节点执行下面这条 SQL 都能看到完整的三个成员信息:
sql复制SELECT member_id, member_host, member_port, member_state, member_role
FROM performance_schema.replication_group_members\G
3.3 验证数据复制和只读限制
集群基线建立后,第一步验证最好在 PRIMARY 上建库建表,写一条测试数据,再到两个 SECONDARY 上查。我记得第一次搭 MGR 时,只看到成员状态 ONLINE 就觉得完事大吉,结果没有实际写数据验证复制链路,后来才发现复制通道因为密码插件问题根本没真正工作。
在 PRIMARY 节点执行:
sql复制CREATE DATABASE appdb;
CREATE TABLE appdb.test_tb (id int primary key, note varchar(50));
INSERT INTO appdb.test_tb VALUES (1, 'hello mgr');
在 SECONDARY 节点查询:
sql复制SELECT * FROM appdb.test_tb;
能查到这条数据说明复制链路正常。另外要重点确认单主模式下 SECONDARY 节点是否真的只读,手工在 SECONDARY 执行一条 INSERT:
sql复制INSERT INTO appdb.test_tb VALUES (2, 'should fail');
如果配置正确,会收到 The MySQL server is running with the --super-read-only option 或类似错误,这正说明单主模式对写入入口做了隔离。如果这个语句意外成功了,很可能是多主模式,需要回头检查 group_replication_single_primary_mode 是否真的生效。
4. KeepAlived 接入:把 VIP 稳定绑定到真正的 PRIMARY
4.1 检测脚本不能只检查 MySQL 进程活着
很多从传统主从复制转过来的运维,写 KeepAlived 检测脚本时只做一件事:mysqladmin ping,端口通就认为数据库正常,于是 KeepAlived 一直持有 VIP。这个思路在 MGR 架构下非常危险。
MGR 场景里有一种情况:原 PRIMARY 节点网络抖动,被组内其他成员判定为故障并踢出,其他节点选出了新 PRIMARY。但原 PRIMARY 的 mysqld 进程可能还活着,mysqladmin ping 照样能通。如果 KeepAlived 只看进程状态,VIP 就不会释放,客户端写入继续打到旧 PRIMARY,但旧 PRIMARY 已经不是组成员,写入可能直接报错,或者更糟糕的是网络恢复后旧节点重新加入,产生一段时间的数据状态混乱。
所以检测脚本必须确认两件事:第一,本机 MySQL 能正常响应;第二,本机在 MGR 中的角色是 ONLINE/PRIMARY。只有同时满足,KeepAlived 才应该持有 VIP。
我推荐先给 root 账号配置 mysql_config_editor 登录路径,避免把密码明文写在 shell 脚本里,也可以避免密码出现在进程列表中:
bash复制mysql_config_editor set --login-path=mgr_check \
--host=localhost --user=root --password
执行后会交互式提示输入密码,生成加密配置文件 ~/.mylogin.cnf。注意运行 KeepAlived 的用户通常是 root,如果你用 systemd 管理,需要确认这个登录路径对 KeepAlived 进程可读。
检测脚本内容如下:
bash复制#!/bin/bash
export PATH=/usr/local/mysql/bin:/usr/sbin:/usr/bin:/sbin:/bin
MEMBER_INFO=$(mysql --login-path=mgr_check -N -B -e \
"SELECT CONCAT(member_state, '/', member_role)
FROM performance_schema.replication_group_members
WHERE member_id = @@server_uuid;" 2>/dev/null)
if [ "$MEMBER_INFO" = "ONLINE/PRIMARY" ]; then
exit 0
fi
exit 1
把脚本放在每个节点的 /etc/keepalived/chk_mgr_primary.sh,记得加执行权限:
bash复制chmod +x /etc/keepalived/chk_mgr_primary.sh
脚本返回 0 表示本节点健康且有 PRIMARY 角色;返回 1 表示不满足条件。KeepAlived 会通过脚本返回码动态调整优先级,进而决定谁持有 VIP。
4.2 keepalived.conf 完整配置与优先级设计
三台节点都要安装 KeepAlived:
bash复制yum install -y keepalived
配置文件核心部分如下。以 192.168.56.101 为例,另外两台只需要改 interface 对应的网卡名和本机实际 IP 相关字段,文件名都是 /etc/keepalived/keepalived.conf:
ini复制global_defs {
router_id MGR_KEEPALIVED
}
vrrp_script chk_mgr_primary {
script "/etc/keepalived/chk_mgr_primary.sh"
interval 2
timeout 2
fall 2
rise 2
weight 50
}
vrrp_instance VI_MGR {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 87654321
}
track_script {
chk_mgr_primary
}
virtual_ipaddress {
192.168.56.100/24 dev eth0 label eth0:1
}
}
为什么三台节点的 state 都用 BACKUP,而不是传统方案里一台 MASTER、两台 BACKUP?因为 MGR 的 PRIMARY 是动态变化的,跟随组内选举结果走。如果固定一台为 MASTER,哪怕它已经不是 MGR 的 PRIMARY,也可能因为优先级机制重新抢回 VIP。
优先级设计是这套配置的核心。基础优先级默认都是 100,检测脚本成功时加 weight 50,变成 150;检测脚本失败时减 50,变成 50。假设当前 PRIMARY 是 101 节点,它的脚本成功,优先级 150;102、103 节点此时只是 SECONDARY,脚本失败,优先级 50。如果 101 节点 MySQL 故障,脚本开始失败,101 优先级跌到 50;而 102 节点在组内被选举为新 PRIMARY 后,脚本开始成功,优先级升到 150。这样 KeepAlived 一定会把 VIP 从旧主漂移到新主。
有一点必须提醒:auth_pass 在 KeepAlived 旧版本里最长 8 位,不同节点必须完全一致,否则 VRRP 报文认证失败,VIP 永远不会漂移。virtual_router_id 在同一网段内也尽量不要和其他 KeepAlived 集群重复,否则两个集群会互相干扰。
4.3 VIP 漂移验证全过程
配置完成后,先启动或重启三个节点的 KeepAlived:
bash复制systemctl restart keepalived
正常情况下只有当前 MGR PRIMARY 节点上能看到 VIP:
bash复制ip addr show eth0 | grep 192.168.56.100
接着做一次故障模拟。直接把 PRIMARY 节点的 mysqld 停掉,或者更粗暴地执行 systemctl stop mysqld,观察 MGR 是否自动选出新 PRIMARY,以及 VIP 是否跟着漂移。
停止主库后,立刻在第二台节点循环查看:
bash复制watch -n 1 "ip addr show eth0 | grep 192.168.56.100"
同时监控组内角色变化:
sql复制SELECT member_host, member_state, member_role
FROM performance_schema.replication_group_members;
实际切换过程中,VIP 并不会和 MGR 选举完全同步。MGR 选主通常一两秒内完成,但 KeepAlived 的检测脚本最短也要 2 秒执行一次,还要加上 fall 2 的连续失败次数判断,所以整个对外不可用时间可能在 5 到 10 秒之间。这个时间窗口应用层需要配置连接重试,否则一条写 SQL 刚好打在切换点上就会报连接中断。
恢复故障节点时不要急着在旧 PRIMARY 上启动 mysqld 后立刻让它抢 VIP。等旧节点重新启动组复制、变成 SECONDARY 并追平数据后,KeepAlived 检测脚本会保持在失败状态,VIP 依然留在新 PRIMARY 上。这个状态是正常的,千万不要手动去旧节点重启 KeepAlived 抢 VIP。
5. 生产环境最容易踩的坑与排查技巧
5.1 MGR 节点加入和状态异常的排查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 新节点一直 RECOVERING | 33061 端口被防火墙拦截,或者 recovery channel 账号密码错误 | 先看错误日志,放行 33061 端口;确认 repl_user 密码和权限;执行 STOP GROUP_REPLICATION 后再 START |
| 节点加入后马上被踢出 | 本机 server_uuid 冲突或数据不一致 | 检查是否是从克隆虚拟机直接复制出来的实例;必要时重新初始化数据目录再重新加入 |
| 主库写入报 super-read-only 错误 | 客户端实际连接到了 SECONDARY | 检查 VIP 是否漂错节点;手工验证 keepalived 检测脚本的返回码 |
| 重启 MySQL 后不会自动回到组内 | group_replication_start_on_boot=OFF | 属预期行为,手动执行 START GROUP_REPLICATION |
| 三个节点网络抖动后成员反复消失 | 组复制对网络延迟敏感 | 检查交换机是否开启了节能省电或组播限制;考虑增加 group_replication_communication_timeout 参数 |
关于新节点一直 RECOVERING,这是 MGR 搭建里出现频率最高的问题。新成员加入时,如果自身数据不是最新的,会通过分布式恢复从其他在线成员拉取增量,甚至是全量数据。很多第一次搭建的人只放行了 3306 端口,没放 33061,结果新节点连不上其他成员的内部通信端口,恢复流程就一直挂在 RECOVERING。
排查 RECOVERING 的固定路径是:
bash复制tail -200 /data/mysql/error.log
日志里如果有类似 Connection refused 或者 timed out 的组通信报错,基本就是网络层问题;如果是认证失败,会直接看到 Access denied for user 相关的 recovery channel 错误。
5.2 KeepAlived 脑裂和 VIP 不漂的排查
KeepAlived 和 MGR 组合起来,最怕出现“VIP 在某台机器上,业务写流量也进了这台机器,但这台机器不是 MGR PRIMARY”的情况。这种情况通常不是 MGR 的问题,而是 KeepAlived 脚本没判断角色,或者判断逻辑没生效。
遇到 VIP 异常时,我一般按下面顺序排查:
第一步,在所有节点上手动执行检测脚本,看退出码:
bash复制/etc/keepalived/chk_mgr_primary.sh
echo $?
返回 1 说明脚本认为当前节点没有资格持有 VIP。如果返回 1 的节点上居然还有 VIP,说明 KeepAlived 没有把脚本失败转换成优先级下降,要检查配置文件里 track_script 是否真的写进了 vrrp_instance。
第二步,看 KeepAlived 的日志:
bash复制journalctl -u keepalived -f
tail -100 /var/log/messages
日志里能看到 VRRP_Script(chk_mgr_primary) succeeded 或 failed 的记录,也能看到状态切换为 MASTER 或 BACKUP 的过程。如果日志显示检测脚本一直 failed,但节点没有从 MASTER 降级,检查
