MySQL 8.4 InnoDB Cluster 的构建方法

摘要

  • MySQL 官方高可用方案 InnoDB Cluster 的搭建、故障转移演练与日常运维
  • 本文基于mysql-8.4.11 LTSmysql-shell-8.4.10mysql-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
2
3
4
5
node1:  10.250.0.11    MySQL 8.4.11 + MySQL Shell
node2: 10.250.0.12 MySQL 8.4.11 + MySQL Shell
node3: 10.250.0.13 MySQL 8.4.11 + MySQL Shell
app1: 10.250.0.21 应用服务器 + MySQL Router
app2: 10.250.0.22 应用服务器 + MySQL Router
  • 三台 MySQL 按 MySql8.4单节点、主从、双主的构建方法 的单节点方式装好,不要去配主从

  • 如果使用主机名,在所有机器上通过 DNS 或 /etc/hosts 保证主机名稳定、双向可解析;也可以直接使用互通的 IP

1
2
3
4
5
6
vim /etc/hosts
10.250.0.11 node1
10.250.0.12 node2
10.250.0.13 node3
10.250.0.21 app1
10.250.0.22 app2
  • 所有节点启用 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=ONenforce_gtid_consistency=ON,这两个不是默认值,必须显式配
binlog 为 ROW 8.4 默认就是 ROW,一般不用动
performance_schema 开启 状态监控依赖它,默认开启
server_id 唯一 三个节点不能相同
report_host 建议显式配 否则节点可能上报成一个别人连不上的主机名
没有未托管复制通道 成员上不要保留手工配置、未由 AdminAPI 管理的异步复制通道
lower_case_table_names 一致 所有成员必须相同;它通常只能在初始化数据目录时确定,不能指望 AdminAPI 上线后修正
没有全局复制过滤器 不要配置 binlog-do-dbreplicate-* 等过滤器,否则成员数据可能不一致
  • 把这些先写进每个节点的 /etc/my.cnf,其它参数 AdminAPI 会帮你配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[mysqld]
# 每个节点不同
server-id = 11
# 显式上报自己的地址,避免元数据里记成解析不到的主机名
report_host = node1

# 组复制必需,这两个不是默认值
gtid_mode = ON
enforce_gtid_consistency = ON

# 8.4默认就是ROW,这里不用配,写出来只是提醒不要改成statement
# binlog_format = ROW

# 禁止使用非InnoDB引擎,提前拦住MyISAM建表
disabled_storage_engines = "MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"

node2 改成 server-id=12report_host=node2,node3 改成 server-id=13report_host=node3,不能整段原样复制。lower_case_table_names 要在三台机器初始化数据目录之前统一决定;Amazon Linux 默认区分表名大小写,通常保持默认值 0

  • 检查一下有没有不合规的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 非InnoDB的表
SELECT table_schema, table_name, engine
FROM information_schema.tables
WHERE engine != 'InnoDB'
AND table_schema NOT IN ('mysql','information_schema','performance_schema','sys');

-- 没有主键,也没有“所有列均为NOT NULL的唯一键”的表
SELECT t.table_schema, t.table_name
FROM information_schema.tables AS t
WHERE t.table_type = 'BASE TABLE'
AND t.table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
AND NOT EXISTS (
SELECT 1
FROM information_schema.statistics AS s
WHERE s.table_schema = t.table_schema
AND s.table_name = t.table_name
AND s.non_unique = 0
GROUP BY s.index_name
HAVING SUM(CASE WHEN s.nullable = 'YES' THEN 1 ELSE 0 END) = 0
);

-- 三台必须返回相同值
SELECT @@lower_case_table_names;

-- 两张表均应为空;同时检查my.cnf里有没有binlog-do-db等启动参数
SELECT * FROM performance_schema.replication_applier_global_filters;
SELECT * FROM performance_schema.replication_applier_filters;

Group Replication 接受非空唯一键,但生产规范仍建议每张表显式定义主键,便于运维和避免复制行查找性能问题。

3.安装 MySQL Shell 和 MySQL Router

  • Amazon Linux 2023 先准备目录和下载工具

1
2
sudo dnf install -y wget tar xz gnupg2
sudo mkdir -p /usr/local/soft
  • MySQL Shell 装在三个 MySQL 节点(或至少一台运维机器)上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 下载地址:https://dev.mysql.com/downloads/shell/
wget https://cdn.mysql.com/Downloads/MySQL-Shell/mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz
wget https://cdn.mysql.com/Downloads/MySQL-Shell/mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz.asc

# 先从MySQL官方下载页导入并核对发布密钥指纹,再验证签名
gpg --verify mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz.asc \
mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz

tar -zxvf mysql-shell-8.4.10-linux-glibc2.28-x86-64bit.tar.gz
sudo mv mysql-shell-8.4.10-linux-glibc2.28-x86-64bit /usr/local/soft/mysqlsh

# 对所有登录用户加入PATH
echo 'export PATH=$PATH:/usr/local/soft/mysqlsh/bin' | sudo tee /etc/profile.d/mysqlsh.sh
source /etc/profile.d/mysqlsh.sh

mysqlsh --version
  • MySQL Router 装在每台应用服务器上(下一步再 bootstrap)。Amazon Linux 2023 x86_64 的 glibc 版本满足该 Generic 包要求;ARM 或其它系统应在下载页选择匹配平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 下载地址:https://dev.mysql.com/downloads/router/
wget https://cdn.mysql.com/Downloads/MySQL-Router/mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz
wget https://cdn.mysql.com/Downloads/MySQL-Router/mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz.asc

