环境 :192.168.195.141 / 142 / 143(CentOS 系 Kylin V10,2.8G 内存)
软件 :MySQL 8.0.35 + MySQL Shell 8.0.35 + MySQL Router 8.0.35(均为官方归档源二进制包,国内镜像最高仅 8.0.28)
覆盖 :InnoDB_Cluster.pdf 全部内容(架构、Shell 安装、实例配置、建集群、加节点、状态查看、Router 部署、PyMySQL 高可用测试)+ 12 个补充场景实测
验证状态:全部场景实测通过。所有命令注明执行节点,可按文档逐步复现。
一、InnoDB Cluster 架构与原理(PDF 知识点)
1.1 组件构成
应用 (PyMySQL / JDBC / 任何 MySQL 客户端)
│
▼ 6446(RW) / 6447(RO) / 6448(X-RW) / 6449(X-RO) / 8443(REST API)
┌──────────────┐
│ MySQL Router │ ← 轻量转发器: 读元数据, 找 PRIMARY/SECONDARY, 秒级自动切流
└──────┬───────┘
│
┌──────▼──────────────────────────────────────┐
│ InnoDB Cluster (= MGR + 元数据 + Shell 管理) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │141 主RW │◄─►│142 从RO │◄─►│143 从RO│ MGR组复制(Paxos多数派认证)
│ └────────┘ └────────┘ └────────┘ │
│ mysql_innodb_cluster_metadata 库 │
└──────────────────────────────────────────────┘
三层分工:
- MySQL Server + MGR 插件:数据层,组复制提供多写协议/自动故障检测/选主(底层就是前一篇笔记的 MGR)
- mysql_innodb_cluster_metadata:元数据库,记录集群拓扑(clusters/instances/routers 表),Router 靠它寻址
- MySQL Shell (mysqlsh):管理入口,dba.* API 把 MGR 复杂操作(配置/建组/加节点/切主/恢复)封装成一条命令
- MySQL Router:接入层,替代应用的 VIP;bootstrap 时把自身注册进元数据,动态感知主从变化
1.2 Router 工作流程(PDF 原文要点)
- 应用向 MySQL Router 发起连接
- Router 检查并选择一个可用的后端节点(MySQL Server)
- Router 与这个后端节点建立连接
- Router 来回转发应用与后端节点之间的数据包
- 如果后端节点出现问题,Router 会断开应用端的连接。应用端重试后,Router 会选择另外一个可用节点重复上述流程
端口规划(默认 5 个监听):
| 端口 | 协议 | 用途 |
|---|---|---|
| 6446 | Classic | 读写(自动路由到 PRIMARY) |
| 6447 | Classic | 只读(round-robin 到 SECONDARY) |
| 6448 | X | 读写(X 协议) |
| 6449 | X | 只读(X 协议) |
| 8443 | HTTP | REST API(监控 Router 自身) |
1.3 与 MGR 的关系(关键认知)
InnoDB Cluster 不是新的复制技术 ,而是「MGR + 元数据 + Shell + Router」的管理套装:
- 底层复制 = Group Replication(单主模式,多数派认证)
dba.createCluster()本质 = 装 MGR 插件 + 建元数据库 + 引导组cluster.addInstance()本质 = 配置实例 + clone/增量恢复 + 加入组 + 写元数据- 故障切换 = MGR 自动选主,Router 盯元数据自动切流 ------ 应用无需感知 IP
二、环境清理(三节点执行)
bash
# 执行节点: 141/142/143
ip addr del 192.168.195.200/24 dev ens32 2>/dev/null # 清 VIP 残留
systemctl stop mysqld mysqlrouter orchestrator orch-smtp 2>/dev/null
systemctl disable mysqld mysqlrouter 2>/dev/null
rm -rf /etc/systemd/system/mysqld.service /etc/systemd/system/mysqlrouter.service \
/etc/systemd/system/orchestrator.service /etc/systemd/system/orch-smtp.service \
/data/mysql /data/myrouter /etc/my.cnf /etc/sysconfig/mysql \
/home/orchestrator /var/lib/orchestrator /var/log/orchestrator /var/mailbox \
/opt/smtp_server.py /etc/profile.d/orchestrator-client.sh \
/etc/masterha /var/log/masterha /usr/local/bin/orchestrator-client \
/usr/local/bin/masterha* /usr/local/mysql /usr/local/mysql-shell /usr/local/mysql-router
rpm -e mha4mysql-node mha4mysql-manager 2>/dev/null
userdel -r mysql 2>/dev/null; userdel -r orchestrator 2>/dev/null
systemctl daemon-reload
# 验证: 无相关服务/进程, /usr/local 下无 mysql*, id mysql 报无此用户
三、软件下载(国内源优先策略)
实测结论(2026-08) :清华/阿里/腾讯/华为/中科大镜像的 MySQL-8.0 目录最高只有 8.0.28 ,Shell/Router 同理。8.0.35 全家桶必须用官方归档源 downloads.mysql.com/archives(直连速度快)。
bash
# 执行节点: 141(下载后 scp 分发, 或任一台有外网的机器)
cd /usr/local/src
# MySQL Server 8.0.35 (glibc2.17)
curl -sL -O "https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.35-linux-glibc2.17-x86_64.tar.xz"
# MySQL Shell 8.0.35
curl -sL -O "https://downloads.mysql.com/archives/get/p/43/file/mysql-shell-8.0.35-linux-glibc2.12-x86-64bit.tar.gz"
# MySQL Router 8.0.35
curl -sL -O "https://downloads.mysql.com/archives/get/p/41/file/mysql-router-8.0.35-linux-glibc2.12-x86_64.tar.xz"
# 校验(md5, 三节点必须一致):
# mysql-8.0.35...tar.xz c17075cfc58d8cd25cea4be36d77eed2
# mysql-shell-8.0.35...tar.gz 73326660b188520acc02fa49a1172436
# mysql-router-8.0.35...tar.xz 0dcddd49a3de1f7454cdaf52958ea7a3
# 分发(141 上执行):
for h in 192.168.195.142 192.168.195.143; do
scp mysql-*.tar.* root@$h:/usr/local/src/
done
四、MySQL 安装与 my.cnf(三节点)
4.1 用户/解压/软链(三节点)
bash
# 执行节点: 141/142/143
groupadd -f mysql && useradd -g mysql -s /sbin/nologin mysql
cd /usr/local
tar xf /usr/local/src/mysql-8.0.35-linux-glibc2.17-x86_64.tar.xz
ln -sf mysql-8.0.35-linux-glibc2.17-x86_64 mysql
tar xf /usr/local/src/mysql-shell-8.0.35-linux-glibc2.12-x86-64bit.tar.gz
ln -sf mysql-shell-8.0.35-linux-glibc2.12-x86-64bit mysql-shell
mkdir -p /data/mysql/3306/data && chown -R mysql:mysql /data/mysql
# 141/142 额外装 Router(双 Router 容灾):
tar xf /usr/local/src/mysql-router-8.0.35-linux-glibc2.12-x86_64.tar.xz
ln -sf mysql-router-8.0.35-linux-glibc2.12-x86_64 mysql-router
/usr/local/mysql/bin/mysql --version # Ver 8.0.35
/usr/local/mysql-shell/bin/mysqlsh --version # Ver 8.0.35
4.2 my.cnf(含逐项注释;三节点仅 server-id/report_host/group_seeds 的 IP 不同)
bash
# 执行节点: 141(142/143 把 IP 尾段与 server-id 对应改)
cat > /etc/my.cnf << 'EOF'
[client]
socket = /data/mysql/3306/data/mysql.sock
[mysqld]
# ========== 基础参数 ==========
basedir = /usr/local/mysql
datadir = /data/mysql/3306/data
user = mysql
port = 3306
socket = /data/mysql/3306/data/mysql.sock
log_error = /data/mysql/3306/data/mysqld.err
log_timestamps = system
# ========== MGR 前提参数(InnoDB Cluster 底层依赖) ==========
server-id = 1 # 三节点唯一: 1/2/3
log-bin = mysql-bin # 必须 binlog
binlog_format = ROW # MGR 强制 ROW
binlog_row_image = FULL # 完整行镜像(冲突认证需要)
gtid_mode = ON # MGR 强制 GTID
enforce_gtid_consistency = ON
log_slave_updates = ON # 组内事务需写回 binlog
master_info_repository = TABLE # 崩溃安全
relay_log_info_repository = TABLE
relay_log_recovery = ON
report_host = 192.168.195.141 # 集群内显示地址(务必写IP,避免主机名解析问题)
report_port = 3306
# ========== Shell configureInstance 会自动检查/补齐的项 ==========
binlog_transaction_dependency_tracking = WRITESET # 并行回放依赖:WRITESET 大幅提升从库应用速度
transaction_write_set_extraction = XXHASH64 # write set 提取算法(冲突检测用)
slave_preserve_commit_order = ON # 从库按主库提交顺序应用(强一致)
# ========== MGR 插件参数(loose-前缀=插件未载入时不报错) ==========
loose-plugin_load_add = 'group_replication.so' # mysqld 启动自动加载 MGR 插件
loose-group_replication_group_seeds = '192.168.195.141:33061' # 组种子地址
loose-group_replication_start_on_boot = OFF # 由 Shell 管理,不随库自启(演示友好)
loose-group_replication_recovery_use_ssl = 0
loose-group_replication_exit_state_action = READ_ONLY # 异常退出自动只读,保护数据
loose-group_replication_single_primary_mode = ON # 单主模式(InnoDB Cluster 默认)
loose-group_replication_message_cache_size = 1073741824 # 组消息缓存 1G
# ========== 内存(2.8G 小内存机器) ==========
innodb_buffer_pool_size = 512M
innodb_redo_log_capacity = 256M
EOF
⚠️ 踩坑提醒 :
group_replication_group_seeds若在 heredoc 里写${report_host}这类变量不会展开,写死完整 IP 最稳。
4.3 systemd 服务 + 初始化(三节点)
bash
# 执行节点: 141/142/143
cat > /etc/systemd/system/mysqld.service << 'EOF'
[Unit]
Description=MySQL Server (InnoDB Cluster Node)
After=network.target syslog.target
[Install]
WantedBy=multi-user.target
[Service]
User=mysql
Group=mysql
Type=forking
PIDFile=/data/mysql/3306/data/mysqld.pid
TimeoutSec=0
ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --pid-file=/data/mysql/3306/data/mysqld.pid --daemonize $MYSQLD_OPTS
EnvironmentFile=-/etc/sysconfig/mysql
LimitNOFILE=65535
Restart=on-failure
RestartPreventExitStatus=1
PrivateTmp=false
EOF
echo 'MYSQLD_OPTS=' > /etc/sysconfig/mysql
systemctl daemon-reload
/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --initialize # 无输出即成功
grep 'temporary password' /data/mysql/3306/data/mysqld.err # 取临时密码
systemctl start mysqld && systemctl enable mysqld
# 改密(三节点各自的临时密码):
mysql -uroot -p'临时密码' -S /data/mysql/3306/data/mysql.sock --connect-expired-password \
-e "ALTER USER user() IDENTIFIED BY '123456';"
4.4 创建远程管理账号(PDF 步骤2, 三节点)
sql
-- 执行节点: 141/142/143(socket 登录)
set session sql_log_bin=0; -- 关键!不记 binlog,避免节点间 GTID 分叉
create user root@'%' identified by '123456';
grant all on *.* to root@'%' with grant option;
set session sql_log_bin=1;
五、dba.configureInstance 配置实例(PDF 步骤3)
5.1 PDF 原版交互式用法(人工操作推荐)
bash
# 执行节点: 141/142/143
mysqlsh
js
// MySQL JS > (PDF 原样; 三个节点各执行一次)
dba.configureInstance('root:123456@192.168.195.141:3306', {
clusterAdmin: 'cluster_admin', clusterAdminPassword: 'cluster_pass'
})
// 提示需要修复的参数(本环境 my.cnf 已预配, 只差并行 applier):
// binlog_transaction_dependency_tracking: COMMIT_ORDER -> WRITESET
// Do you want to perform the required configuration changes? [y/n]: y
// -> The instance '192.168.195.141:3306' was configured to be used in an InnoDB cluster.
configureInstance 做了什么:
- 检查/修复 MGR 必需参数(binlog/GTID/ROW/write_set 等)
- 创建
cluster_admin管理账号(后续建集群/加节点都用它, 权限含 CLONE_ADMIN/REPLICATION_SLAVE 等) - 注册
group_replication插件加载 - 开启并行 appliers(
Successfully enabled parallel appliers)
5.2 脚本化非交互用法(实测可用, 自动化推荐)
bash
# 执行节点: 141/142/143
mysqlsh --uri "root:123456@192.168.195.141:3306" --js \
-e "dba.configureInstance(null, {clusterAdmin: 'cluster_admin', clusterAdminPassword: 'cluster_pass'})"
⚠️ 实测踩坑(自动化同学必看):
configureInstance第 1 参数为null时作用于--uri连接的实例;选项放第 2 参数。直接
mysqlsh -e "dba.configureInstance('root:...')"不带--uri时,Shell 会去连 localhost 且密码解析走 login-path,报Access denied for 'root'@'localhost' (using password: NO)-- 务必--uri全量写。三节点执行后验证(每节点):
sqlSELECT user,host FROM mysql.user WHERE user='cluster_admin'; -- cluster_admin %
六、创建集群与添加节点(PDF 步骤4/5)
6.1 createCluster(141, PDF 步骤4)
bash
# 执行节点: 141
mysqlsh --uri "cluster_admin:cluster_pass@192.168.195.141:3306" --js -e "
dba.createCluster('myCluster', { disableClone: false })
"
text
实际输出(节选):
Creating InnoDB Cluster 'myCluster' on '192.168.195.141: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 节点才能容 1 节点故障
disableClone: false= 允许用 clone 插件做节点供给(8.0.17+ 默认即支持,显式写出与 PDF 一致)。
6.2 addInstance 添加 142(clone 方式, PDF 步骤5)
bash
# 执行节点: 141
mysqlsh --uri "cluster_admin:cluster_pass@192.168.195.141:3306" --js -e "
var cluster = dba.getCluster('myCluster');
cluster.addInstance('cluster_admin:cluster_pass@192.168.195.142:3306',
{recoveryMethod: 'clone'});
"
text
实际输出(节选, 与 PDF 页5 一致):
NOTE: A server restart is expected to happen as part of the clone process...
NOTE: 192.168.195.142:3306 is being cloned from 192.168.195.141:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############ 100% Completed
PAGE COPY ############ 100% Completed
REDO COPY ############ 100% Completed
NOTE: 192.168.195.142:3306 is shutting down...
* Waiting for server restart...
⚠️ 实测踩坑(systemd 部署必遇) :clone 完成后实例自动 RESTART 需要
supervisor权限,systemd 托管的 mysqld 无法自启,错误日志出现:
[MY-013462] Clone shutting down server as RESTART failed. Please start server to complete clone operation.且 mysqlsh 卡在
Waiting for server restart直到超时(MYSQLSH 51156)。处置 :到 142 上
systemctl start mysqld手动拉起,然后在 141 上cluster.rescan({addInstances: ['192.168.195.142:3306'], interactive: false})收编元数据(rescan 会发现"游离"的 GR 成员并写回 metadata)。
6.3 addInstance 添加 143
同 6.2(clone + 超时后 systemctl start mysqld + rescan 收编)。143 添加完成后的最终状态与 PDF 完全一致:
js
// 执行节点: 141
mysqlsh --uri "cluster_admin:cluster_pass@192.168.195.141:3306" --js -e "
var c = dba.getCluster('myCluster'); print(c.status());
"
json
{
"clusterName": "myCluster",
"defaultReplicaSet": {
"name": "default",
"primary": "192.168.195.141:3306",
"ssl": "REQUIRED",
"status": "OK", // <- 全 OK
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"192.168.195.141:3306": { "memberRole": "PRIMARY", "mode": "R/W", "status": "ONLINE", "version": "8.0.35" },
"192.168.195.142:3306": { "memberRole": "SECONDARY", "mode": "R/O", "status": "ONLINE", "version": "8.0.35" },
"192.168.195.143:3306": { "memberRole": "SECONDARY", "mode": "R/O", "status": "ONLINE", "version": "8.0.35" }
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "192.168.195.141:3306"
}
status 字段速查:
| status | 含义 | 处置 |
|---|---|---|
| OK | 全部在线,可容 1 故障 | 正常 |
| OK_NO_TOLERANCE / OK_NOT_TOLERANT | 在线但无可容故障余量(2节点) | 尽快恢复第三节点 |
| OK_NO_TOLERANCE_PARTIAL | 部分成员 MISSING | 检查失联节点 |
| NO_QUORUM | 失去多数派 | forceQuorum 或 reboot |
| UNKNOWN | 元数据不可达 | 检查连接/metadata |
七、测试数据与业务账号
sql
-- 执行节点: 141 (PRIMARY)
CREATE DATABASE cluster_db;
USE cluster_db;
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
customer VARCHAR(50) NOT NULL,
product VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00,
status ENUM('pending','paid','shipped') DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
INSERT INTO orders(customer,product,amount,status) VALUES
('张三','iPhone 15',5999.00,'paid'),
('李四','MacBook Pro',14999.00,'pending'),
('王五','AirPods Pro',1899.00,'shipped'),
('赵六','iPad Air',4399.00,'paid'),
('钱七','Apple Watch',2999.00,'pending');
CREATE TABLE order_items (
item_id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
item_name VARCHAR(100),
qty INT DEFAULT 1,
price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
) ENGINE=InnoDB;
INSERT INTO order_items(order_id,item_name,qty,price) VALUES
(1,'iPhone 15 黑色',1,5999.00),(2,'MacBook 14寸',1,14999.00),(2,'扩展坞',2,199.00),
(3,'AirPods Pro 2',1,1899.00),(4,'iPad Air 5',1,4399.00),(4,'Pencil 2',1,999.00);
-- 业务账号(经 Router 连接用):
CREATE USER 'test_user'@'%' IDENTIFIED BY '123456';
GRANT SELECT,INSERT,UPDATE,DELETE ON cluster_db.* TO 'test_user'@'%';
FLUSH PRIVILEGES;
sql
-- 执行节点: 142/143 (验证秒级同步)
SELECT COUNT(*) orders_cnt FROM cluster_db.orders; -- 5
SELECT COUNT(*) items_cnt FROM cluster_db.order_items; -- 6
八、MySQL Router 部署(PDF 步骤7)
8.1 bootstrap 初始化
bash
# 执行节点: 141
mkdir -p /data/myrouter/6446 && chown -R mysql:mysql /data/myrouter
mysqlrouter --bootstrap cluster_admin:cluster_pass@192.168.195.141:3306 \
--user=mysql --directory /data/myrouter/6446 \
--conf-use-sockets --report-host='192.168.195.141'
text
实际输出(节选, 与 PDF 一致):
# MySQL Router configured for the InnoDB Cluster 'myCluster'
- Read/Write Connections: 192.168.195.141:6446, /data/myrouter/6446/mysql.sock
- Read/Only Connections: 192.168.195.141:6447, /data/myrouter/6446/mysqlro.sock
- Read/Write Connections: 192.168.195.141:6448, /data/myrouter/6446/mysqlx.sock
- Read/Only Connections: 192.168.195.141:6449, /data/myrouter/6446/mysqlxro.sock
bootstrap 同时做了(要点):
- 在集群里建
mysql_router1_xxx账号(存 keyring, 用于读元数据) - 生成
/data/myrouter/6446/mysqlrouter.conf - 把 Router 自身注册进元数据 (
cluster.listRouters()可见)
8.2 mysqlrouter.conf 配置文件讲解(关键字段注释)
ini
[DEFAULT]
user=mysql # 运行用户
logging_folder=/data/myrouter/6446/log # 日志目录
dynamic_state=/data/myrouter/6446/data/state.json # 动态状态缓存(主从拓扑快照,断连元数据也能路由)
client_ssl_mode=PREFERRED # 客户端到Router的SSL
server_ssl_mode=AS_CLIENT # Router到后端跟随客户端
[logger]
level=INFO
[metadata_cache:bootstrap] # 元数据缓存模块
cluster_type=gr # 集群类型: Group Replication
router_id=1 # 本Router在元数据里的ID
user=mysql_router1_w1i4cuz4097l # 读元数据的账号(keyring存密码)
metadata_cluster=myCluster # 集群名
ttl=0.5 # 元数据刷新间隔(秒): 越小故障感知越快
auth_cache_ttl=-1 # 认证缓存(密码变更即时生效)
[routing:bootstrap_rw] # 读写路由段
bind_port=6446 # 监听端口
destinations=metadata-cache://myCluster/?role=PRIMARY # 目标=元数据里的PRIMARY
routing_strategy=first-available # 主只有1个:取第一个可用
protocol=classic
[routing:bootstrap_ro] # 只读路由段
bind_port=6447
destinations=metadata-cache://myCluster/?role=SECONDARY # 目标=所有SECONDARY
routing_strategy=round-robin-with-fallback # 轮询分发;从库全挂则回退主库!
protocol=classic
# (bootstrap_x_rw 6448 / bootstrap_x_ro 6449 为 X 协议段,略)
8.3 启动与验证
bash
# 执行节点: 141
sh /data/myrouter/6446/start.sh # PDF 方式(脚本后台拉起)
# 生产建议 systemd:
cat > /etc/systemd/system/mysqlrouter.service << 'EOF'
[Unit]
Description=MySQL Router for InnoDB Cluster
After=network.target mysqld.service
[Install]
WantedBy=multi-user.target
[Service]
User=mysql
Group=mysql
PIDFile=/data/myrouter/6446/mysqlrouter.pid
ExecStart=/usr/local/mysql-router/bin/mysqlrouter -c /data/myrouter/6446/mysqlrouter.conf
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
EOF
systemctl daemon-reload && systemctl enable --now mysqlrouter
ss -ntl | grep -E '6446|6447|6448|6449|8443' # 5 端口监听(PDF 同款验证)
bash
# 读写分离验证(PDF 核心验证点):
# 执行节点: 141
mysql -utest_user -p123456 -h127.0.0.1 -P6446 -e "SELECT @@hostname;" # MYSQL141 (主,RW)
mysql -utest_user -p123456 -h127.0.0.1 -P6447 -e "SELECT @@hostname;" # MYSQL142/143 轮询(从,RO)
js
// Router 注册信息(PDF cluster.listRouters()):
mysqlsh --uri "cluster_admin:cluster_pass@192.168.195.141:3306" --js -e "
var c=dba.getCluster(); print(JSON.stringify(c.listRouters()));"
// {"routers":{"192.168.195.141::":{...,"rwPort":"6446","roPort":"6447","lastCheckIn":"..."} }}
九、全场景验证实录(A~J 全部实测通过)
场景A: 主库故障自动 failover + Router 自动切流 ✅
bash
# 执行节点: 141 (当时 PRIMARY)
systemctl stop mysqld # 模拟主库宕机
js
// 执行节点: 142 (25秒后查看, mysqlsh 连任意幸存节点)
"primary": "192.168.195.142:3306", // <- 142 自动当选新主
"status": "OK_NO_TOLERANCE_PARTIAL", // 141 显示 (MISSING)
bash
# Router 验证(执行节点: 141, Router 幸存):
mysql -utest_user -p123456 -h127.0.0.1 -P6446 -e "SELECT @@hostname;"
# -> MYSQL142 <- 6446 无缝指向新主,写能力立即恢复
mysql -utest_user -p123456 -h127.0.0.1 -P6447 -e "SELECT @@hostname;"
# -> MYSQL143 <- 只读流量自动避开故障节点
旧主恢复(实例重启后不会自动入组, 需 rejoin):
js
// 执行节点: 142 (新主)
var c=dba.getCluster('myCluster');
c.rejoinInstance('192.168.195.141:3306'); // 只给 host:port, 账号用会话的
// -> The instance '192.168.195.141:3306' was successfully rejoined to the cluster.
注:8.0 的 auto-rejoin 机制下,重启后的实例通常也会自动重入组(本次实测约 30 秒自动回组 SECONDARY);rejoinInstance 是确定性操作手段。
场景B: 手动在线切主 setPrimaryInstance ✅
js
// 执行节点: 任意在线节点 (当时主=142, 切到143)
var c=dba.getCluster('myCluster');
c.setPrimaryInstance('192.168.195.143:3306');
// -> The instance '192.168.195.143:3306' was successfully elected as primary.
// status: primary=143, 全员 ONLINE
bash
# Router 即时跟随(无需重启):
mysql -utest_user -p123456 -h127.0.0.1 -P6446 -e "SELECT @@hostname;" # MYSQL143
mysql -utest_user -p123456 -h127.0.0.1 -P6447 -e "SELECT @@hostname;" # MYSQL142
场景C: 移除节点 removeInstance + 重新加入 ✅
js
// 执行节点: 143 (当时主)
var c=dba.getCluster('myCluster');
c.removeInstance('192.168.195.142:3306');
// -> successfully removed from the cluster.
// 142 上查: performance_schema.replication_group_members 只剩自己, MEMBER_STATE=OFFLINE
// (数据保留, 只是被移出组与元数据)
// 重新加入(142数据还在,差量小 -> incremental 恢复, 不重克隆):
c.addInstance('cluster_admin:cluster_pass@192.168.195.142:3306', {recoveryMethod: 'incremental'});
// -> successfully added to the cluster. 全组 OK
recoveryMethod 选择(面试高频):
| 方式 | 原理 | 适用 |
|---|---|---|
| clone | 物理快照全量覆盖 | 新节点/数据损坏/GTID 缺口大(默认推荐) |
| incremental | GTID 差量 binlog 补 | 节点短暂离线,GTID 是集群子集 |
场景D: 多数派故障(2节点宕机) ✅
bash
# 执行节点: 141/142
systemctl stop mysqld # 141、142 全停, 只剩 143
text
实测结果:
- 143 视角: status=OK_NO_TOLERANCE_PARTIAL ("2 members are not active")
- 错误日志: 'Primary server switched to: 143的uuid' <- 143 成单节点组, 继续当主
- 143 本地写入正常(SELECT @@read_only=0), orders=8
- 但! 部署在141上的 Router 不可用(6446 拒连) -- Router 与 DB 同机殉葬
生产启示:Router 必须独立部署或部署在应用侧(见场景G/J 双 Router 方案);本次恢复 141/142 后集群自动回 OK。
场景E: 网络分区(隔离主库) ✅
bash
# 执行节点: 143 (当时 PRIMARY) 隔离组通信端口 33061
iptables -A INPUT -p tcp --dport 33061 -j DROP
iptables -A OUTPUT -p tcp --dport 33061 -j DROP
text
实测结果(40秒后):
- 多数派(141+142)视角: 143 仍列在成员里但超时驱逐; 若 143 持续失联最终被踢
- 143(少数派)自保护: INSERT 直接报
ERROR 1290: The MySQL server is running with the --super-read-only option
<- 少数派主动只读,杜绝脑裂写(比手搭 MGR 的"事务悬挂"体验更好, 因为
exitStateAction=READ_ONLY + unreachable_majority 处理即时)
- 解除隔离(iptables -D ...)后 30 秒内自动回组, 数据追平
场景F: 隔离+宕机叠加(极端故障) ✅
bash
# 执行节点: 143: iptables 隔离 33061 且 systemctl stop mysqld
text
实测: 多数派 35 秒内完成:
1. 驱逐 143 -> 重新选举 -> 141 当选新 PRIMARY
2. Router 6446 自动指向 141
恢复 143(解除iptables+启动mysqld)后自动回组 SECONDARY, 全组 OK
场景G: 双 Router 容灾部署 ✅
bash
# 执行节点: 142 (第二台 Router)
cd /usr/local && tar xf /usr/local/src/mysql-router-8.0.35-linux-glibc2.12-x86_64.tar.xz
ln -sf mysql-router-8.0.35-linux-glibc2.12-x86_64 mysql-router
mkdir -p /data/myrouter/6446 && chown -R mysql:mysql /data/myrouter
mysqlrouter --bootstrap cluster_admin:cluster_pass@192.168.195.141:3306 \
--user=mysql --directory /data/myrouter/6446 --conf-use-sockets \
--report-host='192.168.195.142'
# (再配 systemd 同 8.3, systemctl enable --now mysqlrouter)
mysql -utest_user -p123456 -h127.0.0.1 -P6446 -e "SELECT @@hostname;" # -> 当前主
# cluster.listRouters() 现在能看到两台: 192.168.195.141:: 和 192.168.195.142::
场景H: PyMySQL 高可用压轴测试(PDF 最后章节) ✅
测试脚本 check_mysql_connection.py(PDF 原版+注释, 见附录A):
bash
# 执行节点: 有 python3+pymysql 的机器(本环境为管理机)
pip3 install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple # 国内源
python3 check_mysql_connection.py rw # 每秒连一次 6446 打印当前节点
压轴实测(跑脚本中途杀主库 141):
text
[18:25:21] [rw] 当前节点: MYSQL141 <- 正常
[18:25:26] [rw] 当前节点: MYSQL141 <- 最后一秒正常
[18:25:30] [rw] 操作失败: (2013, 'Lost connection to MySQL server during query')
[18:25:31] [rw] 当前节点: MYSQL142 <- 仅 1 次失败(~4秒),之后全程 MYSQL142
[18:26:15] ... 持续 MYSQL142
结论:应用层(带重试的连接循环)视角,failover = 1 次报错重连,业务无感恢复。这就是 InnoDB Cluster 的完整价值闭环。
场景I: 整集群宕机恢复 rebootClusterFromCompleteOutage ✅
bash
# 三节点 systemctl stop mysqld -> 全体重启 systemctl start mysqld
# 重启后各节点 GR 是 OFFLINE(组没被引导)
js
// 执行节点: 141
var c = dba.rebootClusterFromCompleteOutage('myCluster');
// -> The instance '192.168.195.142:3306' was successfully rejoined to the cluster.
// -> The instance '192.168.195.141:3306' was successfully rejoined to the cluster.
// status: OK, primary 自动选出(143), 数据完整(orders=8)
⚠️ 函数名易错:是
rebootClusterFromCompleteOutage(不是 rebootFromCompleteOutage)。该命令会自动比对各节点 GTID/executed 集合, 选数据最新的做种子重组集群。
场景J: Router 自身高可用 ✅
bash
# 执行节点: 141 停自己的 Router
systemctl stop mysqlrouter
# 应用切到 142 的 Router(客户端配多 Router 地址或前置 LB):
mysql -utest_user -p123456 -h192.168.195.142 -P6446 -e "SELECT @@hostname;" # -> 主, 正常
# 恢复: systemctl start mysqlrouter
场景K: 日常检查命令集 ✅
js
// 实例级配置体检(可对任意节点, 输出兼容性结论):
dba.checkInstanceConfiguration()
// -> Instance configuration is compatible with InnoDB cluster
// -> No incompatible tables detected (还会检查是否有无主键表等 GR 不兼容对象)
// 集群拓扑描述(轻量, 不做健康检查):
var c=dba.getCluster(); c.describe()
// 集群参数(含每节点的 autoRejoinTries/consistency/exitStateAction 等):
c.options()
// Router 清单与健康打卡:
c.listRouters() // lastCheckIn 时间戳即 Router 心跳
十、常见故障与排错汇总(实测踩坑实录)
10.1 addInstance 卡 "Waiting for server restart" 超时 (MYSQLSH 51156)
根因 :clone 完成后要 RESTART 实例, systemd 托管的 mysqld 无 supervisor 权限自启。
错误日志铁证:[MY-013462] Clone shutting down server as RESTART failed. Please start server to complete clone operation.
处置 :目标节点 systemctl start mysqld -> 主控端 cluster.rescan({addInstances: ['ip:3306'], interactive: false}) 收编。
预防 :RPM 包部署(自带 mysqld-sysctl 包装)或在 my.cnf 加 loose-group_replication_clone_threshold 调大减少 clone 触发。
10.2 rescan 报 "Failed to acquire Cluster lock" (MYSQLSH 51500)
根因 :前一个 mysqlsh 会话(卡死的 addInstance/rescan)还持有集群锁。
处置 :主控节点 pkill -9 mysqlsh 后重试。
预防:脚本化操作统一加超时;操作串行化。
10.3 新节点 GTID 分叉 (ERROR 3092 / "more executed transactions")
根因 :建管理账号时没 SET SQL_LOG_BIN=0, 本地多出组外事务。
处置 :全新节点 RESET MASTER 后重新 addInstance(clone 会覆盖,最稳)。
预防 :凡"管理性质"DDL(建用户/授权/装插件)先 SET SQL_LOG_BIN=0。
10.4 Router 连不上 (ERROR 2003 Can't connect 6446)
排查链 :systemctl status mysqlrouter -> /data/myrouter/6446/log/mysqlrouter.log -> 元数据账号是否被删 -> 集群是否 NO_QUORUM(Router 无主可路由时拒连)。
本次实测 :141 宕机时其上的 Router 也随之不可用 -- Router 别和 DB 节点共生亡, 独立部署或双 Router+应用侧重试。
10.5 少数派写入报 1290 super-read-only
这不是故障,是保护:exitStateAction=READ_ONLY + 多数派失联 = 主动只读防脑裂。恢复网络/多数派后自动回 RW。
10.6 全集群宕机后各节点 OFFLINE 不自愈
设计如此 :谁都不许自引导(防脑裂)。用 dba.rebootClusterFromCompleteOutage('myCluster') 人工确认恢复(场景I)。
十一、运维命令速查卡
| 操作 | 命令(mysqlsh --uri cluster_admin:... --js) |
|---|---|
| 集群状态 | dba.getCluster().status() |
| 拓扑描述 | dba.getCluster().describe() |
| 集群参数 | dba.getCluster().options() |
| 实例体检 | dba.checkInstanceConfiguration() |
| 加节点 | c.addInstance('cluster_admin:xxx@ip:3306',{recoveryMethod:'clone'}) |
| 删节点 | c.removeInstance('ip:3306') |
| 手动切主 | c.setPrimaryInstance('ip:3306') |
| 节点重入 | c.rejoinInstance('ip:3306') |
| 元数据收编 | c.rescan({addInstances:['ip:3306'],interactive:false}) |
| 整集群恢复 | dba.rebootClusterFromCompleteOutage('myCluster') |
| Router 清单 | c.listRouters() |
| 解散集群 | c.dissolve({force:false}) |
| 多主切换 | c.switchToMultiPrimaryMode() |
| 回单主 | c.switchToSinglePrimaryMode('ip:3306') |
GR 层排错视图(SQL, 任意节点):
sql
SELECT * FROM performance_schema.replication_group_members; -- 成员与状态
SELECT * FROM performance_schema.replication_group_member_stats\G -- 事务/认证统计
SELECT * FROM mysql_innodb_cluster_metadata.instances; -- 元数据拓扑
十二、补充知识点(超越 PDF 的生产必知)
- ClusterSet (8.0.27+):多个 InnoDB Cluster 组成"主集群+副本集群"的容灾/异地多活架构,
dba.createClusterSet()创建, 副本集群用异步复制(传统 GTID), 主集群故障可clusterset.setPrimaryCluster()整体切换。 - 读 replicas 扩展读 :
cluster.addInstance(ip, {label:'rr1'})+c.setupRouterAccount后把节点配为 Read Replica 角色(不参与 MGR 投票,纯异步挂载)。 - 一致性与 fallback :
round-robin-with-fallback意味着从库全挂时 6447 会回退到主库 -- 读流量永不 100% 中断。 - MySQL Router 8.1+ 支持读写分离的新策略 与 REST API 完善(
curl http://router:8443/api/20210810/routes), 便于监控探活。 - 备份恢复 :InnoDB Cluster 仍用 mysqlsh
util.dumpInstance()/loadDump()或 XtraBackup 集群外备份, 元数据库随实例一起备份。 - 与 MHA/Orchestrator 对比:MHA/Orch 是"外挂式"仲裁+VIP 切换; InnoDB Cluster 是"内建式"多数派仲裁+Router 路由, 无 VIP 依赖、无脑裂窗口, 是 8.0 官方主推方案。
附录A: 高可用测试脚本(PDF 完整版+注释)
python
# check_mysql_connection.py
import pymysql, time, sys
hostname = '192.168.195.141' # Router 地址(生产: 应用侧 VIP/LB 或多 Router 地址)
username = 'test_user'
password = '123456'
database = 'information_schema'
def check_mysql(hostname, port, username, password, database, mode):
try:
conn = pymysql.connect(host=hostname, port=port, user=username,
password=password, database=database,
connect_timeout=5, read_timeout=10)
cursor = conn.cursor()
cursor.execute("SELECT @@hostname")
result = cursor.fetchone()
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [{mode}] 当前节点: {result[0]}")
cursor.close(); conn.close()
return True
except pymysql.Error as e:
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [{mode}] 操作失败:", e)
return False
mode = sys.argv[1] if len(sys.argv) > 1 else None
port = 6446 if mode == 'rw' else 6447 # rw->主 ro->从
while True:
check_mysql(hostname, port, username, password, database, mode)
time.sleep(1)
附录B: 环境现状快照(2026-08-16 18:30)
集群: myCluster (Single-Primary, status=OK, 可容 1 故障)
PRIMARY = 192.168.195.143 (MYSQL143)
SECONDARY= 192.168.195.141, 192.168.195.142
测试库: cluster_db (orders=8行, order_items=6行, 三节点一致)
账号: root/123456(远程), cluster_admin/cluster_pass(管理), test_user/123456(业务)
Router: 141:6446-6449 + 142:6446-6449 (双 Router, systemd 常驻, listRouters 可见)
组通信端口: 33061 | 元数据库: mysql_innodb_cluster_metadata
md5: server=c17075... shell=7332666... router=0dcddd4...
本笔记所有命令均为实测执行、输出为真实回显。按第四章起逐步复现即可从零搭出同款集群。