暂无图片
暂无图片
5
暂无图片
暂无图片
暂无图片

三台服务器,从零搭出 MySQL 8.4 InnoDB Cluster 高可用集群(实战全记录)

原创 山佳 2026-09-06
351

作者亲测 | 适用:想在生产或测试环境快速落地 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。


十、生产建议(血泪总结)

  1. 三台机器的 /etc/my.cnf 里 GR 端口全部写 :3306,别信旧文档的 33061。
  2. GR 参数以 datadir/mysqld-auto.cnf 为准(SET PERSIST_ONLY 写入),别两边写不一致。
  3. Router 至少部署 2 个(建议放到独立节点或与应用同机),否则 Router 是单点;生产用 systemd 托管并做健康检查。
  4. 应用连接串指向 Router 的 6446/6447,绝不直连实例,否则主从切换时应用会断。
  5. 客户端必须做连接重试 + 超时兜底,并接受写链路"几十秒不可用或只读"的窗口。
  6. 给 group_replication_member_expel_timeout、group_replication_unreachable_majority_timeout 设非零值,别用 0(否则故障表现是"挂起"而非"报错",很难排查)。
  7. 无主键表是组复制的天敌,表必须有主键;MyISAM 已被禁用。
  8. 演练要常态化:优雅停、kill -9、从库故障、法定人数丢失这四种都跑过,才算真的高可用。
  9. 监控推荐:performance_schema.replication_group_members(成员状态)+ group_member_stats(延迟与冲突)+ Router 端口探活。
  10. 备份不能省: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,欢迎留言交流踩坑经历。

最后修改时间:2026-09-07 10:17:11
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论