# MD5只能检查传输损坏,不能证明来源可信
echo '5a55cedd61215508f8c903b0d9816d64 mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz' | md5sum -c -
# 导入并核对MySQL官方发布密钥后,用签名验证来源
gpg --verify mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz.asc \
mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz

tar -Jxvf mysql-router-8.4.10-linux-glibc2.28-x86_64.tar.xz
sudo mv mysql-router-8.4.10-linux-glibc2.28-x86_64 /usr/local/soft/mysqlrouter

# 创建受限的运行组和账号;已存在时跳过
getent group mysqlrouter >/dev/null || sudo groupadd --system mysqlrouter
id mysqlrouter >/dev/null 2>&1 || sudo useradd --system --gid mysqlrouter \
--home-dir /nonexistent --shell /sbin/nologin mysqlrouter

# 方便后面直接使用mysqlrouter命令
sudo ln -sfn /usr/local/soft/mysqlrouter/bin/mysqlrouter /usr/local/bin/mysqlrouter
mysqlrouter --version
  • Amazon Linux 2023 的 yum 实际由 DNF 提供。配置 MySQL 官方仓库后也可用下面的方式安装,但版本取决于仓库当前状态

1
2
3
yum info mysql-router-community
sudo yum install -y mysql-router-community
mysqlrouter --version

本文后续的目录和 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
2
3
4
5
6
7
8
 MySQL  localhost:3306 ssl  JS > dba.checkInstanceConfiguration('root@localhost:3306')

Validating MySQL instance at node1:3306 for use in an InnoDB cluster...
This instance reports its own address as node1:3306
Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected
Checking instance configuration...
Instance configuration is compatible with InnoDB cluster

clusterAdmin 是什么账号

先说清楚这个账号的用途,否则下面的命令会一头雾水。

搭集群时你是用 root 连上去的,但 AdminAPI 之后要反复从一个节点去连另一个节点addInstance() 要连新节点、setPrimaryInstance() 要连目标节点、status() 要挨个去问成员状态。它用的不是你当前的 root,而是这个专门的账号,官方叫 「服务器配置账号」(server configuration account)

它需要元数据表的完整读写权限,外加 SUPERGRANT OPTIONCREATEDROP 等一整套管理员权限。不用自己去 GRANTconfigureInstance() 会自动授全。

配置实例并创建这个账号

  • 不传密码,Shell 会交互式提示你输入。该命令会持久化 Group Replication 所需配置,具体可能写入指定的 my.cnf,也可能通过 SET PERSIST 写入 mysqld-auto.cnf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 MySQL  localhost:3306 ssl  JS > dba.configureInstance('root@localhost:3306', {
clusterAdmin: "'icadmin'@'10.250.0.0/27'",
clusterAdminPasswordExpiration: 'NEVER'
})

Please provide the password for 'root@localhost:3306': ********

Configuring MySQL instance at node1:3306 for use in an InnoDB cluster...
This instance reports its own address as node1:3306

Missing the password for new account icadmin@10.250.0.0/27. Please provide one.
Password for new account: ********
Confirm password: ********

Creating user icadmin@10.250.0.0/27.
Account icadmin@10.250.0.0/27 was successfully created.

The instance 'node1:3306' is valid for InnoDB Cluster usage.
  • 'icadmin'@'10.250.0.0/27' 是标准的 MySQL 账号格式:用户名 + 允许连接的 IPv4 CIDR。这个范围仅覆盖本文的数据库与 Router 节点;实际环境应换成专用管理网段。MySQL 8.4 已弃用 Host 中的 %_ 通配写法

  • 也可以把密码写在参数里,但不推荐,它会留在 Shell 的命令历史里

1
2
// 图省事可以这么写,生产环境别这么干
MySQL localhost:3306 ssl JS > dba.configureInstance('root@localhost:3306', {clusterAdmin: "'icadmin'@'10.250.0.0/27'", clusterAdminPassword: '<STRONG_PASSWORD>', clusterAdminPasswordExpiration: 'NEVER'})
  • clusterAdminPasswordExpiration:'NEVER' 要在账号首次创建时传入;已有账号要改成永不过期,使用 ALTER USER 'icadmin'@'10.250.0.0/27' PASSWORD EXPIRE NEVER

为什么三个节点都要执行一遍

分别登录 node2、node3,在各自机器上连接 root@localhost:3306,使用与 node1 完全相同的 icadmin 密码执行:

1
2
3
4
5
6
7
8
MySQL  localhost:3306 ssl  JS > dba.configureInstance('root@localhost:3306', {
clusterAdmin: "'icadmin'@'10.250.0.0/27'",
clusterAdminPasswordExpiration: 'NEVER'
})
MySQL localhost:3306 ssl JS > dba.configureInstance('root@localhost:3306', {
clusterAdmin: "'icadmin'@'10.250.0.0/27'",
clusterAdminPasswordExpiration: 'NEVER'
})

警告⚠️
这个账号不会自动同步到其它节点。 MySQL Shell 在执行 configureInstance() 时会关掉 binlog,所以建账号这个动作不进 binlog、也就不会被复制——必须在每个节点单独建一次。

而且此时集群还没建起来,三个节点之间本来就没有任何复制关系,指望它自己传过去是不可能的。

密码必须三个节点一致:AdminAPI 拿着你在当前会话里用的这套凭据去连其它节点,某个节点上的密码不一样,addInstance() 就会在那里认证失败。

setupAdminAccount() 的区别
后面第 11 节的 cluster.setupAdminAccount() 建的账号走 binlog、会自动复制到所有成员,所以那个只需要在一个节点上执行一次。
区别就在于执行时机:configureInstance() 跑在集群成立之前(无复制、且主动关了 binlog),setupAdminAccount() 跑在集群成立之后(有复制)。

