MySQL 8.4 InnoDB Cluster 的构建方法
摘要
- MySQL 官方高可用方案 InnoDB Cluster 的搭建、故障转移演练与日常运维
- 本文基于
mysql-8.4.11 LTS、mysql-shell-8.4.10、mysql-router-8.4.10 - 操作系统使用 Amazon Linux 2023(x86_64);其它发行版的用户、服务、软件包和配置文件路径可能不同
- 组成:Group Replication(数据复制与选主)+ MySQL Shell(AdminAPI)(搭建与管理)+ MySQL Router(把写流量路由到当前 Primary)
- 不需要自建证书也能建集群:内网实验沿用 MySQL/Router 自动证书和默认
AUTO/PREFERRED,通常仍会加密,但不验证对端身份;生产环境应为成员间、Router 与客户端启用身份校验 - 单节点/主从/双主的搭法见 MySql8.4单节点、主从、双主的构建方法;为什么不用 MHA 见 MySql-MHA的构建方法
常见面试题
0.先搞清楚这三个组件在干什么
MySql8.4单节点、主从、双主的构建方法 里的主从/双主,主库挂了不会自动切:要人工改应用连接串、人工提升从库。8.0 时代常用 MHA 补这一步,但原版 Perl MHA 0.58 使用的旧语句已被 8.4 删除,因此不兼容;面向 8.4 的社区重写版 MHA-Go 见 MySQL 8.4 上使用社区版 MHA-Go,本文只讲官方 InnoDB Cluster。InnoDB Cluster 不是「主从外面再挂一个监控进程」,而是把选主做进了数据库内核:
| 组件 | 装在哪 | 干什么 |
|---|---|---|
| Group Replication | 每个 MySQL 节点(内置插件) | 组内成员互相通信、按 Paxos 类协议对事务达成共识;Primary 失联后,多数派形成新视图并按选举规则产生新 Primary |
| MySQL Shell (AdminAPI) | 运维机器上一份就够 | 用 dba.* / cluster.* 命令搭集群、加减节点、切主、查状态,不用手写 Group Replication 参数 |
| MySQL Router | 贴着应用部署 | 读集群元数据,感知谁是 Primary;应用只连 Router 的固定端口,切主后 Router 自动把写流量指到新 Primary |
和主从 + MHA 的关键区别
-
选主是组内共识,不是外部进程探测。不需要一台单点 Manager,也不需要 SSH 免密去各节点捞 binlog。
-
靠多数派(quorum)防脑裂。少数派分区会变成不可写,而不是两边都能写。
-
故障切换时应用不用再改连接串。首次从直连 MySQL 迁移到 Router 时仍要改一次地址和端口,之后不用 VIP 漂移脚本,Router 负责把连接送到当前 Primary。
-
代价:生产高可用通常至少 3 个节点;参与复制的业务表必须使用 InnoDB,并具备主键或非空唯一键;事务要小。
容错能力看节点数
集群要能继续写,必须有超过半数成员在线。所以:
| 节点数 | 能坏几个 | 说明 |
|---|---|---|
| 2 | 0 | 坏一个就没多数派,整个集群不可写。不要用 2 节点 |
| 3 | 1 | 最小可用生产配置 |
| 4 | 1 | 容错数和 3 节点一样,但仍可增加只读能力和维护余量 |
| 5 | 2 | 要容忍 2 个故障才上 5 |
| 最多 9 | — | 组成员上限 |
生产高可用至少使用 3 个节点。为了获得更好的容错成本比,通常选择奇数节点,但这不是技术上的硬限制。节点还应分布在不同故障域;跨可用区部署要同时评估网络时延和丢包对提交性能的影响。
1.节点规划
1 | node1: 10.250.0.11 MySQL 8.4.11 + MySQL Shell |
-
三台 MySQL 按 MySql8.4单节点、主从、双主的构建方法 的单节点方式装好,不要去配主从
-
如果使用主机名,在所有机器上通过 DNS 或
/etc/hosts保证主机名稳定、双向可解析;也可以直接使用互通的 IP
1 | vim /etc/hosts |
-
所有节点启用 NTP/chrony 同步时间;节点间网络要低延迟、低丢包且双向互通
-
本文只在
app1演示 Router 命令;app2按相同步骤部署。应用实例分别连接本机 Router,避免 Router 成为公共单点
端口要放通
| 端口 | 用途 |
|---|---|
| 3306 | MySQL 客户端协议;8.4 新建集群默认使用 MYSQL 通信栈,组内通信也复用该端口,所以三个节点必须互通 |
| 33060 | 可选,仅使用 MySQL X 协议时开放 |
| 33061 | 可选,仅显式使用 XCOM 通信栈时常用;实际以 localAddress 为准 |
| 6446–6450 | 应用访问 Router 的端口,不需要在数据库节点之间开放 |
MySQL 8.0.27 起,AdminAPI 新建集群默认使用
MYSQL通信栈。本文采用默认值,不使用 33061;如果希望沿用独立的 XCOM 端口,要在创建集群时显式设置communicationStack:'xcom'。
2.前置条件(不满足后面必然失败)
InnoDB Cluster 用的就是 Group Replication,所以要求完全一致:
| 要求 | 说明 |
|---|---|
| 参与复制的业务表都是 InnoDB | MyISAM / MEMORY 等非事务引擎会导致组复制报错,先转换掉 |
| 每张业务表都有主键或等价键 | 主键最佳;也接受所有列均为 NOT NULL 的唯一键 |
| GTID 开启 | gtid_mode=ON、enforce_gtid_consistency=ON,这两个不是默认值,必须显式配 |
| binlog 为 ROW | 8.4 默认就是 ROW,一般不用动 |
| performance_schema 开启 | 状态监控依赖它,默认开启 |
| server_id 唯一 | 三个节点不能相同 |
| report_host 建议显式配 | 否则节点可能上报成一个别人连不上的主机名 |
| 没有未托管复制通道 | 成员上不要保留手工配置、未由 AdminAPI 管理的异步复制通道 |
lower_case_table_names 一致 |
所有成员必须相同;它通常只能在初始化数据目录时确定,不能指望 AdminAPI 上线后修正 |
| 没有全局复制过滤器 | 不要配置 binlog-do-db、replicate-* 等过滤器,否则成员数据可能不一致 |
-
把这些先写进每个节点的
/etc/my.cnf,其它参数 AdminAPI 会帮你配
1 | [mysqld] |
node2 改成 server-id=12、report_host=node2,node3 改成 server-id=13、report_host=node3,不能整段原样复制。lower_case_table_names 要在三台机器初始化数据目录之前统一决定;Amazon Linux 默认区分表名大小写,通常保持默认值 0。
-
检查一下有没有不合规的表
1 | -- 非InnoDB的表 |
Group Replication 接受非空唯一键,但生产规范仍建议每张表显式定义主键,便于运维和避免复制行查找性能问题。
3.安装 MySQL Shell 和 MySQL Router
-
Amazon Linux 2023 先准备目录和下载工具
1 | sudo dnf install -y wget tar xz gnupg2 |
-
MySQL Shell 装在三个 MySQL 节点(或至少一台运维机器)上
1 | # 下载地址:https://dev.mysql.com/downloads/shell/ |
-
MySQL Router 装在每台应用服务器上(下一步再 bootstrap)。Amazon Linux 2023 x86_64 的 glibc 版本满足该 Generic 包要求;ARM 或其它系统应在下载页选择匹配平台
1 | # 下载地址:https://dev.mysql.com/downloads/router/ |
-
Amazon Linux 2023 的
yum实际由 DNF 提供。配置 MySQL 官方仓库后也可用下面的方式安装,但版本取决于仓库当前状态
1 | yum info mysql-router-community |
本文后续的目录和 systemd 单元均按上面的 tar 包编写。使用 yum 安装时,应采用 RPM 自带的路径和
mysqlrouter.service,不要再创建第 9 节的同名服务。
Shell、Router 和 Server 独立发版,小版本号不要求完全相同,但官方建议 Shell、Router 的版本不低于 Server。本文写作时没有 8.4.11 的 Shell/Router 可下载,因此使用同属 8.4 LTS 的 8.4.10;这是受可用版本限制的组合,生产环境应在新版工具发布后优先升级。
4.检查并配置实例
AdminAPI 提供两个命令:checkInstanceConfiguration() 只检查,configureInstance() 会真的去改配置。
-
先登录 node1,在本机用 Shell 连接
root@localhost,进入 JS 模式。默认安装通常没有可从其它主机登录的 root 账号
1 | mysqlsh root@localhost:3306 |
-
检查当前实例是否满足要求
1 | MySQL localhost:3306 ssl JS > dba.checkInstanceConfiguration('root@localhost:3306') |
clusterAdmin 是什么账号
先说清楚这个账号的用途,否则下面的命令会一头雾水。
搭集群时你是用 root 连上去的,但 AdminAPI 之后要反复从一个节点去连另一个节点:addInstance() 要连新节点、setPrimaryInstance() 要连目标节点、status() 要挨个去问成员状态。它用的不是你当前的 root,而是这个专门的账号,官方叫 「服务器配置账号」(server configuration account)。
它需要元数据表的完整读写权限,外加 SUPER、GRANT OPTION、CREATE、DROP 等一整套管理员权限。不用自己去 GRANT,configureInstance() 会自动授全。
配置实例并创建这个账号
-
不传密码,Shell 会交互式提示你输入。该命令会持久化 Group Replication 所需配置,具体可能写入指定的
my.cnf,也可能通过SET PERSIST写入mysqld-auto.cnf
1 | MySQL localhost:3306 ssl JS > dba.configureInstance('root@localhost:3306', { |
-
'icadmin'@'10.250.0.0/27'是标准的 MySQL 账号格式:用户名 + 允许连接的 IPv4 CIDR。这个范围仅覆盖本文的数据库与 Router 节点;实际环境应换成专用管理网段。MySQL 8.4 已弃用 Host 中的%和_通配写法 -
也可以把密码写在参数里,但不推荐,它会留在 Shell 的命令历史里
1 | // 图省事可以这么写,生产环境别这么干 |
-
clusterAdminPasswordExpiration:'NEVER'要在账号首次创建时传入;已有账号要改成永不过期,使用ALTER USER 'icadmin'@'10.250.0.0/27' PASSWORD EXPIRE NEVER
为什么三个节点都要执行一遍
分别登录 node2、node3,在各自机器上连接 root@localhost:3306,使用与 node1 完全相同的 icadmin 密码执行:
1 | MySQL localhost:3306 ssl JS > dba.configureInstance('root@localhost:3306', { |
警告⚠️
这个账号不会自动同步到其它节点。 MySQL Shell 在执行 configureInstance() 时会关掉 binlog,所以建账号这个动作不进 binlog、也就不会被复制——必须在每个节点单独建一次。
而且此时集群还没建起来,三个节点之间本来就没有任何复制关系,指望它自己传过去是不可能的。
密码必须三个节点一致:AdminAPI 拿着你在当前会话里用的这套凭据去连其它节点,某个节点上的密码不一样,addInstance() 就会在那里认证失败。
和 setupAdminAccount() 的区别
后面第 11 节的 cluster.setupAdminAccount() 建的账号走 binlog、会自动复制到所有成员,所以那个只需要在一个节点上执行一次。
区别就在于执行时机:configureInstance() 跑在集群成立之前(无复制、且主动关了 binlog),setupAdminAccount() 跑在集群成立之后(有复制)。
如果提示需要重启(比如刚加的
gtid_mode),按提示重启对应节点的 mysqld 再重新执行检查。
-
建完可以验证一下账号确实在每个节点上都有
1 | mysql> SELECT user, host FROM mysql.user WHERE user = 'icadmin'; |
5.创建 TLS 证书(实验可跳过)
Group Replication 不强制 TLS。内网学习、验证选主和 Router 时,可直接跳到第 6 节的「实验环境」命令。
生产环境建议至少覆盖三条链路:
| 链路 | 作用 |
|---|---|
| MySQL 成员 ↔ 成员 | Group Replication 通信与分布式恢复 |
| 客户端 ↔ Router | 应用连 6446/6447/6450 |
| Router ↔ MySQL | Router 读元数据、转发业务连接 |
下面用 OpenSSL 自建 CA,仅为演示。生产应改用企业 CA、Vault PKI 或支持导出私钥的 ACM / AWS Private CA 证书,并妥善保管私钥。
在运维机生成 CA
1 | sudo dnf install -y openssl |
为每个 MySQL 节点签发证书
证书 SAN 必须包含客户端实际用来连接的主机名或 IP。本文成员之间使用主机名 node1 / node2 / node3,所以每张节点证书至少包含自己的 DNS 名和对应 IP:
1 | # 一次生成三套独立文件,避免复制命令时覆盖node1证书 |
-
验证签发链、SAN、有效期以及证书和私钥是否匹配
1 | for name in node1 node2 node3 |
为 Router 签发证书
本文应用连 127.0.0.1,证书 SAN 至少包含该地址;若还要用主机名访问,一并写入:
1 | # app1、app2分别生成独立文件,分发时再统一改成Router使用的文件名 |
-
同样进行验证
1 | for name in app1 app2 |
分发到各机器
1 | # MySQL节点:源文件名和目标节点一一对应 |
CA 私钥 ca/ca-key.pem 只保留在受控运维机或离线密钥库,绝不能复制到 MySQL 或 Router 节点。
实验环境怎么选
- 只想尽快跑通集群:跳过自建证书,使用 MySQL 初始化时自动生成的证书以及默认
AUTO/PREFERRED。这通常是加密连接,但不验证对端身份,并不等于明文传输。 - 准备上生产:先完成本节,再建集群时使用
memberSslMode:'VERIFY_IDENTITY',Router bootstrap 也带上 TLS 参数。 memberSslMode在createCluster()时确定,上线后再补身份校验成本很高,生产务必一次做对。
6.创建集群
实验环境:不自建证书
内网学习可直接创建。MySQL 初始化通常已经生成 ca.pem、server-cert.pem 等自动证书;memberSslMode:'AUTO' 会在实例支持 TLS 时启用加密,但不校验证书中的主机身份。不要把它表述成“关闭 TLS”。
-
用管理账号连上准备当第一个 Primary 的节点(种子节点)
1 | mysqlsh icadmin@node1:3306 |
-
创建集群。显式写出默认的
MYSQL通信栈,并把 AdminAPI 内部恢复账号限制在集群网段
1 | MySQL node1:3306 ssl JS > var cluster = dba.createCluster('myCluster', { |
生产环境:启用成员 TLS
先把第 5 节生成的证书配进每个节点的 my.cnf,然后重启 mysqld:
1 | [mysqld] |
确认主监听通道已加载证书;MySQL 8.4 不再使用 have_ssl 变量:
1 | mysql> SELECT CHANNEL, PROPERTY, VALUE |
-
用管理账号连上种子节点,并校验服务端身份
1 | mysqlsh --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/mysql/tls/ca.pem icadmin@node1:3306 |
-
创建集群时显式开启成员身份校验
1 | MySQL node1:3306 ssl JS > var cluster = dba.createCluster('myCluster', { |
-
此时只有一个节点,注意最后那句提示:要 3 个节点才能容忍一次故障
var cluster = ... 这个变量很重要
后面所有 cluster.xxx() 操作都靠它。如果 Shell 会话断了,重新连上后用 var cluster = dba.getCluster() 把集群对象取回来,不用重建集群。实验环境直接 mysqlsh icadmin@nodeX:3306;生产环境继续带上 --ssl-mode=VERIFY_IDENTITY --ssl-ca=...。
7.加入其余节点
-
如果省略
recoveryMethod,Shell 可能会询问怎么同步已有数据:
| 方式 | 说明 |
|---|---|
| Clone(推荐) | 从自动选择的在线 donor 获取完整物理快照,目标节点原有数据会被清空,接收实例随后需要重启 |
| Incremental recovery | 从其它成员的 binlog 增量补齐;要求目标 GTID 是集群 GTID 的子集、没有 errant GTID、历史事务始终使用 GTID,且所需 binlog 尚未清理 |
| Abort | 放弃 |
-
新节点一般直接指定 Clone;下面是实际执行的两条命令
1 | MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node2:3306', {recoveryMethod: 'clone', label: 'node2'}) |
警告⚠️
选 Clone 会清空目标节点上的所有数据,用 donor 的数据覆盖。Clone 插件通常会自动重启接收实例,AdminAPI 等待它恢复并继续配置;如果当前启动方式不支持由 MySQL 发起重启,实例会关闭,需要手工启动 mysqld 后让 AdminAPI 继续或重新执行。给新机器用没问题,往一台有数据的实例上加之前一定要确认清楚,同时检查目标磁盘空间和 donor 的 I/O、网络压力。需要固定来源时使用 cloneDonor:'node2:3306'。
8.查看集群状态
1 | MySQL node1:3306 ssl JS > cluster.status() |
-
重点看三个字段:
primary是谁、status和每个成员的status -
默认是 Single-Primary:一个
R/W,其余R/O(Secondary 上super_read_only=ON,想写也写不进去)
集群级 status 的含义
| 值 | 含义 |
|---|---|
OK |
有多数派,且还能再坏至少一个 |
OK_PARTIAL |
有成员掉了,但仍有容错余量 |
OK_NO_TOLERANCE |
还能写,但再坏一个就会失去 quorum,应尽快恢复冗余 |
OK_NO_TOLERANCE_PARTIAL |
同上,且有成员不在组里 |
NO_QUORUM |
失去多数派,不可写;Router 默认 unreachable_quorum_allowed_traffic=none,会断开相关现有连接并拒绝新连接 |
OFFLINE |
所有成员的 Group Replication 都处于 OFFLINE,即插件已加载但未加入复制组 |
ERROR |
没有在线成员 |
UNREACHABLE / UNKNOWN |
Shell 无法联系任何在线成员以确定状态;检查 DNS、网络、TLS、账号和 mysqld |
FENCED_WRITES |
集群被 AdminAPI 禁止写入;常见于 ClusterSet fencing 场景 |
不建议把
unreachable_quorum_allowed_traffic改为read或all。特别是all允许失去 quorum 的分区继续写入,会削弱防脑裂保护。
-
extended:1查看组协议、成员角色和 fenced 变量;GTID、applier、连接状态等事务细节使用extended:2或3
1 | MySQL node1:3306 ssl JS > cluster.status({extended: 1}) |
-
也可以直接查 performance_schema,不依赖 Shell
1 | mysql> SELECT MEMBER_ID, MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE, MEMBER_VERSION |
9.部署 MySQL Router
集群自己会选主,但应用怎么知道新 Primary 是谁?这就是 Router 的活。
bootstrap
Router 不需要手写配置,用 --bootstrap 连一次集群,它会把拓扑信息抓下来自动生成配置。
-
先在集群上为 app1 预建 Router 专用账号,避免 bootstrap 创建
routeruser@'%'
1 | MySQL node1:3306 ssl JS > cluster.setupRouterAccount('routeruser@10.250.0.21', { |
Shell 会提示设置密码,该操作经 Group Replication 复制到全部成员。部署 app2 时再创建 routeruser@10.250.0.22。
实验环境:不自建证书的 bootstrap
1 | # tar 包不会预置 /etc/mysqlrouter 等目录,本文用 --directory 生成自包含实例 |
Router 会自动生成自签证书供客户端可选加密使用;上面显式写出的都是默认模式,便于说明实验流量通常仍会加密,但不验证对端身份。生产不要依赖这份自动证书做身份校验。
生产环境:启用 TLS 的 bootstrap
先把第 5 节的 CA 与 Router 证书放到 /etc/mysqlrouter/tls/(若前面已分发可跳过):
1 | sudo mkdir -p /etc/mysqlrouter/tls |
然后执行:
1 | # tar 包不会预置 /etc/mysqlrouter 等目录,本文用 --directory 生成自包含实例 |
--directory 会把配置、日志、keyring 及启停脚本集中到指定目录。一台机器需要多个 Router 实例时,为每个实例使用不同目录、--name 和端口。本文禁用了不需要的 REST API;如果启用,默认还会生成 HTTPS 8443 服务,必须单独限制监听地址、认证、证书和防火墙。
这条命令里有三个不同的「用户」,别搞混
| 参数 | 是什么 | 密码 |
|---|---|---|
icadmin@node1:3306 |
MySQL 账号,就是第 4 节建的那个 clusterAdmin。bootstrap 这一次用它登录集群,读取拓扑、往元数据里注册自己 | 第 4 节 configureInstance() 时设的,bootstrap 会提示你输 |
--user=mysqlrouter |
操作系统账号,不是 MySQL 账号。本文在安装 tar 包时手动创建;RPM/DEB 安装则会自动创建 | 不用于 MySQL 认证 |
--account=routeruser |
MySQL 账号,Router 长期运行时用它刷新集群元数据 | 已由 setupRouterAccount() 创建;bootstrap 会提示输入该账号密码并存入 keyring |
-
--user决定进程及生成文件的操作系统属主;以 root 执行 bootstrap 时必须指定 -
routeruser技术上也可以换成icadmin,但这严重违反最小权限原则,生产环境不应这样做 -
本文使用
never,账号不存在就立即失败;if-not-exists会复用或创建,always则要求账号此前不存在 -
bootstrap 不会给已有账号自动补权限,所以先用
cluster.setupRouterAccount()创建;升级后还要使用{update:true}更新这个自定义账号的权限 -
多个 Router 共用账号便于管理,但会扩大凭据泄漏和轮换的影响范围;是否共享要按安全边界决定
-
bootstrap 将 Router 密码加密存入 keyring,systemd 启动时无需再次输入。本文对应的文件位于
/usr/local/soft/mysqlrouter-instance/data/keyring和/usr/local/soft/mysqlrouter-instance/mysqlrouter.key,不要放宽权限
--conf-bind-address=127.0.0.1适用于 Router 与应用同机。如果 Router 集中部署在独立机器,需要改成应用能够访问的地址,并用防火墙限制 6446–6450 的来源。
Router 暴露的端口
| 端口 | 协议 | 去哪 |
|---|---|---|
| 6446 | classic | PRIMARY。可读可写,但所有语句都发给 Primary,不做读写分离 |
| 6447 | classic | 正常情况下轮询 SECONDARY;默认 round-robin-with-fallback,没有 Secondary 时会回退到 Primary |
| 6448 | X 协议 | PRIMARY |
| 6449 | X 协议 | SECONDARY |
| 6450 | classic | 读写分离端口,Router 解析事务类型自动分流(8.2 起默认生成,--disable-rw-split 可关掉) |
启动并设置开机自启
tar 包不附带 systemd 单元,先为本文的实例创建一个:
1 | sudo tee /etc/systemd/system/mysqlrouter.service >/dev/null <<'EOF' |
先建业务账号
前面建的 icadmin(管集群)和 routeruser(Router 自用)都不是给业务用的,业务账号要自己建。
-
只在 Primary 上执行一次,会经 binlog 自动复制到其它成员
1 | -- 连到当前Primary,或者直接连Router的6446 |
警告⚠️
host 段要匹配 MySQL Server 实际观察到的来源地址,通常是 Router 所在机器的地址。
Router 是代理,MySQL 看到的连接来源是 Router 的 IP,不是原始客户端的 IP(MySQL Server 和 Router 都不支持 Proxy Protocol)。上面分别为 app1 和 app2 创建同名、同密码账号,使应用连接任一机器上的本地 Router 都能认证。
如果应用与 Router 不在同一网络命名空间,按应用容器地址授权可能会报 Access denied。存在 NAT、多网卡或容器网络时,应先从 MySQL 端确认实际来源地址,再设置最小范围的 host。
-
要排查某个连接的真实客户端 IP,走 SSL 连接时 Router 会把它塞进连接属性里
1 | mysql> SELECT program_name, user, attr_value AS client_ip |
应用怎么连
1 | # 实验环境:默认PREFERRED,通常加密但不验证Router身份 |
-
JDBC 连接串同样只指向 Router,不再写任何数据库节点的 IP
1 | # 实验环境 |
把第 5 节的 CA 导入应用专用 truststore:
1 | sudo install -d -o app -g app -m 750 /etc/myapp |
把示例中的
app:app换成应用进程的实际用户和组;truststore 密码应从密钥管理系统注入,并按 URL 规则编码,不能硬编码进仓库。该示例对应“应用与 Router 同机”和--conf-bind-address=127.0.0.1。集中部署时改用 Router 域名,并保证域名存在于证书 SAN 中。旧参数useSSL=true通常只保证尝试加密,不等于验证服务端身份。
连上 6446 之后,读写到底怎么走
这里有个很容易误解的地方:6446 叫「读写端口」,意思是「可读可写」,不是「自动读写分离」。
-
6446 上的所有 SQL——包括
SELECT——都发给 Primary,Secondary 一点读流量都分不到 -
Router 在 6446 / 6447 上不解析 SQL,它只是个连接级的转发器,按端口决定去哪,不看你发的是什么语句
-
6447 正常连到 Secondary,写操作会因
super_read_only=ON报错;但所有 Secondary 都不可用时,默认策略会回退到 Primary,此时写操作可能成功。不能把 6447 当成权限意义上的只读边界
于是有两种用法:
| 做法 | 怎么用 | 适合 |
|---|---|---|
| 应用自己分流 | 写连 6446,读连 6447,业务代码或框架里配两个数据源 | 想精确控制哪些查询能容忍延迟 |
| 交给 Router 分流 | 将连接端口改为 6450,Router 按事务类型判断,读发 Secondary、写发 Primary | 应用语句经过兼容性验证后使用 |
6450 至少需要修改连接端口,而且不能直接假设“零改造”,相关特性要分开看:
-
明确路由到 Primary:
CALL、DDL、DML、事务和锁语句、账号及大部分管理语句 -
access_mode=auto不支持:复制语句,以及 Router 无法归类的部分管理语句;执行时会直接报错 -
可以执行但影响连接共享:临时表、用户变量、预处理语句、
GET_LOCK()等会让后端连接暂时不能进入共享池;部分依赖前序会话状态的语句必须放在事务中
上线前应使用真实驱动和 SQL 集做回归测试,不能只验证普通 SELECT、INSERT。
-
6450 可以按 session 临时改行为,比如强制这个会话全部走 Primary
1 | mysql> ROUTER SET access_mode='read_write'; |
wait_for_my_writes=1 只跟踪同一个客户端会话最后一次写入。默认最多等待 1 秒(wait_for_my_writes_timeout=1),Secondary 仍未追上时会回退到 Primary;该机制仅适用于 6450 读写分离路由,不能替代显式连接 6447 时的一致性设计。
写入是怎么到其它节点的
写进 Primary 之后会同步到另外两个节点,但机制和普通主从不一样,有个关键区别要知道:
-
提交时:事务要被多数派接收并确认全局顺序才会给客户端返回成功。正常 quorum 仍然存在、发生单一 Primary 故障时,可用多数派能够保留已确认事务
-
应用时:Secondary 把事务真正回放到自己的表里是异步的
所以「写完立刻从 6447 读」仍然可能读不到刚写的数据。这不是 bug,是组复制的正常行为。需要保证能立即读到本会话刚刚写入的数据时,可使用 6450 默认的 wait_for_my_writes 机制,或让这类查询直接走 6446。强制 quorum、从完整停机中选错 GTID 节点或相关故障域同时损坏,不属于上述单节点自动切换保证。
顺带说个术语:InnoDB Cluster 里叫 Primary / Secondary,没有 master/slave 的说法。8.4 连 SQL 语句都改名了(
SHOW REPLICA STATUS之类),细节见 MySql8.4单节点、主从、双主的构建方法。
警告⚠️
Router 自己是单点。 官方推荐的部署方式是把 Router 和应用放在同一台机器上(每个应用实例一个 Router),这样 Router 挂了只影响这一个应用实例。不要全公司共用一台 Router;如果一定要集中部署,就多台 Router + 前面加 LB/VIP。
10.故障转移演练
-
先确认当前 Primary 是 node1。
systemctl stop是正常离组,适合验证计划内维护;下面使用进程崩溃模拟非计划故障
1 | # 在node1上 |
-
连到 node2 看集群状态
1 | # 实验环境 |
1 | MySQL node2:3306 ssl JS > var cluster = dba.getCluster() |
-
primary已经变成 node2,整个过程没有人工干预,通常在秒级完成 -
node1 的状态是
(MISSING),集群变成OK_NO_TOLERANCE_PARTIAL:还能写,但再坏一个就失去多数派 -
应用连接地址仍是本机 6446;原连接会断开,但客户端重连或连接池重试后,新请求会被送到 node2
1 | mysql -uappuser -p -h127.0.0.1 -P6446 -e "select @@hostname, @@super_read_only;" |
警告⚠️
切主时已建立的连接会断开。 Router 只保证新连接被送到新 Primary,它不会把一个已经断掉的 TCP 会话「搬」过去。连接中断时还可能出现“提交结果未知”:服务端也许已经提交,只是响应没送回来。因此不能无条件重放写事务,订单、支付等操作必须使用请求 ID、唯一约束或业务去重保证幂等。
别拿实验室里测出来的秒数当 SLA:真实耗时取决于故障检测、选主、积压事务回放、Router 刷新元数据和客户端重试策略。
-
把 node1 修好后启动 mysqld,它一般会自动重新加入
1 | sudo systemctl start mysqld |
-
没自动加回来就手工 rejoin
1 | MySQL node2:3306 ssl JS > cluster.rejoinInstance('icadmin@node1:3306') |
注意 node1 回来之后是 SECONDARY,不会自动抢回 Primary。想让它当主得手工切(见下一节)。
11.日常运维命令
主动切主(计划内维护)
1 | // 把Primary切到node1,用于打补丁、重启前腾空某个节点 |
runningTransactionsTimeout 的有效范围是 0–3600 秒。设置后,AdminAPI 最多等待切换开始时仍在运行的事务 60 秒,并拒绝新的事务进入;不设置则没有等待上限,期间还可能继续进入新事务。超时时先排查或终止长事务,不要直接把计划切主当作故障切换。
摘除 / 加回节点
1 | // 正常摘除 |
force:true只会强制清理元数据,不能让不可达节点停止运行。执行前必须确认旧节点已经关机或完成网络/存储 fencing;重新使用时检查 GTID,必要时用 Clone 重建。
加只读副本(8.1 首次引入,8.4 LTS 提供)
Read Replica 是异步只读副本:不参与投票、不影响容错计算,适合承载报表或额外读流量。添加前要像普通成员一样配置实例和管理账号,并确认没有未由 AdminAPI 管理的复制通道。
1 | // 登录node4,在本机交互输入与前三个节点相同的icadmin密码 |
只读副本会出现在 cluster.status() 的 readReplicas 里,但 Router 默认只把只读流量发给 Secondary。要让指定 Router 使用 Read Replica:
1 | // 先用cluster.listRouters()确认名称 |
补建管理账号
1 | MySQL node1:3306 ssl JS > cluster.setupAdminAccount('icadmin2@10.250.0.0/27', { |
查看集群配置项
1 | MySQL node1:3306 ssl JS > cluster.options() |
单主 / 多主切换
1 | MySQL node1:3306 ssl JS > cluster.switchToMultiPrimaryMode() |
警告⚠️
多主模式不是「性能更好的单主」。 多主下所有节点都能写,冲突靠事务认证阶段检测,撞了就回滚;多级外键依赖并带 CASCADE 操作的表结构不受支持,DDL 与同一对象的 DML 还必须在同一节点协调执行。多主模式通常建议使用 READ COMMITTED,绝大多数业务应优先采用默认单主模式。
12.故障恢复
场景一:失去多数派(NO_QUORUM)
3 节点挂了 2 个,剩下的这个凑不出多数派,集群不可写。这时需要人工告诉它「就用这个分区继续跑」:
1 | // 连到还活着的那个节点 |
警告⚠️
这个命令是强行重定义成员关系,绕过正常投票保护。执行前必须把被排除节点关机、隔离或 fence,不能只凭“连不上”判断。恢复后,旧成员必须通过 remove/rejoin 或 Clone 回到新成员关系,不能让旧分区自行恢复运行,否则可能形成脑裂。
场景二:所有节点都挂了(完全停机)
比如整个机房断电后重启,三个 mysqld 都起来了但组复制没起来。不要只比较 gtid_executed 后就凭感觉选节点;AdminAPI 还会检查 Group Replication 已认证但尚未应用的事务。先连接候选节点并做演练检查:
1 | MySQL node1:3306 ssl JS > dba.rebootClusterFromCompleteOutage( |
-
默认恢复:Shell 会比较所有可达成员,并要求当前连接的节点具有完整事务集
1 | MySQL node1:3306 ssl JS > var cluster = dba.rebootClusterFromCompleteOutage('myCluster') |
-
指定 Primary:指定节点默认仍必须拥有 GTID 超集,不能仅为了偏好而选择落后节点
1 | MySQL node1:3306 ssl JS > var cluster = dba.rebootClusterFromCompleteOutage( |
-
强制恢复:仅在成员确实无法恢复且已隔离时使用;它也允许选择较旧或分叉的事务集
1 | MySQL node1:3306 ssl JS > var cluster = dba.rebootClusterFromCompleteOutage( |
force:true用于忽略不可达成员时不必然丢数据,但若接受了较旧或分叉的事务集,会放弃所选节点不包含的事务。强制恢复后,缺失成员未必能直接rejoinInstance(),GTID 分叉时要用 Clone 重建。
场景三:某个节点数据分叉了
节点上有集群里不存在的事务(比如被人在 Secondary 上强行写过),它会拒绝加入。做法是摘掉再用 Clone 重灌:
1 | MySQL node1:3306 ssl JS > cluster.removeInstance('icadmin@node3:3306', {force: true}) |
13.几个必须知道的坑
切主时的一致性保护
第 9 节已经说明直接读取 Secondary 可能得到旧数据。MySQL 8.4 在切主时另有一层保护:group_replication_consistency 默认是 BEFORE_ON_PRIMARY_FAILOVER。新 Primary 在回放完旧 Primary 的积压事务之前不会接受新的读写,避免应用切过去读到旧值;代价是切主时间会随积压量增加。
1 | -- 查看当前级别 |
强一致读需求建议按 session 设置,不要全局拉到最高级别,否则整体延迟都会被拖高。
大事务会被拒绝
组复制要把事务在成员间达成共识,太大的事务会增加网络和内存压力。默认上限由 group_replication_transaction_size_limit 控制,为 150,000,000 字节(约 143 MiB),超出后事务会回滚且不会发送给组通信系统。
所以 DELETE FROM t WHERE ... 一把删掉几百万行应改成按主键分批、每批独立提交。大表 DDL 是另一类问题,应先评估 MySQL 8.4 原生 ALGORITHM=INSTANT / INPLACE、锁等待和 Secondary 回放压力。gh-ost 上游不支持 Group Replication,不能直接作为 InnoDB Cluster 的通用方案。
不能自己挂异步复制通道
InnoDB Cluster 不支持在成员上手工配置 AdminAPI 之外的复制通道。想挂只读副本用 addReplicaInstance(),不要自己去 CHANGE REPLICATION SOURCE TO。
节点要合理分布在故障域
奇数节点能获得更好的容错成本比,但偶数节点仍可增加读能力和维护余量。三个节点如果都在同一台宿主机上,这套高可用只能防 mysqld 进程故障,不能防宿主机或机架故障。
高可用不是备份
三个成员会把误删、错误更新和逻辑损坏一起复制,Group Replication 不能替代备份。生产至少要有:
-
定期全量备份,并将备份放在集群故障域之外
-
持续保留 binlog,用于时间点恢复(PITR)
-
定期在隔离环境执行恢复演练,验证 RPO/RTO,而不是只检查备份文件存在
小数据量可用 MySQL Shell util.dumpInstance() / util.loadDump();大数据量根据版本和许可选择经过验证的物理备份方案。Clone 用于成员初始化和重建,不是历史备份。
恢复时不要把备份直接覆盖到仍属于在线集群的成员上。应先在隔离环境恢复并校验数据,再按恢复目标处理:
-
util.loadDump()如果需要通过updateGtidSet更新 GTID,必须先停止 Group Replication -
物理恢复后如果
server_uuid发生变化,旧 metadata 中的实例身份已经失效,不能直接rejoinInstance();应移除旧身份并用rescan()重新识别,或把它作为新实例加入 -
全集群恢复应选出事务最完整的恢复节点作为种子,再用 Clone 重建其它成员;全过程先验证 GTID,防止把较新的事务覆盖掉
监控不能只看 ONLINE
至少对以下内容设置告警:
1 | MySQL node1:3306 ssl JS > cluster.status({extended: 2}) |
1 | SELECT MEMBER_ID, COUNT_TRANSACTIONS_IN_QUEUE, |
-
成员状态、Primary 变化和成员数量是否低于容错要求
-
applier/certifier 队列是否持续增长,以及 flow control 是否频繁触发
-
磁盘、binlog、Clone 空间、网络时延和丢包
-
Router 进程、6446/6447/6450 端口及实际读写探测
-
MySQL、Router 叶子证书和 CA 的剩余有效期,至少提前 30 天告警
可把下面的检查接入 cron 或现有监控;退出码非 0 表示 30 天内过期:
1 | # 在MySQL节点执行 |
最慢的成员可能触发 flow control 并限制整个集群写入速度,所以 Secondary 不是免费的读扩容。
证书轮换
叶子证书续签后,先执行第 5 节的链、SAN 和公私钥匹配检查,再按 Secondary → Primary 顺序逐台替换:
-
原子替换该节点的证书和私钥,保持
root:mysql、证书644、私钥640 -
执行
ALTER INSTANCE RELOAD TLS,让 MySQL 主监听端口的新连接使用新证书 -
ALTER INSTANCE RELOAD TLS不会刷新正在运行的 Group Replication TLS 上下文;执行STOP GROUP_REPLICATION; START GROUP_REPLICATION; -
等该成员恢复
ONLINE后再处理下一台;处理 Primary 前先使用setPrimaryInstance()计划切主 -
Router 证书逐台替换并重启 Router,验证 6446/6447/6450 后再处理下一台
CA 轮换不能直接覆盖旧 CA:先下发同时包含新旧 CA 的信任链,再换服务端证书和客户端信任,最后移除旧 CA。整个过程保持至少两个 Group Replication 成员在线,不要同时停止所有成员。
滚动维护与升级
升级前先核对 Server、Shell、Router 的兼容矩阵和发布说明,并做好备份与回滚方案。先把运维端 MySQL Shell 升级到不低于目标 Server 的版本,再在每个数据库节点本机执行检查;configPath 是运行 Shell 这台机器上的本地路径:
1 | MySQL JS > util.checkForServerUpgrade('icadmin@localhost:3306', { |
app2 的 Router 账号也要执行一次 {update:true}。Server 常规顺序是逐台升级 Secondary,每台恢复 ONLINE 后再处理下一台,最后计划切主并升级原 Primary。不要一次停掉超过容错范围的成员,Router 也应逐台重启并验证端口探测。
跨机房要用 ClusterSet
Group Replication 要求成员之间网络延迟低,不适合跨地域拉伸。跨机房容灾用 InnoDB ClusterSet:一个主集群 + 一个或多个从集群,集群之间异步复制。注意 ClusterSet 的跨集群切换是管理员手动触发的,紧急切换还可能丢未复制的事务,它解决的是容灾,不是又一层自动 failover。
14.和其它方案对比
| 方案 | 自动选主 | 一致性 | 节点数 | 说明 |
|---|---|---|---|---|
| InnoDB Cluster | 是,组内自动选主 | 正常 quorum 单节点故障下保留已确认事务 | 生产通常 ≥3 | MySQL 官方方案 |
| 主从 + 半同步 | 否 | 半同步缩小丢数窗口,超时降级异步 | ≥2 | 见 MySql8.4单节点、主从、双主的构建方法 |
| 主从 + 原版 MHA 0.58 | 是 | 尽量补齐 binlog,极端情况丢数 | ≥3 | 不兼容 8.4,见 MySql-MHA的构建方法 |
| 主从 + MHA-Go | 是 | GTID 主从;默认 salvage 补不齐则中止 | ≥2 库 + Manager | 社区版,仅 8.4.x/9.7,见 MySQL 8.4 上使用社区版 MHA-Go |
| Percona XtraDB Cluster | 是 | Galera 认证复制 | ≥3 | 生态和运维模型不同,按兼容性评估 |
| 云数据库多可用区 | 取决于产品 | RPO/RTO 取决于厂商实现与规格 | — | 以厂商 SLA、演练结果和成本为准 |
15.官方资料
InnoDB Cluster 凭什么能自动切主?应用要改什么?
- Group Replication 通过多数派维护成员关系和事务全局顺序,Primary 故障后由组内自动选主。
- 生产通常至少 3 节点;奇数节点容错成本比更高,但不是强制限制。
- 应用首次改为连接 Router 后,故障切换不再修改连接串;6446 指向 Primary,6450 才是自动读写分离。
- 切主会断开已有连接,重试写请求必须解决“提交结果未知”和幂等问题。
- 8.4 默认
BEFORE_ON_PRIMARY_FAILOVER,新 Primary 回放完积压后才服务;直接读 Secondary 仍可能陈旧。 - 业务表必须使用 InnoDB,并有主键或非空唯一键;GTID 必须开启,大事务要分批。
- 内网实验可不自建证书,默认
AUTO/PREFERRED通常仍会加密;生产应对成员间、Router 与客户端做身份校验。
MySQL 8.4 + 3节点 + GTID + 自动 Failover 方案对比
| 能力/方案 | MHA 0.58 | mha_go | MoHA | Replication Manager | Orchestrator | InnoDB Cluster |
|---|---|---|---|---|---|---|
| MySQL 8.4 新部署 | ⚠️(不兼容) | ✅ | ⚠️ | ✅ | ⚠️ | ✅ |
| GTID | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 自动 Failover | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Switchover | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 自动选主 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Semi-sync 支持 | ✅ | 视实现 | 视实现 | ✅ | ✅ | GR |
| 脑裂控制 | ⚠️(较弱) | 较强 | 强 | 较强 | ⚠️ | 协议级 |
| Proxy 集成 | 自己做 | 自己做 | 自己做 | 较强 | 自己做 | Router 原生 |
| Old Primary Rejoin | 手工/脚本 | 有机制 | 有机制 | 较强 | 有 | 较强 |
| Clone/Re-seed 支持 | 自己处理 | 部分 | 有 | 有 | 有限 | 原生 |
| 组件数量 | 少 | 少 | 多 | 中 | 中 | 中 |
| 社区/项目成熟度 | 历史成熟,但多年未更新 | 新 | 中小,但多年未更新 | 较成熟 | 已归档 | 官方体系 |
| 新项目推荐价值 | 低 | 高 | 中 | 高 | 低 | 高,推荐 |