MySql8.4 InnoDB Cluster 的构建方法
摘要
- MySQL 官方高可用方案 InnoDB Cluster 的搭建、故障转移演练与日常运维
- 本文基于
mysql-8.4.11 LTS、mysql-shell-8.4.10、mysql-router-8.4.10 - 组成:Group Replication(数据复制与选主)+ MySQL Shell(AdminAPI)(搭建与管理)+ MySQL Router(把写流量路由到当前 Primary)
- 单节点/主从/双主的搭法见 MySql8.4单节点、主从、双主的构建方法;为什么不用 MHA 见 MySql-MHA的构建方法
常见面试题
0.先搞清楚这三个组件在干什么
MySql8.4单节点、主从、双主的构建方法 里的主从/双主,**主库挂了不会自动切**:要人工改应用连接串、人工提升从库。8.0 时代常用 MHA 补这一步,但 MHA 在 8.4 上已经不可用。InnoDB Cluster 不是「主从外面再挂一个监控进程」,而是把选主做进了数据库内核:
| 组件 | 装在哪 | 干什么 |
|---|---|---|
| Group Replication | 每个 MySQL 节点(内置插件) | 组内成员互相通信、按 Paxos 类协议对事务达成共识;成员失联时组内自己投票选出新 Primary |
| MySQL Shell (AdminAPI) | 运维机器上一份就够 | 用 dba.* / cluster.* 命令搭集群、加减节点、切主、查状态,不用手写 Group Replication 参数 |
| MySQL Router | 贴着应用部署 | 读集群元数据,感知谁是 Primary;应用只连 Router 的固定端口,切主后 Router 自动把写流量指到新 Primary |
和主从 + MHA 的关键区别
-
选主是组内共识,不是外部进程探测。不需要一台单点 Manager,也不需要 SSH 免密去各节点捞 binlog。
-
靠多数派(quorum)防脑裂。少数派分区会变成不可写,而不是两边都能写。
-
应用不改连接串。不用 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单节点、主从、双主的构建方法 的单节点方式装好,不要去配主从
-
在所有机器的 hosts 中做好映射,Group Replication 内部通信依赖主机名解析
1 | vim /etc/hosts |
端口要放通
| 端口 | 用途 |
|---|---|
| 3306 | MySQL 客户端协议,Router 和 Shell 连这个 |
| 33060 | MySQL X 协议 |
| 33061 | Group Replication 组内通信,节点之间必须互通,这个最容易漏开 |
| 6446 / 6447 | Router 对应用暴露的读写 / 只读端口 |
2.前置条件(不满足后面必然失败)
InnoDB Cluster 用的就是 Group Replication,所以要求完全一致:
| 要求 | 说明 |
|---|---|
| 所有表都是 InnoDB | MyISAM / MEMORY 会导致组复制报错,先转换掉 |
| 所有表都有主键 | 组复制靠主键做冲突检测,没主键的表写不进去 |
| GTID 开启 | gtid_mode=ON、enforce_gtid_consistency=ON,这两个不是默认值,必须显式配 |
| binlog 为 ROW | 8.4 默认就是 ROW,一般不用动 |
| performance_schema 开启 | 状态监控依赖它,默认开启 |
| server_id 唯一 | 三个节点不能相同 |
| report_host 建议显式配 | 否则节点可能上报成一个别人连不上的主机名 |
-
把这些先写进每个节点的
/etc/my.cnf,其它参数 AdminAPI 会帮你配
1 | [mysqld] |
-
检查一下有没有不合规的表
1 | -- 非InnoDB的表 |
3.安装 MySQL Shell 和 MySQL Router
-
MySQL Shell 装在三个 MySQL 节点(或至少一台运维机器)上
1 | # 下载地址:https://dev.mysql.com/downloads/shell/ |
-
MySQL Router 装在应用服务器上(下一步再 bootstrap)
1 | # 下载地址:https://dev.mysql.com/downloads/router/ |
Shell 和 Router 要选和 Server 相同的系列(8.4 LTS),但小版本号不需要和 Server 一致:它们是独立发版的,Server 已经到 8.4.11 时 Shell / Router 的 8.4 系列可能还停在 8.4.10,各自取所在系列的最新版即可。
4.检查并配置实例
AdminAPI 提供两个命令:checkInstanceConfiguration() 只检查,configureInstance() 会真的去改配置。
-
先用 Shell 连上 node1,进 JS 模式
1 | mysqlsh root@node1:3306 |
-
检查当前实例是否满足要求
1 | MySQL node1:3306 ssl JS > dba.checkInstanceConfiguration('root@node1:3306') |
clusterAdmin 是什么账号
先说清楚这个账号的用途,否则下面的命令会一头雾水。
搭集群时你是用 root 连上去的,但 AdminAPI 之后要反复从一个节点去连另一个节点:addInstance() 要连新节点、setPrimaryInstance() 要连目标节点、status() 要挨个去问成员状态。它用的不是你当前的 root,而是这个专门的账号,官方叫**「服务器配置账号」(server configuration account)**。
它需要元数据表的完整读写权限,外加 SUPER、GRANT OPTION、CREATE、DROP 等一整套管理员权限。不用自己去 GRANT,configureInstance() 会自动授全。
配置实例并创建这个账号
-
不传密码,Shell 会交互式提示你输入,这一步会把 Group Replication 需要的参数写进配置文件并持久化
1 | MySQL node1:3306 ssl JS > dba.configureInstance('root@node1:3306', {clusterAdmin: "'icadmin'@'10.250.%'"}) |
-
'icadmin'@'10.250.%'是标准的 MySQL 账号格式:用户名 + 允许从哪些地址连。这里限定成内网段,比%安全 -
也可以把密码写在参数里,但不推荐,它会留在 Shell 的命令历史里
1 | // 图省事可以这么写,生产环境别这么干 |
-
默认会套用
default_password_lifetime,管理账号被动过期会让运维操作突然失败,可以显式指定不过期
1 | MySQL node1:3306 ssl JS > dba.configureInstance('root@node1:3306', {clusterAdmin: "'icadmin'@'10.250.%'", clusterAdminPasswordExpiration: 'NEVER'}) |
为什么三个节点都要执行一遍
-
三个节点都跑一次,用户名和密码必须完全一样(在 node1 上就能远程配,也可以分别登上去配)
1 | MySQL node1:3306 ssl JS > dba.configureInstance('root@node2:3306', {clusterAdmin: "'icadmin'@'10.250.%'"}) |
警告⚠️
这个账号不会自动同步到其它节点。 MySQL Shell 在执行 configureInstance() 时会关掉 binlog,所以建账号这个动作不进 binlog、也就不会被复制——必须在每个节点单独建一次。
而且此时集群还没建起来,三个节点之间本来就没有任何复制关系,指望它自己传过去是不可能的。
密码必须三个节点一致:AdminAPI 拿着你在当前会话里用的这套凭据去连其它节点,某个节点上的密码不一样,addInstance() 就会在那里认证失败。
和 setupAdminAccount() 的区别
后面第 10 节的 cluster.setupAdminAccount() 建的账号走 binlog、会自动复制到所有成员,所以那个只需要在一个节点上执行一次。
区别就在于执行时机:configureInstance() 跑在集群成立之前(无复制、且主动关了 binlog),setupAdminAccount() 跑在集群成立之后(有复制)。
如果提示需要重启(比如刚加的
gtid_mode),按提示重启对应节点的 mysqld 再重新执行检查。
-
建完可以验证一下账号确实在每个节点上都有
1 | mysql> SELECT user, host FROM mysql.user WHERE user = 'icadmin'; |
5.创建集群
-
用管理账号连上准备当第一个 Primary 的节点(种子节点)
1 | mysqlsh icadmin@node1:3306 |
-
创建集群
1 | MySQL node1:3306 ssl JS > var cluster = dba.createCluster('myCluster') |
-
此时只有一个节点,注意最后那句提示:要 3 个节点才能容忍一次故障
var cluster = ... 这个变量很重要
后面所有 cluster.xxx() 操作都靠它。如果 Shell 会话断了,重新连上后用 var cluster = dba.getCluster() 把集群对象取回来,不用重建集群。
6.加入其余节点
1 | MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node2:3306') |
-
加节点时 Shell 会问怎么同步已有数据,给三个选项:
| 方式 | 说明 |
|---|---|
| Clone(推荐) | 用 Clone 插件把种子节点的数据整份物理克隆过来,新节点原有数据会被清空 |
| Incremental recovery | 走分布式恢复,从其它成员的 binlog 增量补齐,要求 binlog 没被清理掉 |
| Abort | 放弃 |
-
新节点一般选 Clone,也可以直接在参数里指定,避免交互
1 | MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node2:3306', {recoveryMethod: 'clone'}) |
警告⚠️
选 Clone 会清空目标节点上的所有数据,用种子节点的数据覆盖。给新机器用没问题,往一台有数据的实例上加之前一定要确认清楚。
-
可以给节点起个易读的标签,便于后面运维时引用
1 | MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node3:3306', {recoveryMethod: 'clone', label: 'node3'}) |
7.查看集群状态
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 |
还能写,但再坏一个就完蛋,要赶紧修 |
OK_NO_TOLERANCE_PARTIAL |
同上,且有成员不在组里 |
NO_QUORUM |
失去多数派,不可写,只能读,需要人工介入 |
UNKNOWN |
你连的这个节点自己就不在线,换个节点连 |
-
要看更详细的信息(GTID 集、复制延迟、成员状态机)
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 |
8.部署 MySQL Router
集群自己会选主,但应用怎么知道新 Primary 是谁?这就是 Router 的活。
bootstrap
Router 不需要手写配置,用 --bootstrap 连一次集群,它会把拓扑信息抓下来自动生成配置。
-
在应用服务器上执行
1 | # 系统级配置,生成到 /etc/mysqlrouter/mysqlrouter.conf |
-
如果一台机器上要跑多个 Router 实例,用
--directory生成自包含目录
1 | sudo mysqlrouter --bootstrap icadmin@node1:3306 --directory /opt/myrouter --user=mysqlrouter |
这条命令里有三个不同的「用户」,别搞混
| 参数 | 是什么 | 密码 |
|---|---|---|
icadmin@node1:3306 |
MySQL 账号,就是第 4 节建的那个 clusterAdmin。bootstrap 这一次用它登录集群,读取拓扑、往元数据里注册自己 | 第 4 节 configureInstance() 时设的,bootstrap 会提示你输 |
--user=mysqlrouter |
操作系统账号,不是 MySQL 账号。mysqlrouter 进程平时以这个身份运行 | 没有密码,它是个不能登录的系统账号 |
--account=routeruser |
MySQL 账号,Router 长期运行时用它连集群、刷新拓扑 | bootstrap 过程中提示你输入并创建 |
--user:进程以哪个系统身份运行
官方原话是「a system login account, not a MySQL user listed in the grant tables」——它对应 /etc/passwd 里的用户,和 MySQL 权限表毫无关系。
-
用 RPM / DEB 包装 Router 时会自动建好这个名叫
mysqlrouter的系统用户;用 tar 包手工装的话要自己建
1 | # -r 建系统账号,-s /bin/false 禁止登录,所以它根本没有密码 |
-
它的作用有两个:bootstrap 生成的所有文件(配置、keyring)都归它所有;运行时进程降权成它,不用 root 跑
-
用 root 执行 bootstrap 时必须带
--user,否则直接报错
--account:Router 跑起来之后用哪个 MySQL 账号
bootstrap 用 icadmin 只是一次性的动作。做完之后 Router 要常驻,每隔几秒去刷新集群拓扑(谁是 Primary、谁掉线了),这时用的就是 --account 指定的账号。不能继续用 clusterAdmin——那是个近乎 root 的超级账号,而 Router 只需要读元数据的权限。
-
不写
--account会怎样:bootstrap 自动生成一个名字形如mysql_router1_a8kd0s的账号,配一个随机密码。省事,但一台机器 bootstrap 一次就多一个账号,久了mysql.user里全是这种垃圾 -
写了
--account:用户名你定,多个 Router 可以共用一个账号,便于管理。代价是要自己记住这个账号 -
Router 实际只需要这些权限,
--account-create建账号时会自动授好
1 | GRANT USAGE ON *.* TO `routeruser`@`%`; |
--account-create 的三个取值
| 取值 | 行为 |
|---|---|
if-not-exists(默认) |
账号存在就复用,不存在就创建。重复 bootstrap 不会失败,日常用这个 |
always |
只在账号不存在时才 bootstrap 并创建它;账号已存在会直接报错退出 |
never |
只在账号已存在时才 bootstrap 并复用;账号不存在会报错。适合账号由 DBA 预先建好的场景 |
警告⚠️
always 的语义是「确保这是个全新账号」,不是「不存在才建」——后者才是 if-not-exists。拿 always 去重跑 bootstrap(比如换了集群、想重新生成配置)会因为账号已存在而失败。
另外 --account-create 必须和 --account 一起用,且不能和 --account-host 同时出现。
用 --account 复用已存在的账号时要注意
bootstrap 不会给已存在的账号补权限,它只做检查,把缺失的权限打印到控制台,然后照样继续执行。结果就是 bootstrap 显示成功,Router 一跑起来却因为权限不足连不上元数据。
加 --strict 可以让权限检查不通过时直接失败退出,别让问题留到运行时。
密码是什么时候创建的,之后存在哪
bootstrap 过程中会出现两次密码交互:
1 | sudo mysqlrouter --bootstrap icadmin@node1:3306 --user=mysqlrouter \ |
-
第二个密码就是
routeruser的密码,在 bootstrap 这一刻才创建,不需要你提前建账号 -
只要带了
--account,每次 bootstrap 都会提示输密码,哪怕 keyring 里已经存了 -
输完之后你再也不用管它了:Router 把这个密码加密存进 keyring 文件,加密用的 master key 放在
mysqlrouter.key里
1 | # 系统级安装 |
-
所以
systemctl start mysqlrouter不用输任何密码,也不要去配置文件里找密码,明文密码不在里面
警告⚠️
mysqlrouter.key 是解开 keyring 的钥匙,它和 keyring 放在一起就等于把锁和钥匙挂在同一个钩子上。这两个文件必须只有 --user 指定的那个系统用户可读(包安装默认已经设好权限,别手工改宽)。安全要求高的场景用 --master-key-reader / --master-key-writer 接外部密钥管理,让 master key 不落盘。
Router 暴露的端口
| 端口 | 协议 | 去哪 |
|---|---|---|
| 6446 | classic | PRIMARY。可读可写,但所有语句都发给 Primary,不做读写分离 |
| 6447 | classic | SECONDARY,只读,轮询。发写操作会报错 |
| 6448 | X 协议 | PRIMARY |
| 6449 | X 协议 | SECONDARY |
| 6450 | classic | 读写分离端口,Router 解析事务类型自动分流(8.2 起默认生成,--disable-rw-split 可关掉) |
启动并设置开机自启
1 | sudo systemctl start mysqlrouter |
先建业务账号
前面建的 icadmin(管集群)和 routeruser(Router 自用)都不是给业务用的,业务账号要自己建。
-
只在 Primary 上执行一次,会经 binlog 自动复制到其它成员
1 | -- 连到当前Primary,或者直接连Router的6446 |
警告⚠️
host 段要写 Router 所在机器的地址,不是应用机器的地址。
Router 是代理,MySQL 看到的连接来源是 Router 的 IP,不是原始客户端的 IP(MySQL Server 和 Router 都不支持 Proxy Protocol)。上面例子里 Router 装在 app1(10.250.0.21),所以授权写的是 'appuser'@'10.250.0.21'。
如果按直觉写成应用容器的地址,会一直报 Access denied——而且日志里看到的来源 IP 是 Router 的,容易查半天。这也是官方推荐把 Router 和应用装同一台机器的原因之一:这样这个地址就是同一个,不用为 Router 单独放开一批授权。
-
要排查某个连接的真实客户端 IP,走 SSL 连接时 Router 会把它塞进连接属性里
1 | mysql> SELECT program_name, user, attr_value AS client_ip |
应用怎么连
1 | # 写流量(以及不方便拆分读写的场景)统一连6446 |
-
JDBC 连接串就是指向 Router,不再写任何数据库节点的 IP
1 | jdbc:mysql://127.0.0.1:6446/mydb?useSSL=true&autoReconnect=false |
连上 6446 之后,读写到底怎么走
这里有个很容易误解的地方:6446 叫「读写端口」,意思是「可读可写」,不是「自动读写分离」。
-
6446 上的所有 SQL——包括
SELECT——都发给 Primary,Secondary 一点读流量都分不到 -
Router 在 6446 / 6447 上不解析 SQL,它只是个连接级的转发器,按端口决定去哪,不看你发的是什么语句
-
所以连 6447 发写操作会直接报错(Secondary 上
super_read_only=ON),Router 不会帮你纠正
于是有两种用法:
| 做法 | 怎么用 | 适合 |
|---|---|---|
| 应用自己分流 | 写连 6446,读连 6447,业务代码或框架里配两个数据源 | 想精确控制哪些查询能容忍延迟 |
| 交给 Router 分流 | 统一连 6450,Router 按事务类型自动判断,读发 Secondary、写发 Primary | 不想改代码。8.2 起 bootstrap 默认就会生成这个端口 |
-
6450 还能按 session 临时改行为,比如强制这个会话全部走 Primary
1 | mysql> ROUTER SET access_mode='read_write'; |
写入是怎么到其它节点的
写进 Primary 之后会同步到另外两个节点,但机制和普通主从不一样,有个关键区别要知道:
-
提交时:事务要被多数派认证通过才会给客户端返回成功。所以返回成功意味着这个事务已经被多数节点确认,Primary 立刻宕机也不会丢
-
应用时:Secondary 把事务真正回放到自己的表里是异步的
所以「写完立刻从 6447 读」仍然可能读不到刚写的数据。这不是 bug,是组复制的正常行为。要避免就用上面的 wait_for_my_writes=1,或者这类查询直接走 6446。
顺带说个术语:InnoDB Cluster 里叫 Primary / Secondary,没有 master/slave 的说法。8.4 连 SQL 语句都改名了(
SHOW REPLICA STATUS之类),细节见 MySql8.4单节点、主从、双主的构建方法。
警告⚠️
Router 自己是单点。 官方推荐的部署方式是把 Router 和应用放在同一台机器上(每个应用实例一个 Router),这样 Router 挂了只影响这一个应用实例。不要全公司共用一台 Router;如果一定要集中部署,就多台 Router + 前面加 LB/VIP。
9.故障转移演练
-
先确认当前 Primary 是 node1,然后直接把 node1 的 mysqld 干掉
1 | # 在node1上 |
-
连到 node2 看集群状态
1 | MySQL node2:3306 ssl JS > var cluster = dba.getCluster() |
-
primary已经变成 node2,整个过程没有人工干预,通常在秒级完成 -
node1 的状态是
(MISSING),集群变成OK_NO_TOLERANCE_PARTIAL:还能写,但再坏一个就失去多数派 -
此时应用侧仍然连 6446,写操作照常成功
1 | mysql -uappuser -p -h127.0.0.1 -P6446 -e "select @@hostname, @@super_read_only;" |
警告⚠️
切主时已建立的连接会断开。 Router 只保证新连接被送到新 Primary,它不会把一个已经断掉的 TCP 会话「搬」过去,正在执行的事务会失败。应用必须能重连并重试,否则这一瞬间的请求照样报错。别拿实验室里测出来的秒数当 SLA:真实耗时取决于故障检测、选主、积压事务回放、Router 刷新元数据、客户端重试策略。
-
把 node1 修好后启动 mysqld,它一般会自动重新加入
1 | sudo systemctl start mysqld |
-
没自动加回来就手工 rejoin
1 | MySQL node2:3306 ssl JS > cluster.rejoinInstance('icadmin@node1:3306') |
注意 node1 回来之后是 SECONDARY,不会自动抢回 Primary。想让它当主得手工切(见下一节)。
10.日常运维命令
主动切主(计划内维护)
1 | // 把Primary切到node1,用于打补丁、重启前腾空某个节点 |
摘除 / 加回节点
1 | // 正常摘除 |
加只读副本(8.4 新能力)
8.4 的 AdminAPI 支持给集群挂异步只读副本:它不参与投票、不影响容错计算,纯粹用来扛读流量或跑报表。源节点挂了会自动切到别的成员。
1 | // 默认从Primary复制 |
只读副本会出现在
cluster.status()的readReplicas里,Router 也能把只读流量路由到它。
补建管理账号
1 | MySQL node1:3306 ssl JS > cluster.setupAdminAccount('icadmin2@10.250.%') |
查看集群配置项
1 | MySQL node1:3306 ssl JS > cluster.options() |
单主 / 多主切换
1 | MySQL node1:3306 ssl JS > cluster.switchToMultiPrimaryMode() |
警告⚠️
多主模式不是「性能更好的单主」。 多主下所有节点都能写,冲突靠事务认证阶段检测,撞了就回滚;而且多主不支持带级联约束的外键,还建议把隔离级别降到 READ COMMITTED。绝大多数业务应该老老实实用默认的单主模式。
11.故障恢复
场景一:失去多数派(NO_QUORUM)
3 节点挂了 2 个,剩下的这个凑不出多数派,集群不可写。这时需要人工告诉它「就用这个分区继续跑」:
1 | // 连到还活着的那个节点 |
警告⚠️
这个命令是强行重定义多数派,绕过了正常的投票保护。执行前必须确认另外那些节点真的挂了,而不只是网络不通。如果它们其实还活着并且在另一侧也接受写入,就会造成脑裂,两份数据后面无法自动合并。
场景二:所有节点都挂了(完全停机)
比如整个机房断电后重启,三个 mysqld 都起来了但组复制没起来:
1 | // 连到数据最新的那个节点(GTID最全) |
Shell 会检查你连的这个节点是不是 GTID 最全的那个,不是就报错。用
force跳过这个检查意味着主动放弃另一个节点上多出来的事务,要清楚代价。
场景三:某个节点数据分叉了
节点上有集群里不存在的事务(比如被人在 Secondary 上强行写过),它会拒绝加入。做法是摘掉再用 Clone 重灌:
1 | MySQL node1:3306 ssl JS > cluster.removeInstance('icadmin@node3:3306', {force: true}) |
12.几个必须知道的坑
从 Secondary 读可能读到旧数据
Secondary 是异步应用事务的,写完立刻去 6447 读,有可能读不到。这和 MySql8.4单节点、主从、双主的构建方法 里主从的读写分离是一样的问题。
8.4 在切主这一步做了保护:group_replication_consistency 的默认值从 8.0 的 EVENTUAL 改成了 BEFORE_ON_PRIMARY_FAILOVER。含义是新 Primary 在把旧 Primary 的积压事务回放完之前,不接受新的读写,避免应用切过去读到旧值。代价是切主耗时会随积压量增加。
1 | -- 查看当前级别 |
强一致读需求建议按 session 设置,不要全局拉到最高级别,否则整体延迟都会被拖高。
大事务会被拒绝
组复制要把事务在成员间达成共识,太大的事务会撑爆网络和内存。默认上限由 group_replication_transaction_size_limit 控制(默认 150MB),超了直接报错。
所以 DELETE FROM t WHERE ... 一把删掉几百万行这种操作在 InnoDB Cluster 上必须改成按主键分批、每批几千行单独提交。大表 DDL 同理,走 gh-ost 之类的影子表方案,别指望一条 ALTER 直接过去。
不能自己挂异步复制通道
InnoDB Cluster 不支持在成员上手工配置 AdminAPI 之外的复制通道。想挂只读副本用 addReplicaInstance(),不要自己去 CHANGE REPLICATION SOURCE TO。
节点数要奇数,且分故障域
前面说过 2 节点和 4 节点都没意义。另外三个节点如果都在同一台宿主机上,那这套高可用只防 mysqld 进程挂,不防机器挂。
跨机房要用 ClusterSet
Group Replication 要求成员之间网络延迟低,不适合跨地域拉伸。跨机房容灾用 InnoDB ClusterSet:一个主集群 + 一个或多个从集群,集群之间异步复制。注意 ClusterSet 的跨集群切换是管理员手动触发的,紧急切换还可能丢未复制的事务,它解决的是容灾,不是又一层自动 failover。
13.和其它方案对比
| 方案 | 自动选主 | 一致性 | 节点数 | 说明 |
|---|---|---|---|---|
| InnoDB Cluster | 是,组内投票 | 组复制认证,切主不丢已确认事务 | ≥3(奇数) | 官方方案,8.4 首选 |
| 主从 + 半同步 | 否 | 半同步缩小丢数窗口,超时降级异步 | ≥2 | 见 MySql8.4单节点、主从、双主的构建方法 |
| 主从 + MHA | 是 | 尽量补齐 binlog,极端情况丢数 | ≥3 | 8.4 上不可用,见 MySql-MHA的构建方法 |
| Percona XtraDB Cluster | 是 | Galera 准同步 | ≥3 | 非官方,对一致性要求更高时可选 |
| 云数据库多可用区 | 是 | 平台保证 | — | 能上云优先 |
InnoDB Cluster 凭什么能自动切主?应用要改什么?
场景:从一主一从 + 人工切换,改造成 8.4 上的自动高可用。
1. 它和「主从 + MHA」的本质区别在哪?
-
选主的位置不同。MHA 是外部 Perl 进程探测主库、再去各节点捞 binlog 补齐、然后提升从库;InnoDB Cluster 的选主做在 Group Replication 里,组内成员自己投票。
-
没有单点 Manager。MHA 的 Manager 本身是单点,还得给它做保活;InnoDB Cluster 不需要这个角色。
-
防脑裂机制不同。组复制要求多数派才能写,少数派分区自动变不可写;MHA 靠二次检查和脚本,判断错了就可能双写。
-
8.4 上 MHA 根本跑不起来:它发的
CHANGE MASTER TO、SHOW SLAVE STATUS已被删除。
2. 为什么至少要 3 个节点?2 个不行吗?
不行。要继续写必须有超过半数成员在线。2 节点时坏 1 个只剩 1 个,凑不出多数派,集群直接不可写——比单机还麻烦。3 节点能坏 1 个,5 节点能坏 2 个,所以节点数取奇数,并且要放在不同故障域。
3. 主库挂了,应用是怎么找到新主的?
-
组内选出新 Primary,新 Primary 关掉
super_read_only开始接写。 -
MySQL Router 读集群元数据,发现 Primary 换人,把 6446 端口的新连接指向新 Primary。
-
应用不用改连接串、不用 VIP 漂移脚本,连的一直是 Router。
-
但已有连接会断,正在跑的事务会失败,所以应用必须能重连重试。Router 建议贴着应用部署,它自己是单点。
4. 切主的瞬间会读到旧数据吗?
8.4 默认不会。group_replication_consistency 默认值在 8.4 是 BEFORE_ON_PRIMARY_FAILOVER(8.0 是 EVENTUAL):新 Primary 回放完旧 Primary 的积压事务之后才对外服务,避免读到旧值。代价是切主耗时随积压量增加。
但从 Secondary(6447)读仍然可能读到旧数据,那是正常的读写分离延迟,要强一致就走 6446 或按 session 设 group_replication_consistency='BEFORE'。
5. 上这套方案,业务要付出什么代价?
-
所有表必须是 InnoDB、必须有主键,否则写不进去。
-
事务要小,默认超过
group_replication_transaction_size_limit(150MB)直接报错,大批量删改要分批。 -
至少 3 台机器的成本;跨机房要用 ClusterSet,而且它的跨集群切换是手动的。
-
多主模式不要随便开:冲突回滚、不支持级联外键,默认单主才是常规选择。
面试可背:InnoDB Cluster = Group Replication 选主 + Shell 管理 + Router 路由;至少 3 节点奇数、多数派才可写;选主在组内不靠外部 Manager;应用只连 Router 的 6446/6447,切主时连接会断所以必须重试;8.4 默认
BEFORE_ON_PRIMARY_FAILOVER防止切主读到旧数据;前置条件是 InnoDB + 主键 + GTID,事务不能太大。