如果提示需要重启(比如刚加的 gtid_mode),按提示重启对应节点的 mysqld 再重新执行检查。

  • 建完可以验证一下账号确实在每个节点上都有

1
2
3
4
5
6
mysql> SELECT user, host FROM mysql.user WHERE user = 'icadmin';
+---------+-----------+
| user | host |
+---------+-----------+
| icadmin | 10.250.0.0/27 |
+---------+-----------+

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sudo dnf install -y openssl
mkdir -p ~/mysql-tls/{ca,nodes,router} && cd ~/mysql-tls

# 显式声明CA用途,避免依赖不同系统的openssl.cnf默认值
cat > ca/ca.cnf <<'EOF'
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no

[req_distinguished_name]
CN = MySQL Lab CA

[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:TRUE
keyUsage = critical,keyCertSign,cRLSign
EOF

# 自签CA,有效期10年;CA私钥不能分发到任何节点
umask 077
openssl genrsa -out ca/ca-key.pem 4096
openssl req -new -x509 -sha256 -days 3650 -key ca/ca-key.pem -out ca/ca.pem \
-config ca/ca.cnf
chmod 600 ca/ca-key.pem

# 确认CA:TRUE和签名用途
openssl x509 -in ca/ca.pem -noout -subject -dates \
-ext basicConstraints -ext keyUsage

为每个 MySQL 节点签发证书

证书 SAN 必须包含客户端实际用来连接的主机名或 IP。本文成员之间使用主机名 node1 / node2 / node3,所以每张节点证书至少包含自己的 DNS 名和对应 IP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 一次生成三套独立文件,避免复制命令时覆盖node1证书
for item in \
"node1 10.250.0.11" \
"node2 10.250.0.12" \
"node3 10.250.0.13"
do
read -r name ip <<< "$item"

cat > "nodes/${name}.cnf" <<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = ${name}

[v3_req]
basicConstraints = critical,CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = ${name}
IP.1 = ${ip}
EOF

openssl genrsa -out "nodes/${name}-key.pem" 2048
openssl req -new -key "nodes/${name}-key.pem" \
-out "nodes/${name}.csr" -config "nodes/${name}.cnf"
openssl x509 -req -in "nodes/${name}.csr" \
-sha256 -CA ca/ca.pem -CAkey ca/ca-key.pem -CAcreateserial \
-out "nodes/${name}-cert.pem" -days 825 \
-extensions v3_req -extfile "nodes/${name}.cnf"
done
  • 验证签发链、SAN、有效期以及证书和私钥是否匹配

1
2
3
4
5
6
7
8
9
for name in node1 node2 node3
do
openssl verify -CAfile ca/ca.pem "nodes/${name}-cert.pem"
openssl x509 -in "nodes/${name}-cert.pem" \
-noout -subject -dates -ext subjectAltName
cmp <(openssl pkey -in "nodes/${name}-key.pem" -pubout) \
<(openssl x509 -in "nodes/${name}-cert.pem" -pubkey -noout) \
&& echo "${name}: certificate and key match"
done

为 Router 签发证书

本文应用连 127.0.0.1,证书 SAN 至少包含该地址;若还要用主机名访问,一并写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# app1、app2分别生成独立文件,分发时再统一改成Router使用的文件名
for item in \
"app1 10.250.0.21" \
"app2 10.250.0.22"
do
read -r name ip <<< "$item"

cat > "router/${name}.cnf" <<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = ${name}

[v3_req]
basicConstraints = critical,CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = ${name}
IP.1 = 127.0.0.1
IP.2 = ${ip}
EOF

openssl genrsa -out "router/${name}-key.pem" 2048
openssl req -new -key "router/${name}-key.pem" \
-out "router/${name}.csr" -config "router/${name}.cnf"
openssl x509 -req -in "router/${name}.csr" \
-sha256 -CA ca/ca.pem -CAkey ca/ca-key.pem -CAcreateserial \
-out "router/${name}-cert.pem" -days 825 \
-extensions v3_req -extfile "router/${name}.cnf"
done
  • 同样进行验证

1
2
3
4
5
6
7
8
9
for name in app1 app2
do
openssl verify -CAfile ca/ca.pem "router/${name}-cert.pem"
openssl x509 -in "router/${name}-cert.pem" \
-noout -subject -dates -ext subjectAltName
cmp <(openssl pkey -in "router/${name}-key.pem" -pubout) \
<(openssl x509 -in "router/${name}-cert.pem" -pubkey -noout) \
&& echo "${name}: certificate and key match"
done

分发到各机器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# MySQL节点:源文件名和目标节点一一对应
install_mysql_cert() {
host="$1"
ssh "$host" 'sudo install -d -o root -g mysql -m 750 /etc/mysql/tls'
scp ca/ca.pem "nodes/${host}-cert.pem" "nodes/${host}-key.pem" "${host}:/tmp/"
ssh "$host" \
"sudo install -o root -g mysql -m 644 /tmp/ca.pem /etc/mysql/tls/ca.pem &&
sudo install -o root -g mysql -m 644 /tmp/${host}-cert.pem /etc/mysql/tls/${host}-cert.pem &&
sudo install -o root -g mysql -m 640 /tmp/${host}-key.pem /etc/mysql/tls/${host}-key.pem &&
rm -f /tmp/ca.pem /tmp/${host}-cert.pem /tmp/${host}-key.pem"
}

install_mysql_cert node1
install_mysql_cert node2
install_mysql_cert node3

# Router节点:各自证书在目标机统一命名为router-cert.pem/router-key.pem
install_router_cert() {
host="$1"
ssh "$host" 'sudo install -d -o root -g mysqlrouter -m 750 /etc/mysqlrouter/tls'
scp ca/ca.pem "router/${host}-cert.pem" "router/${host}-key.pem" "${host}:/tmp/"
ssh "$host" \
"sudo install -o root -g mysqlrouter -m 640 /tmp/ca.pem /etc/mysqlrouter/tls/ca.pem &&
sudo install -o root -g mysqlrouter -m 640 /tmp/${host}-cert.pem /etc/mysqlrouter/tls/router-cert.pem &&
sudo install -o root -g mysqlrouter -m 640 /tmp/${host}-key.pem /etc/mysqlrouter/tls/router-key.pem &&
rm -f /tmp/ca.pem /tmp/${host}-cert.pem /tmp/${host}-key.pem"
}

install_router_cert app1
install_router_cert app2

CA 私钥 ca/ca-key.pem 只保留在受控运维机或离线密钥库,绝不能复制到 MySQL 或 Router 节点。

实验环境怎么选

  • 只想尽快跑通集群:跳过自建证书,使用 MySQL 初始化时自动生成的证书以及默认 AUTO / PREFERRED。这通常是加密连接,但不验证对端身份,并不等于明文传输。
  • 准备上生产:先完成本节,再建集群时使用 memberSslMode:'VERIFY_IDENTITY',Router bootstrap 也带上 TLS 参数。
  • memberSslModecreateCluster() 时确定,上线后再补身份校验成本很高,生产务必一次做对。

6.创建集群

实验环境:不自建证书

内网学习可直接创建。MySQL 初始化通常已经生成 ca.pemserver-cert.pem 等自动证书;memberSslMode:'AUTO' 会在实例支持 TLS 时启用加密,但不校验证书中的主机身份。不要把它表述成“关闭 TLS”。

  • 用管理账号连上准备当第一个 Primary 的节点(种子节点)

1
mysqlsh icadmin@node1:3306
  • 创建集群。显式写出默认的 MYSQL 通信栈,并把 AdminAPI 内部恢复账号限制在集群网段

1
2
3
4
5
MySQL  node1:3306 ssl  JS > var cluster = dba.createCluster('myCluster', {
communicationStack: 'mysql',
memberSslMode: 'AUTO',
replicationAllowedHost: '10.250.0.0/28'
})

生产环境:启用成员 TLS

先把第 5 节生成的证书配进每个节点的 my.cnf,然后重启 mysqld:

1
2
3
4
5
6
7
[mysqld]
require_secure_transport = ON
ssl_ca = /etc/mysql/tls/ca.pem
# 每台机器必须使用自己的文件;下面以node1为例,
# node2/node3分别改为node2-*和node3-*
ssl_cert = /etc/mysql/tls/node1-cert.pem
ssl_key = /etc/mysql/tls/node1-key.pem

确认主监听通道已加载证书;MySQL 8.4 不再使用 have_ssl 变量:

1
2
3
4
5
6
7
mysql> SELECT CHANNEL, PROPERTY, VALUE
FROM performance_schema.tls_channel_status
WHERE CHANNEL = 'mysql_main';

mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.session_status
WHERE VARIABLE_NAME IN ('Ssl_version', 'Ssl_cipher');
  • 用管理账号连上种子节点,并校验服务端身份

1
mysqlsh --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/mysql/tls/ca.pem icadmin@node1:3306
  • 创建集群时显式开启成员身份校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 MySQL  node1:3306 ssl  JS > var cluster = dba.createCluster('myCluster', {
communicationStack: 'mysql',
memberSslMode: 'VERIFY_IDENTITY',
replicationAllowedHost: '10.250.0.0/28'
})

A new InnoDB Cluster will be created on instance 'node1:3306'.

Validating instance configuration at node1:3306...
This instance reports its own address as node1:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'node1:3306'.
Creating InnoDB Cluster 'myCluster' on 'node1:3306'...

Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.
  • 此时只有一个节点,注意最后那句提示:要 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
2
MySQL  node1:3306 ssl  JS > cluster.addInstance('icadmin@node2:3306', {recoveryMethod: 'clone', label: 'node2'})
MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node3:3306', {recoveryMethod: 'clone', label: 'node3'})

警告⚠️
选 Clone 会清空目标节点上的所有数据,用 donor 的数据覆盖。Clone 插件通常会自动重启接收实例,AdminAPI 等待它恢复并继续配置;如果当前启动方式不支持由 MySQL 发起重启,实例会关闭,需要手工启动 mysqld 后让 AdminAPI 继续或重新执行。给新机器用没问题,往一台有数据的实例上加之前一定要确认清楚,同时检查目标磁盘空间和 donor 的 I/O、网络压力。需要固定来源时使用 cloneDonor:'node2:3306'

8.查看集群状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 MySQL  node1:3306 ssl  JS > cluster.status()
{
"clusterName": "myCluster",
"defaultReplicaSet": {
"primary": "node1:3306",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"node1:3306": {
"memberRole": "PRIMARY",
"mode": "R/W",
"status": "ONLINE"
},
"node2": {
"address": "node2:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"status": "ONLINE"
},
"node3": {
"address": "node3:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"status": "ONLINE"
}
},
"topologyMode": "Single-Primary"
}
}
  • 重点看三个字段: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 改为 readall。特别是 all 允许失去 quorum 的分区继续写入,会削弱防脑裂保护。

  • extended:1 查看组协议、成员角色和 fenced 变量;GTID、applier、连接状态等事务细节使用 extended:23

