作者亲测 | 适用:想在生产或测试环境快速落地 MySQL 高可用的同学
环境:Rocky Linux 8.10 × 3 + MySQL 8.4.10(Server / Shell / Router)
全部命令与输出来自真实机器,可以直接照抄执行。
一、为什么要写这篇文章
MySQL 官方的高可用方案演进到现在,"InnoDB Cluster"已经是中小团队最省心的一条路:
- MySQL Router 负责把读写流量自动分流到主/从,主挂了自动切换,应用无感知;
- MySQL Shell(AdminAPI) 用几条命令就能创建、扩容、缩容集群;
- 底层 Group Replication(组复制) 保证三节点数据强一致。
但"省心"的前提是版本要对。网上大量教程基于 5.7 / 8.0,直接套用到 8.4 上会踩到不少隐蔽的坑(比如组复制通信端口到底是 3306 还是 33061)。这篇文章是我在 3 台 Rocky Linux 真机上从零到故障演练全流程跑通后的记录,每一个数字、每一段报错都来自真实执行结果。
如果你手头刚好有 3 台机器(虚拟机也行),跟着本文走一遍,大约 2~4 小时就能得到一个可用的高可用集群。
二、架构与硬件规划
2.1 最终要达成的效果
┌────────────────────────────┐
应用 / 运维机 │ MySQL Router (8.4.10) │
──────────────►│ 6446 读写 / 6447 只读 │
│ 6450 读写分离 │
└───────────┬────────────────┘
│ metadata_cache (ttl=0.5s)
┌───────────▼────────────────┐
│ InnoDB Cluster │
│ (Group Replication, 单主) │
└────────────────────────────┘
┌──────────┬──────────┬──────────┐
▼ ▼ ▼
db-node1 db-node2 db-node3
PRIMARY SECONDARY SECONDARY
R/W R/O R/O
2.2 主机规划
| 主机名 | IP | server_id | 角色 | memberWeight | 部署组件 |
|---|---|---|---|---|---|
| db-node1 | 192.168.40.152 | 152 | 种子主 / 管理机 | 50 | Server + Shell + Router |
| db-node2 | 192.168.40.153 | 153 | 从节点 | 30 | Server |
| db-node3 | 192.168.40.154 | 154 | 从节点 | 20 | Server |
机器配置(实测):4 核 / 7.5G 内存 / 60G 磁盘、Rocky Linux 8.10、glibc 2.28、firewalld 开启(放行端口而非关闭)、chronyd 已启用。
2.3 版本与端口(重要)
| 端口 | 用途 | 实测说明 |
|---|---|---|
| 3306 | MySQL 服务端 + 8.4 组复制内部通信 | 一定要放行 |
| 33060 | X Plugin | 放行 |
| 33061 | 老版本 XCom 通信端口 | 8.4 MYSQL 通信栈下不监听,不用放行也行 |
| 6446 / 6447 / 6450 | Router 读写 / 只读 / 读写分离 | Router 所在机放行 |
| 6448 / 6449 | Router X 协议读写 / 只读 | 同上 |
坑 1(最隐蔽):放行
33061并把它写进group_replication_group_seeds。在 MySQL 8.4 默认communication_stack=MYSQL下,组复制成员间通信走的就是 3306。如果你手写 my.cnf 时写错成 33061,集群创建当时不会报错,但节点一重启就再也加不回来,报错形如:[ERROR] [MY-011735] [GCS] Error on opening a connection to peer node 192.168.40.153:33061 when joining a group. My local port is: 3306.
三、第一步:环境准备(3 台都做)
3.1 设置主机名与 hosts
# 152 / 153 / 154 分别执行
hostnamectl set-hostname db-node1 # db-node2 / db-node3
# 三台都执行
cat >> /etc/hosts <<'EOF'
192.168.40.152 db-node1
192.168.40.153 db-node2
192.168.40.154 db-node3
EOF
3.2 创建用户与目录
groupadd mysql 2>/dev/null || true
useradd -r -g mysql -s /bin/false mysql 2>/dev/null || true
mkdir -p /data/mysql
chown -R mysql:mysql /data/mysql
cat > /etc/profile.d/mysql.sh <<'EOF'
export PATH=/usr/local/mysql/bin:/usr/local/mysql-shell/bin:/usr/local/mysql-router/bin:$PATH
EOF
source /etc/profile.d/mysql.sh
3.3 放行防火墙端口
for p in 3306 6446 6447 6448 6449 6450; do
firewall-cmd --permanent --add-port=${p}/tcp
done
firewall-cmd --reload
firewall-cmd --list-ports
也可以连
33061 33060一起放行,图个以后切换通信栈省事。
3.4 基线自查
cat /etc/os-release | grep PRETTY_NAME
ldd --version | head -1 # 需 >= 2.28
systemctl is-active chronyd # 组复制对时钟敏感,必须 active
四、第二步:安装 MySQL Server 8.4(3 台都做)
4.1 下载解压(约 794MB)
cd /tmp
PKG=mysql-8.4.10-linux-glibc2.28-x86_64.tar.xz
curl -fL --retry 3 -C - -o "$PKG" \
"https://cdn.mysql.com/Downloads/MySQL-8.4/${PKG}"
cd /usr/local
tar xf /tmp/$PKG
mv mysql-8.4.10-linux-glibc2.28-x86_64 mysql
/usr/local/mysql/bin/mysqld --version
# 输出:Ver 8.4.10 for Linux on x86_64 (MySQL Community Server - GPL)
4.2 写 my.cnf(每台只有 IP / server_id 不同)
以 152 为例(153/154 替换 server_id、report_host、local_address 中的 IP):
[mysqld]
server_id = 152
port = 3306
basedir = /usr/local/mysql
datadir = /data/mysql
socket = /tmp/mysql.sock
pid-file = /data/mysql/mysql.pid
log-error = /data/mysql/error.log
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
log-bin = mysql-bin
binlog_format = ROW
binlog-checksum = NONE
binlog_expire_logs_seconds = 604800
gtid_mode = ON
enforce_gtid_consistency = ON
log_replica_updates = ON
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.40.152:3306"
group_replication_group_seeds = "192.168.40.152:3306,192.168.40.153:3306,192.168.40.154:3306"
group_replication_bootstrap_group = OFF
report_host = "192.168.40.152"
disabled_storage_engines = MyISAM,BLACKHOLE
performance_schema = ON
max_connections = 500
注意:别写
[client]段(如default-character-set),否则后面 mysqlsh 会报unknown option。
group_replication_group_name先用占位值即可,AdminAPI 建集群时会用mysqld-auto.cnf覆盖成真实 UUID。
4.3 初始化:这里有个 8.4 的隐藏大坑
cat > /tmp/my-init.cnf <<'EOF'
[mysqld]
server_id = 152
port = 3306
basedir = /usr/local/mysql
datadir = /data/mysql
socket = /tmp/mysql.sock
pid-file = /data/mysql/mysql.pid
log-error = /data/mysql/error.log
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
log-bin = mysql-bin
binlog_format = ROW
binlog-checksum = NONE
binlog_expire_logs_seconds = 604800
gtid_mode = ON
enforce_gtid_consistency = ON
log_replica_updates = ON
disabled_storage_engines = MyISAM,BLACKHOLE
performance_schema = ON
EOF
/usr/local/mysql/bin/mysqld --defaults-file=/tmp/my-init.cnf --initialize --user=mysql
echo "init_rc=$?" # 期望 0
grep 'temporary password' /data/mysql/error.log
坑 2:直接拿上面带
group_replication_*参数的完整 my.cnf 去--initialize,会报
[ERROR] [MY-000067] unknown variable 'group_replication_group_name'。
原因是 8.4 在初始化阶段会忽略plugin_load_add,于是组复制参数全部变成"未知变量"。
解法:初始化用"最小配置",正常启动再用完整配置(分两步)。
4.4 注册 systemd 并启动
cat > /etc/systemd/system/mysql.service <<'EOF'
[Unit]
Description=MySQL Server 8.4
After=network.target
[Service]
Type=simple
User=mysql
Group=mysql
ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf
Restart=on-failure
RestartSec=5
TimeoutSec=120
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable mysql
systemctl start mysql
systemctl is-active mysql # active
4.5 初始化 root 账号
# ① 用临时密码登录并改密码
TPW=$(awk -F': ' '/temporary password/{print $NF}' /data/mysql/error.log)
MYSQL_PWD="$TPW" /usr/local/mysql/bin/mysql -uroot --connect-expired-password \
--socket=/tmp/mysql.sock -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Root@123456';"
# ② 建远程 root(供 AdminAPI / Router / 应用连接)
MYSQL_PWD='Root@123456' /usr/local/mysql/bin/mysql -uroot --socket=/tmp/mysql.sock -e "
CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'Root@123456';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;"
# ③ 验证
MYSQL_PWD='Root@123456' /usr/local/mysql/bin/mysql -h127.0.0.1 -P3306 -uroot -N \
-e "SELECT VERSION(), @@hostname, @@port, @@server_id;"
三台都要装完、启动、改好 root 密码,再进入下一步。
五、第三步:创建 InnoDB Cluster(只需在 152 上操作)
5.1 装 MySQL Shell
cd /tmp
curl -fL -o mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz \
"https://cdn.mysql.com/Downloads/MySQL-Shell/mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz"
cd /usr/local
tar xf /tmp/mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz
mv mysql-shell-8.4.10-linux-glibc2.28-x86-64bit mysql-shell
/usr/local/mysql-shell/bin/mysqlsh --version
5.2 检查三台实例配置
连接串里密码的 @ 记得转义成 %40:
SH=/usr/local/mysql-shell/bin/mysqlsh
for IP in 152 153 154; do
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.${IP}:3306');
dba.checkInstanceConfiguration('root:Root%40123456@192.168.40.${IP}:3306');"
done
# 每台期望输出:All checks passed.
5.3 交给 AdminAPI 自动配置(三台逐个执行)
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.152:3306');
dba.configureInstance();"
# 依次把 IP 换成 .153 / .154 再执行两次
# 期望:The instance '...' was configured to use InnoDB Cluster.
AdminAPI 会把 GR 参数用
SET PERSIST_ONLY写进各实例/data/mysql/mysqld-auto.cnf,
它的优先级高于 /etc/my.cnf。配置完可以cat /data/mysql/mysqld-auto.cnf检查。
5.4 createCluster 创建集群
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.152:3306');
var c = dba.createCluster('myCluster',
{consistency: 'BEFORE_ON_PRIMARY_FAILOVER', memberWeight: 50});
c.status();"
成功输出关键片段(实测):
A new InnoDB cluster will be created on instance '192.168.40.152:3306'.
Cluster successfully created. ...
NOTE: Group Replication will communicate with other members using '192.168.40.152:3306'.
...
"primary": "192.168.40.152:3306",
"status": "OK"
5.5 addInstance 添加两个从节点
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.152:3306');
var c = dba.getCluster('myCluster');
c.addInstance('root:Root%40123456@192.168.40.153:3306',
{memberWeight: 30, recoveryMethod: 'clone'});
c.addInstance('root:Root%40123456@192.168.40.154:3306',
{memberWeight: 20, recoveryMethod: 'clone'});"
坑 3:
recoveryMethod必须显式指定为'clone'。若不指定,当目标实例存在历史 GTID 时,mysqlsh 会停下等你交互选择,命令行脚本会直接超时。坑 4:clone 完成后 AdminAPI 会尝试自动重启目标实例,但我们的 systemd 单元是
Type=simple,不支持 MySQL 的RESTART命令,于是会看到:MYSQLSH 51156 ... waiting for the server to restart不要慌,两步修复:
# 在 153 / 154 上手动拉起 mysqld systemctl start mysql # 回 152 上 rescan,把两个实例补进元数据 $SH --js -e " shell.connect('root:Root%40123456@192.168.40.152:3306'); var c = dba.getCluster('myCluster'); c.rescan({addInstances: ['192.168.40.153:3306','192.168.40.154:3306']});"
5.6 查看集群状态
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.152:3306');
print(dba.getCluster().status());"
"clusterName": "myCluster",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"192.168.40.152:3306": {"memberRole": "PRIMARY", "memberState": "ONLINE"},
"192.168.40.153:3306": {"memberRole": "SECONDARY", "memberState": "ONLINE"},
"192.168.40.154:3306": {"memberRole": "SECONDARY", "memberState": "ONLINE"}
}
六、第四步:部署 MySQL Router(152 上执行)
6.1 下载解压
cd /tmp
curl -fL -o mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz \
"https://cdn.mysql.com/Downloads/MySQL-Router/mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz"
cd /usr/local
tar xf /tmp/mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz
mv mysql-router-8.4.10-linux-glibc2.28-x86_64 mysql-router
/usr/local/mysql-router/bin/mysqlrouter --version
6.2 bootstrap 引导
/usr/local/mysql-router/bin/mysqlrouter \ --bootstrap root:Root%40123456@192.168.40.152:3306 \ --directory /usr/local/mysqlrouter-data --user=mysql
实测输出关键片段:
MySQL Router configured for the InnoDB Cluster 'myCluster'
## MySQL Classic protocol
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
- Read/Write Split Connections: localhost:6450
## MySQL X protocol
- Read/Write Connections: localhost:6448
- Read/Only Connections: localhost:6449
坑 5:很多老教程说端口是 64460 / 64470,8.4.10 实测就是 6446/6447/6450/6448/6449,别找错了。
6.3 启动 Router
mkdir -p /usr/local/mysqlrouter-data
chown -R mysql:mysql /usr/local/mysqlrouter-data
setsid /usr/local/mysql-router/bin/mysqlrouter \
-c /usr/local/mysqlrouter-data/mysqlrouter.conf \
--pid-file=/usr/local/mysqlrouter-data/mysqlrouter.pid >/dev/null 2>&1 &
sleep 3
ss -tlnp | grep -E '6446|6447|6450'
七、第五步:功能验证
7.1 读写 / 只读路由(实测输出)
# 只读 6447:在 db-node2 / db-node3 之间轮询,且都是 rw=1
mysql -h127.0.0.1 -P6447 -uroot -N -e "SELECT @@hostname, @@read_only;"
# 6447 db-node2 1
# 6447 db-node3 1 (轮询)
# 读写 6446:稳定指向主库 db-node1,rw=0
mysql -h127.0.0.1 -P6446 -uroot -N -e "SELECT @@hostname, @@read_only;"
# 6446 db-node1 0
# 读写分离 6450:轮询到从库
mysql -h127.0.0.1 -P6450 -uroot -N -e "SELECT @@hostname, @@read_only;"
7.2 数据同步验证
-- 经 6446(写端口)建表插数
CREATE DATABASE testdb;
CREATE TABLE testdb.t1(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50),
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
INSERT INTO testdb.t1(name) VALUES ('A-152'),('B-152'),('C-152');
实测:三台节点 SELECT COUNT(*) 都是 3,内容完全一致,说明同步正常。
7.3 限制性用例(了解边界)
| 操作 | 实测结果 |
|---|---|
| 直连从节点写入 | ERROR 1290 ... --super-read-only option(被拒,符合预期) |
| 建无主键表 | 可以建,但写入报 ERROR 3098 ... not comply with the requirements |
| 建 MyISAM 表 | ERROR 3161 ... Storage engine MyISAM is disabled(被禁用) |
八、第六步:高可用演练(这才是重点)
我把能想到的故障场景都真打了一遍。环境约定:systemd 配置了 Restart=on-failure,这会影响后面很多结论。
8.1 主库优雅下线(mysqladmin shutdown)
- 切换耗时:约 10 秒完成选主。
- 输出关键日志(从库视角):
A new primary with address 192.168.40.153:3306 was elected. - Router 6446 在十几秒内自动指向新主;旧主重启后作为 SECONDARY 自动加回。
- 数据:全程一致,零丢失。
8.2 主库 kill -9(模拟崩溃)
- 因为 systemd 是
Restart=on-failure,mysqld 被强杀后 2 秒内被自动拉起,并因start_on_boot=ON自动尝试回组。 - 故障切换窗口反而比优雅停机长:约 33 秒才恢复稳定可写。
- 应用侧会经历两段:① 6446 连不上(约 14s);② 连上但短暂只读 rw=1(约 2s)。
- 只读端口 6447 全程不中断;数据零丢失。
生产启示:高可用不是"切换多快",而是"应用扛不扛得住那几十秒"。
客户端必须有连接重试,写链路要有短暂只读降级预案。
8.3 从节点故障
- 写路径零影响;只读连接池自动收敛到剩下那台从库。
- 从库拉起后约 10 秒自动加回。
8.4 真正的高危场景:法定人数丢失(两台同时挂)
这是很多人以为"自己遇到了",但实际没遇到(或者遇到了不知道怎么处理)的场景。
- 现象:幸存节点显示 ONLINE/PRIMARY、
read_only=0,表面一切正常,但任何写事务永久卡在waiting for handler commit,不报错、不驱逐。 - 失联成员执行
START GROUP_REPLICATION会报ERROR 3092——因为加成员也需要多数派确认。
恢复步骤(重要):
# ① 清理幸存节点上所有挂起的写会话
# ② 用 forceQuorumUsingPartitionOf 恢复法定人数(不是 rebootClusterFromCompleteOutage!)
$SH --js -e "
shell.connect('root:Root%40123456@192.168.40.154:3306');
var c = dba.getCluster('myCluster');
c.forceQuorumUsingPartitionOf('root:Root%40123456@192.168.40.154:3306');"
实测输出:
Restoring cluster 'myCluster' from loss of quorum, by using the partition composed of [192.168.40.154:3306]
The InnoDB cluster was successfully restored using the partition from the instance 'root@192.168.40.154:3306'.
WARNING: To avoid a split-brain scenario, ensure that all other members are removed or joined back...
# ③ 其余成员加回
c.rejoinInstance('root:Root%40123456@192.168.40.152:3306', {recoveryMethod:'incremental'});
c.rejoinInstance('root:Root%40123456@192.168.40.153:3306', {recoveryMethod:'incremental'});
- 全流程约 4 分钟,恢复后三节点数据完全一致。
- 最值得记录的一点:恢复法定人数后,此前挂起的写事务自动完成提交——"已提交事务零丢失、未决事务最终落定"得到了直接验证。
坑 6:别用
dba.rebootClusterFromCompleteOutage()去恢复"实例还可达"的法定人数丢失。实测会直接拒绝并提示:The MySQL instance ... belongs to an InnoDB Cluster and is reachable. Please use <Cluster>.forceQuorumUsingPartitionOf() to restore from the quorum loss.
九、日常运维命令速查
SH=/usr/local/mysql-shell/bin/mysqlsh
# 集群状态 / 拓扑 / 参数
$SH --js -e "shell.connect('root:Root%40123456@192.168.40.152:3306'); var c=dba.getCluster('myCluster'); print(c.status());"
$SH --js -e "... c.describe();"
$SH --js -e "... c.options();"
# 监控 SQL(队列、冲突、延迟)
mysql -h127.0.0.1 -P3306 -uroot -N -e "
SELECT member_host,member_state,member_role FROM performance_schema.replication_group_members;
SELECT COUNT_TRANSACTIONS_IN_QUEUE, COUNT_CONFLICTS_DETECTED
FROM performance_schema.replication_group_member_stats;"
# 平滑切主(Router 数秒内自动跟随,无需重启)
$SH --js -e "shell.connect('...@192.168.40.152:3306'); var c=dba.getCluster('myCluster');
c.setPrimaryInstance('root:Root%40123456@192.168.40.152:3306');"
# 修改集群参数(如自动回组次数)
c.setOption('autoRejoinTries', 5);
# 单主 <-> 多主切换
c.switchToMultiPrimaryMode();
c.switchToSinglePrimaryMode();
# 缩容 / 扩容
c.removeInstance('root:Root%40123456@192.168.40.153:3306');
c.addInstance('root:Root%40123456@192.168.40.153:3306', {recoveryMethod:'incremental'});
注:2 节点集群状态为
OK_NO_TOLERANCE(不再容忍单点故障),扩容回 3 节点后恢复OK。
十、生产建议(血泪总结)
- 三台机器的
/etc/my.cnf里 GR 端口全部写:3306,别信旧文档的 33061。 - GR 参数以
datadir/mysqld-auto.cnf为准(SET PERSIST_ONLY写入),别两边写不一致。 - Router 至少部署 2 个(建议放到独立节点或与应用同机),否则 Router 是单点;生产用 systemd 托管并做健康检查。
- 应用连接串指向 Router 的 6446/6447,绝不直连实例,否则主从切换时应用会断。
- 客户端必须做连接重试 + 超时兜底,并接受写链路"几十秒不可用或只读"的窗口。
- 给
group_replication_member_expel_timeout、group_replication_unreachable_majority_timeout设非零值,别用 0(否则故障表现是"挂起"而非"报错",很难排查)。 - 无主键表是组复制的天敌,表必须有主键;MyISAM 已被禁用。
- 演练要常态化:优雅停、kill -9、从库故障、法定人数丢失这四种都跑过,才算真的高可用。
- 监控推荐:
performance_schema.replication_group_members(成员状态)+group_member_stats(延迟与冲突)+ Router 端口探活。 - 备份不能省:InnoDB Cluster 解决的是"高可用",不是"容灾",全备/增量(如
mysqlbackup或逻辑导出)仍然要做。
十一、总结
MySQL 8.4 的 InnoDB Cluster 把"复制 + 故障转移 + 路由"整合成了一体,落地门槛其实不高——真正难的是那些版本差异带来的隐性坑。本文的每个坑都来自真实报错:
| # | 坑 | 一句话结论 |
|---|---|---|
| 1 | 组复制通信端口 | 8.4 用 3306,不是 33061 |
| 2 | --initialize 报 unknown variable |
初始化用最小配置,别带 GR 段 |
| 3 | addInstance 卡住等待交互 | 显式 recoveryMethod: 'clone' |
| 4 | MYSQLSH 51156 waiting for server restart |
systemd Type=simple 不支持自动重启,手动 start + rescan |
| 5 | Router 端口 64460? | 8.4.10 就是 6446/6447/6450/6448/6449 |
| 6 | 法定人数丢失用错 API | 用 forceQuorumUsingPartitionOf(),别用 rebootClusterFromCompleteOutage() |
最后用一句实测数据收尾:三台机器、单主模式,主库优雅停机约 10 秒完成切换,kill -9 约 33 秒恢复稳定可写,全程数据零丢失。 这套东西,值得每个 MySQL 团队拥有。
如果你也在迁移 8.4 或准备上 InnoDB Cluster,欢迎留言交流踩坑经历。