1
2
MySQL  node1:3306 ssl  JS > cluster.status({extended: 1})
MySQL node1:3306 ssl JS > cluster.status({extended: 2})
  • 也可以直接查 performance_schema,不依赖 Shell

1
2
3
4
5
6
7
8
9
mysql> SELECT MEMBER_ID, MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE, MEMBER_VERSION
FROM performance_schema.replication_group_members;
+--------------------------------------+-------------+-------------+--------------+-------------+----------------+
| MEMBER_ID | MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE | MEMBER_VERSION |
+--------------------------------------+-------------+-------------+--------------+-------------+----------------+
| 8a1b... | node1 | 3306 | ONLINE | PRIMARY | 8.4.11 |
| 9c2d... | node2 | 3306 | ONLINE | SECONDARY | 8.4.11 |
| ae3f... | node3 | 3306 | ONLINE | SECONDARY | 8.4.11 |
+--------------------------------------+-------------+-------------+--------------+-------------+----------------+

9.部署 MySQL Router

集群自己会选主,但应用怎么知道新 Primary 是谁?这就是 Router 的活。

bootstrap

Router 不需要手写配置,用 --bootstrap 连一次集群,它会把拓扑信息抓下来自动生成配置。

  • 先在集群上为 app1 预建 Router 专用账号,避免 bootstrap 创建 routeruser@'%'

1
2
3
MySQL  node1:3306 ssl  JS > cluster.setupRouterAccount('routeruser@10.250.0.21', {
passwordExpiration: 'NEVER'
})

Shell 会提示设置密码,该操作经 Group Replication 复制到全部成员。部署 app2 时再创建 routeruser@10.250.0.22

实验环境:不自建证书的 bootstrap

1
2
3
4
5
6
7
8
9
10
# tar 包不会预置 /etc/mysqlrouter 等目录,本文用 --directory 生成自包含实例
# 目标目录必须尚不存在,bootstrap 会自动创建
sudo mysqlrouter --bootstrap icadmin@node1:3306 \
--directory /usr/local/soft/mysqlrouter-instance \
--user=mysqlrouter \
--account=routeruser --account-create=never \
--ssl-mode=PREFERRED \
--client-ssl-mode=PREFERRED \
--server-ssl-mode=AS_CLIENT \
--disable-rest --strict --conf-bind-address=127.0.0.1

Router 会自动生成自签证书供客户端可选加密使用;上面显式写出的都是默认模式,便于说明实验流量通常仍会加密,但不验证对端身份。生产不要依赖这份自动证书做身份校验。

生产环境:启用 TLS 的 bootstrap

先把第 5 节的 CA 与 Router 证书放到 /etc/mysqlrouter/tls/(若前面已分发可跳过):

1
2
3
4
5
sudo mkdir -p /etc/mysqlrouter/tls
# 将ca.pem、router-cert.pem、router-key.pem安全复制到该目录
sudo chown -R root:mysqlrouter /etc/mysqlrouter/tls
sudo chmod 750 /etc/mysqlrouter/tls
sudo chmod 640 /etc/mysqlrouter/tls/*

然后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# tar 包不会预置 /etc/mysqlrouter 等目录,本文用 --directory 生成自包含实例
# 目标目录必须尚不存在,bootstrap 会自动创建
sudo mysqlrouter --bootstrap icadmin@node1:3306 \
--directory /usr/local/soft/mysqlrouter-instance \
--user=mysqlrouter \
--account=routeruser --account-create=never \
--ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/mysqlrouter/tls/ca.pem \
--client-ssl-mode=REQUIRED \
--client-ssl-cert=/etc/mysqlrouter/tls/router-cert.pem \
--client-ssl-key=/etc/mysqlrouter/tls/router-key.pem \
--server-ssl-mode=REQUIRED \
--server-ssl-verify=VERIFY_IDENTITY \
--server-ssl-ca=/etc/mysqlrouter/tls/ca.pem \
--disable-rest --strict --conf-bind-address=127.0.0.1

# 输出大致如下
# MySQL Router configured for the InnoDB Cluster 'myCluster'
#
# After this MySQL Router has been started with the generated configuration
# $ /usr/local/soft/mysqlrouter-instance/start.sh
#
# InnoDB Cluster 'myCluster' can be reached by connecting to:
#
# ## 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

--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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sudo tee /etc/systemd/system/mysqlrouter.service >/dev/null <<'EOF'
[Unit]
Description=MySQL Router
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=mysqlrouter
Group=mysqlrouter
ExecStart=/usr/local/soft/mysqlrouter/bin/mysqlrouter -c /usr/local/soft/mysqlrouter-instance/mysqlrouter.conf
Restart=on-failure
RestartSec=5
LimitNOFILE=10000

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start mysqlrouter
sudo systemctl enable mysqlrouter
sudo systemctl status mysqlrouter

先建业务账号

前面建的 icadmin(管集群)和 routeruser(Router 自用)都不是给业务用的,业务账号要自己建。

  • 只在 Primary 上执行一次,会经 binlog 自动复制到其它成员

1
2
3
4
5
6
-- 连到当前Primary,或者直接连Router的6446
mysql> CREATE USER 'appuser'@'10.250.0.21' IDENTIFIED BY '<STRONG_PASSWORD>';
mysql> GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON mydb.* TO 'appuser'@'10.250.0.21';

mysql> CREATE USER 'appuser'@'10.250.0.22' IDENTIFIED BY '<SAME_STRONG_PASSWORD>';
mysql> GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON mydb.* TO 'appuser'@'10.250.0.22';

警告⚠️
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
2
3
4
mysql> SELECT program_name, user, attr_value AS client_ip
FROM performance_schema.session_connect_attrs
JOIN sys.processlist ON conn_id = processlist_id
WHERE attr_name = '_client_ip';

应用怎么连

1
2
3
4
5
6
7
8
9
10
11
# 实验环境:默认PREFERRED,通常加密但不验证Router身份
# 写流量(以及不方便拆分读写的场景)统一连6446
mysql -uappuser -p -h127.0.0.1 -P6446

# 只读流量连6447
mysql -uappuser -p -h127.0.0.1 -P6447

# 生产环境:信任自建CA并校验Router证书中的127.0.0.1 SAN
mysql --ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysqlrouter/tls/ca.pem \
-uappuser -p -h127.0.0.1 -P6446
  • JDBC 连接串同样只指向 Router,不再写任何数据库节点的 IP

1
2
3
4
5
# 实验环境
jdbc:mysql://127.0.0.1:6446/mydb

# 生产环境
jdbc:mysql://127.0.0.1:6446/mydb?sslMode=VERIFY_IDENTITY&trustCertificateKeyStoreUrl=file:/etc/myapp/mysql-truststore.p12&trustCertificateKeyStoreType=PKCS12&trustCertificateKeyStorePassword=<URL_ENCODED_PASSWORD>

把第 5 节的 CA 导入应用专用 truststore:

1
2
3
4
5
6
7
8
9
sudo install -d -o app -g app -m 750 /etc/myapp
sudo keytool -importcert -noprompt \
-alias mysql-innodb-cluster-ca \
-file /etc/mysqlrouter/tls/ca.pem \
-keystore /etc/myapp/mysql-truststore.p12 \
-storetype PKCS12 \
-storepass '<TRUSTSTORE_PASSWORD>'
sudo chown app:app /etc/myapp/mysql-truststore.p12
sudo chmod 640 /etc/myapp/mysql-truststore.p12

把示例中的 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 至少需要修改连接端口,而且不能直接假设“零改造”,相关特性要分开看:

  • 明确路由到 PrimaryCALL、DDL、DML、事务和锁语句、账号及大部分管理语句

  • access_mode=auto 不支持:复制语句,以及 Router 无法归类的部分管理语句;执行时会直接报错

  • 可以执行但影响连接共享:临时表、用户变量、预处理语句、GET_LOCK() 等会让后端连接暂时不能进入共享池;部分依赖前序会话状态的语句必须放在事务中

上线前应使用真实驱动和 SQL 集做回归测试,不能只验证普通 SELECTINSERT

  • 6450 可以按 session 临时改行为,比如强制这个会话全部走 Primary

1
2
3
4
mysql> ROUTER SET access_mode='read_write';

-- Router 8.4默认已经是1;这里只演示显式设置
mysql> ROUTER SET wait_for_my_writes=1;

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
2
3
4
5
6
# 在node1上
# 计划内正常停止:
# sudo systemctl stop mysqld

# 非计划进程故障:
sudo kill -9 $(pidof mysqld)
  • 连到 node2 看集群状态

1
2
3
4
5
# 实验环境
mysqlsh icadmin@node2:3306

# 生产环境
mysqlsh --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/mysql/tls/ca.pem icadmin@node2:3306
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 MySQL  node2:3306 ssl  JS > var cluster = dba.getCluster()
MySQL node2:3306 ssl JS > cluster.status()
{
"clusterName": "myCluster",
"defaultReplicaSet": {
"primary": "node2:3306",
"status": "OK_NO_TOLERANCE_PARTIAL",
"statusText": "Cluster is NOT tolerant to any failures. 1 member is not active.",
"topology": {
"node1:3306": {
"mode": "n/a",
"status": "(MISSING)"
},
"node2": {
"address": "node2:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"status": "ONLINE"
},
"node3": {
"address": "node3:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"status": "ONLINE"
}
}
}
}
  • primary 已经变成 node2,整个过程没有人工干预,通常在秒级完成

  • node1 的状态是 (MISSING),集群变成 OK_NO_TOLERANCE_PARTIAL:还能写,但再坏一个就失去多数派

  • 应用连接地址仍是本机 6446;原连接会断开,但客户端重连或连接池重试后,新请求会被送到 node2

1
2
3
4
5
6
mysql -uappuser -p -h127.0.0.1 -P6446 -e "select @@hostname, @@super_read_only;"
+------------+-------------------+
| @@hostname | @@super_read_only |
+------------+-------------------+
| node2 | 0 |
+------------+-------------------+

警告⚠️
切主时已建立的连接会断开。 Router 只保证新连接被送到新 Primary,它不会把一个已经断掉的 TCP 会话「搬」过去。连接中断时还可能出现“提交结果未知”:服务端也许已经提交,只是响应没送回来。因此不能无条件重放写事务,订单、支付等操作必须使用请求 ID、唯一约束或业务去重保证幂等。

别拿实验室里测出来的秒数当 SLA:真实耗时取决于故障检测、选主、积压事务回放、Router 刷新元数据和客户端重试策略。

  • 把 node1 修好后启动 mysqld,它一般会自动重新加入

1
sudo systemctl start mysqld
  • 没自动加回来就手工 rejoin

1
2
MySQL  node2:3306 ssl  JS > cluster.rejoinInstance('icadmin@node1:3306')
MySQL node2:3306 ssl JS > cluster.status()

注意 node1 回来之后是 SECONDARY,不会自动抢回 Primary。想让它当主得手工切(见下一节)。

11.日常运维命令

主动切主(计划内维护)

1
2
3
4
5
// 把Primary切到node1,用于打补丁、重启前腾空某个节点
MySQL node2:3306 ssl JS > cluster.setPrimaryInstance(
'node1:3306',
{runningTransactionsTimeout: 60}
)

runningTransactionsTimeout 的有效范围是 0–3600 秒。设置后,AdminAPI 最多等待切换开始时仍在运行的事务 60 秒,并拒绝新的事务进入;不设置则没有等待上限,期间还可能继续进入新事务。超时时先排查或终止长事务,不要直接把计划切主当作故障切换。

摘除 / 加回节点

1
2
3
4
5
6
7
8
// 正常摘除
MySQL node1:3306 ssl JS > cluster.removeInstance('icadmin@node3:3306')

// 节点已经连不上了,强制从元数据里摘掉
MySQL node1:3306 ssl JS > cluster.removeInstance('icadmin@node3:3306', {force: true})

// 加回来
MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node3:3306', {recoveryMethod: 'clone'})

force:true 只会强制清理元数据,不能让不可达节点停止运行。执行前必须确认旧节点已经关机或完成网络/存储 fencing;重新使用时检查 GTID,必要时用 Clone 重建。

加只读副本(8.1 首次引入,8.4 LTS 提供)

Read Replica 是异步只读副本:不参与投票、不影响容错计算,适合承载报表或额外读流量。添加前要像普通成员一样配置实例和管理账号,并确认没有未由 AdminAPI 管理的复制通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 登录node4,在本机交互输入与前三个节点相同的icadmin密码
MySQL localhost:3306 ssl JS > dba.configureInstance('root@localhost:3306', {
clusterAdmin: "'icadmin'@'10.250.0.0/27'",
clusterAdminPasswordExpiration: 'NEVER'
})

// 新节点明确使用Clone;指定优先从Secondary复制,源失效后自动选择其它Secondary
MySQL node1:3306 ssl JS > cluster.addReplicaInstance('icadmin@node4:3306', {
label: 'report1',
recoveryMethod: 'clone',
replicationSources: 'secondary'
})

// Read Replica离线且可用来源已经恢复时,通常直接重新加入
MySQL node1:3306 ssl JS > cluster.rejoinInstance('icadmin@node4:3306')

// 只有原来使用显式来源列表且列表已失效时,才先换成新的来源
MySQL node1:3306 ssl JS > cluster.setInstanceOption(
'node4:3306',
'replicationSources',
['node2:3306', 'node3:3306']
)
MySQL node1:3306 ssl JS > cluster.rejoinInstance('icadmin@node4:3306')

只读副本会出现在 cluster.status()readReplicas 里,但 Router 默认只把只读流量发给 Secondary。要让指定 Router 使用 Read Replica:

1
2
3
// 先用cluster.listRouters()确认名称
MySQL node1:3306 ssl JS > cluster.setRoutingOption('app1::system', 'read_only_targets', 'all')
// all:Secondary + Read Replica;read_replicas:只使用Read Replica

补建管理账号

1
2
3
MySQL  node1:3306 ssl  JS > cluster.setupAdminAccount('icadmin2@10.250.0.0/27', {
passwordExpiration: 'NEVER'
})

查看集群配置项

1
MySQL  node1:3306 ssl  JS > cluster.options()

单主 / 多主切换

1
2
MySQL  node1:3306 ssl  JS > cluster.switchToMultiPrimaryMode()
MySQL node1:3306 ssl JS > cluster.switchToSinglePrimaryMode('node1:3306')

警告⚠️
多主模式不是「性能更好的单主」。 多主下所有节点都能写,冲突靠事务认证阶段检测,撞了就回滚;多级外键依赖并带 CASCADE 操作的表结构不受支持,DDL 与同一对象的 DML 还必须在同一节点协调执行。多主模式通常建议使用 READ COMMITTED,绝大多数业务应优先采用默认单主模式。

12.故障恢复

场景一:失去多数派(NO_QUORUM)

3 节点挂了 2 个,剩下的这个凑不出多数派,集群不可写。这时需要人工告诉它「就用这个分区继续跑」:

1
2
3
// 连到还活着的那个节点
MySQL node1:3306 ssl JS > var cluster = dba.getCluster()
MySQL node1:3306 ssl JS > cluster.forceQuorumUsingPartitionOf('icadmin@node1:3306')

警告⚠️
这个命令是强行重定义成员关系,绕过正常投票保护。执行前必须把被排除节点关机、隔离或 fence,不能只凭“连不上”判断。恢复后,旧成员必须通过 remove/rejoin 或 Clone 回到新成员关系,不能让旧分区自行恢复运行,否则可能形成脑裂。

场景二:所有节点都挂了(完全停机)

比如整个机房断电后重启,三个 mysqld 都起来了但组复制没起来。不要只比较 gtid_executed 后就凭感觉选节点;AdminAPI 还会检查 Group Replication 已认证但尚未应用的事务。先连接候选节点并做演练检查:

1
2
3
4
MySQL  node1:3306 ssl  JS > dba.rebootClusterFromCompleteOutage(
'myCluster',
{dryRun: true}
)
  • 默认恢复:Shell 会比较所有可达成员,并要求当前连接的节点具有完整事务集

1
MySQL  node1:3306 ssl  JS > var cluster = dba.rebootClusterFromCompleteOutage('myCluster')
  • 指定 Primary:指定节点默认仍必须拥有 GTID 超集,不能仅为了偏好而选择落后节点

1
2
3
4
MySQL  node1:3306 ssl  JS > var cluster = dba.rebootClusterFromCompleteOutage(
'myCluster',
{primary: 'node2:3306'}
)
  • 强制恢复:仅在成员确实无法恢复且已隔离时使用;它也允许选择较旧或分叉的事务集

1
2
3
4
5
MySQL  node1:3306 ssl  JS > var cluster = dba.rebootClusterFromCompleteOutage(
'myCluster',
{force: true}
)
MySQL node1:3306 ssl JS > cluster.rejoinInstance('icadmin@node3:3306')

force:true 用于忽略不可达成员时不必然丢数据,但若接受了较旧或分叉的事务集,会放弃所选节点不包含的事务。强制恢复后,缺失成员未必能直接 rejoinInstance(),GTID 分叉时要用 Clone 重建。

场景三:某个节点数据分叉了

节点上有集群里不存在的事务(比如被人在 Secondary 上强行写过),它会拒绝加入。做法是摘掉再用 Clone 重灌:

1
2
MySQL  node1:3306 ssl  JS > cluster.removeInstance('icadmin@node3:3306', {force: true})
MySQL node1:3306 ssl JS > cluster.addInstance('icadmin@node3:3306', {recoveryMethod: 'clone'})

13.几个必须知道的坑

切主时的一致性保护

第 9 节已经说明直接读取 Secondary 可能得到旧数据。MySQL 8.4 在切主时另有一层保护:group_replication_consistency 默认是 BEFORE_ON_PRIMARY_FAILOVER。新 Primary 在回放完旧 Primary 的积压事务之前不会接受新的读写,避免应用切过去读到旧值;代价是切主时间会随积压量增加。

1
2
3
4
5
-- 查看当前级别
mysql> SELECT @@group_replication_consistency;

-- 要求「读也必须看到之前所有已提交事务」,代价是读延迟上升
mysql> SET SESSION group_replication_consistency = 'BEFORE';

强一致读需求建议按 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
2
3
4
5
SELECT MEMBER_ID, COUNT_TRANSACTIONS_IN_QUEUE,
COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE,
COUNT_TRANSACTIONS_CHECKED,
COUNT_CONFLICTS_DETECTED
FROM performance_schema.replication_group_member_stats;
  • 成员状态、Primary 变化和成员数量是否低于容错要求

  • applier/certifier 队列是否持续增长,以及 flow control 是否频繁触发

  • 磁盘、binlog、Clone 空间、网络时延和丢包

  • Router 进程、6446/6447/6450 端口及实际读写探测

  • MySQL、Router 叶子证书和 CA 的剩余有效期,至少提前 30 天告警

可把下面的检查接入 cron 或现有监控;退出码非 0 表示 30 天内过期:

1
2
3
4
5
6
7
8
9
10
# 在MySQL节点执行
cert="/etc/mysql/tls/$(hostname -s)-cert.pem"
openssl x509 -checkend $((30 * 86400)) -noout -in "$cert"
openssl x509 -checkend $((30 * 86400)) -noout -in /etc/mysql/tls/ca.pem

# 在Router节点执行
openssl x509 -checkend $((30 * 86400)) -noout \
-in /etc/mysqlrouter/tls/router-cert.pem
openssl x509 -checkend $((30 * 86400)) -noout \
-in /etc/mysqlrouter/tls/ca.pem

最慢的成员可能触发 flow control 并限制整个集群写入速度,所以 Secondary 不是免费的读扩容。

证书轮换

叶子证书续签后,先执行第 5 节的链、SAN 和公私钥匹配检查,再按 Secondary → Primary 顺序逐台替换:

  1. 原子替换该节点的证书和私钥,保持 root:mysql、证书 644、私钥 640

  2. 执行 ALTER INSTANCE RELOAD TLS,让 MySQL 主监听端口的新连接使用新证书

  3. ALTER INSTANCE RELOAD TLS 不会刷新正在运行的 Group Replication TLS 上下文;执行 STOP GROUP_REPLICATION; START GROUP_REPLICATION;

  4. 等该成员恢复 ONLINE 后再处理下一台;处理 Primary 前先使用 setPrimaryInstance() 计划切主

  5. Router 证书逐台替换并重启 Router,验证 6446/6447/6450 后再处理下一台

CA 轮换不能直接覆盖旧 CA:先下发同时包含新旧 CA 的信任链,再换服务端证书和客户端信任,最后移除旧 CA。整个过程保持至少两个 Group Replication 成员在线,不要同时停止所有成员。

滚动维护与升级

升级前先核对 Server、Shell、Router 的兼容矩阵和发布说明,并做好备份与回滚方案。先把运维端 MySQL Shell 升级到不低于目标 Server 的版本,再在每个数据库节点本机执行检查;configPath 是运行 Shell 这台机器上的本地路径:

1
2
3
4
5
6
7
8
9
10
11
12
 MySQL  JS > util.checkForServerUpgrade('icadmin@localhost:3306', {
targetVersion: '<TARGET_MYSQL_VERSION>',
configPath: '/etc/my.cnf'
})
MySQL node1:3306 ssl JS > cluster.listRouters()
MySQL node1:3306 ssl JS > dba.upgradeMetadata()

// 自定义名称不以mysql_router_开头,不会由upgradeMetadata自动补权限
MySQL node1:3306 ssl JS > cluster.setupRouterAccount(
'routeruser@10.250.0.21',
{update: true}
)

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 支持 自己处理 部分 有限 原生
组件数量
社区/项目成熟度 历史成熟,但多年未更新 中小,但多年未更新 较成熟 已归档 官方体系
新项目推荐价值 高,推荐