高可用架构方案¶
高可用架构方案 一句话口诀
单点不可怕,可怕的是没切换方案——高可用 = 监控 + 自动故障转移 + 数据冗余。
MHA / Orchestrator / MGR / RDS 四选一:按运维成本、一致性、切换时间三维度选型。
读写分离必处理主从延迟——强一致读走主库,写后读用 GTID 等待。
GTID 是跨主从一致性读的通用抓手——WAIT_FOR_EXECUTED_GTID_SET 比 Seconds_Behind_Master 可靠。
连接池不是越大越好——(核数 × 2) + 磁盘数 是经验公式,过多连接只会拖垮主库。
📖 边界声明:本文聚焦 "MySQL 架构层的高可用选型与读写分离机制",以下主题请见对应专题:
- Binlog / GTID 的底层格式、Event 类型、并行复制模型 → Binlog与主从复制
- 主从延迟飙升的现场排查 checklist → 实战问题与避坑指南 §坑 15~17
- 单机事务隔离级别、MVCC 原理 → 事务与并发控制
1. 类比:高可用像飞机的"发动机冗余"¶
民航客机必配双发动机 + 自动切换逻辑——单发动机坏了另一台能顶住,这就是"高可用"三要素的生活投影:
| 飞机场景 | MySQL 高可用对应 | 核心作用 |
|---|---|---|
| 仪表盘告警发动机异常 | 监控(心跳 / 健康检查 / Agent) | 先发现才能谈切换 |
| 副机长自动接管 | 故障转移(Failover:MHA/Orchestrator 切主) | 主挂了要在秒级选出新主 |
| 两台发动机都有完整燃油 | 数据冗余(主从复制 / MGR 多副本) | 切换过去还能跑 |
| 副机长要有飞行记录才能接管 | 候选主库追平 Binlog / 选 GTID 最全者 | 数据一致性底线 |
| 塔台雷达告诉地面哪架飞机是正班 | VIP 漂移 / 中间件路由(ProxySQL / MaxScale) | 业务不用感知切换 |
| "两台发动机同时坏"才算彻底故障 | 半同步 + 跨机房多副本 | 降低 RPO(数据丢失窗口) |
| 双人驾驶舱但听一人指挥 | 主从异步复制(1主多从) | 可用性高但有复制延迟 |
| 并列双机长投票决策 | MGR / Galera(多主强一致) | 一致性高但吞吐量受限 |
一句话:高可用的本质 = "多一份数据副本 + 发生故障时按既定规则换一个副本服务"——三大难点永远是「怎么监控 / 切给谁 / 业务怎么无感」。本文每一节都是在拆解 4 种主流架构对这三问的不同答法。
2. 它解决了什么问题?¶
单点 MySQL 存在单点故障风险,高可用架构解决:
- 故障自动切换:主库宕机后,自动提升从库为新主库,业务无感知
- 读写分离:读请求分发到从库,降低主库压力
- 数据冗余:多副本保证数据不丢失
3. 高可用方案对比¶
| 方案 | 切换时间 | 数据一致性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 主从 + MHA | 30s~2min | 可能丢少量数据 | 中 | 中小规模,成本敏感 |
| 主从 + Orchestrator | 10s~30s | 可能丢少量数据 | 中 | 大规模,自动化运维 |
| MGR(组复制) | 5s~10s | 强一致 | 高 | 对一致性要求高 |
| 云数据库 RDS | 秒级 | 强一致 | 低 | 云上业务,省运维 |
📖 术语家族:MySQL 高可用方案
字面义:High Availability = 高可用,指系统在故障时仍能对外提供服务的能力。 在 MySQL 生态中的含义:通过冗余节点 + 故障自动切换 + 数据一致性协议实现 "主库宕机 → 秒级到分钟级恢复" 的能力,不同方案在 切换时间 / 一致性 / 运维复杂度 三维度间权衡。 同家族成员:
| 成员 | 角色定位 | 技术本质 |
|---|---|---|
MHA(Master High Availability) | 主从架构的故障切换管理器 | Perl 脚本 + SSH 互信,基于 Binlog 补全数据 |
Orchestrator | 复制拓扑的可视化管理平台 | Go 语言,Web UI + REST API,与 Consul/ZK 集成 |
MGR(Group Replication) | MySQL 官方的多副本一致性集群 | 基于 Paxos 变种协议,原生嵌入 mysqld |
ProxySQL | SQL 层代理,负责读写分离与连接池 | C++ 高性能代理,基于 SQL 规则路由 |
VIP(Virtual IP) | 网络层切换抓手,应用无感知切换 | Keepalived / 云厂商 SLB 实现漂移 |
RDS / PolarDB | 云厂商托管方案,高可用能力内置 | 底层多用 Paxos/Raft,用户侧免运维 |
Consul / ZooKeeper | 分布式协调服务,给 Orchestrator 提供一致性仲裁 | Raft / ZAB 协议 |
命名规律:*Manager / Orchestrator = 监控切换器;*Replication = 数据复制协议实现;*Proxy = SQL 代理层;RDS/PolarDB = 云托管形态。按"监控 + 协议 + 代理 + 托管"四层即可归类任何高可用产品。
4. 主从复制 + MHA¶
MHA(Master High Availability)是最经典的 MySQL 高可用方案。
架构¶
flowchart TB
subgraph MHA 架构
MHA["MHA Manager\n(监控主库)"]
M["主库 Master"]
S1["从库 Slave1\n(候选主库)"]
S2["从库 Slave2"]
VIP["VIP(虚拟 IP)"]
end
App["应用"] --> VIP --> M
MHA -->|监控| M
M -->|复制| S1
M -->|复制| S2 故障切换流程¶
sequenceDiagram
participant MHA as MHA Manager
participant M as 主库(宕机)
participant S1 as 从库1(候选主)
participant S2 as 从库2
participant App as 应用
MHA->>M: 检测到主库不可达
MHA->>S1: 从 Relay Log 补全数据
MHA->>S1: 提升为新主库
MHA->>S2: 指向新主库
MHA->>App: 切换 VIP 到新主库
App->>S1: 连接新主库,业务恢复 MHA 的局限:
- 依赖 SSH 互信,运维复杂
- 切换时间较长(30s~2min)
- 不支持多主架构
Fencing + 数据补全的源码依据¶
MHA 切换成否的两个核心动作都落在 Perl 脚本里,理解它们才能写出不数据分叉的 failover 流程。
动作①:数据补全,对应源码 apply_diff_relay_logs(MHA::ManagerUtil 模块):
# mha4mysql-manager中 apply_diff_relay_logs 核心逻辑(精简)
# 1. 比较所有存活从库的 Relay Log 位点,找到最新的「位点领先者」
my $latest = find_latest_slave(@alive_slaves);
# 2. 从「领先者」拷贝缺失的 Relay Log 到「候选新主」
scp $latest:$relay_log $candidate:/tmp/
# 3. 在候选新主上回放缺失的 Binlog Event
mysqlbinlog /tmp/relay-xxxxx.log | mysql -u... -p...
动作②:Fencing 脚本,在 masterha_manager 配置的 master_ip_failover_script:
#!/usr/bin/env perl
# master_ip_failover 脚本示例(官方样例 + Fencing 加固)
use strict;
use Getopt::Long;
my ($command, $orig_master_host, $new_master_host, $new_master_ip, $new_master_port);
GetOptions(
'command=s' => \$command,
'orig_master_host=s' => \$orig_master_host,
'new_master_host=s' => \$new_master_host,
'new_master_ip=s' => \$new_master_ip,
'new_master_port=s' => \$new_master_port,
);
if ($command eq "stop" || $command eq "stopssh") {
# ⭐ Fencing 三连击:必须保证老主不再写入
# 1. 先试 SSH 关 mysqld
my $rc = system("ssh -o ConnectTimeout=5 root\@$orig_master_host 'systemctl stop mysqld'");
# 2. SSH 不通则调 IPMI 强制断电(STONITH)
if ($rc != 0) {
system("ipmitool -H $orig_master_host -U ADMIN -P ADMIN power off");
}
# 3. 最后再把 VIP 从老主剔除
system("ssh root\@$orig_master_host 'ip addr del $vip/24 dev eth0'");
exit 0;
}
elsif ($command eq "start") {
# 在新主上绑定 VIP
system("ssh root\@$new_master_host 'ip addr add $vip/24 dev eth0'");
system("ssh root\@$new_master_host 'arping -U -c 3 -I eth0 $vip'"); # 广播 ARP
exit 0;
}
| 阶段 | 源码依据 | 关键动作 |
|---|---|---|
| 监控主库 | MHA::HealthCheck | SSH + MySQL ping 双重心跳,默认 ping_interval=3s、secondary_check_script 防误判 |
| 数据补全 | apply_diff_relay_logs | 从「位点领先的存活从库」拾贝 Relay Log Event 到候选新主 |
| 候选人选举 | MHA::MasterFailover::select_new_master | 优先选 candidate_master=1 的节点,次选 GTID / Binlog 位点最新 |
| Fencing | master_ip_failover_script + shutdown_script | 用户必须自定义,MHA 只挂钩不代实现 |
MHA 0.58 已于 2019 停止维护
原作者离开 DeNA,MHA 进入社区维护模式(yoshinorim/mha4mysql-manager),新项目推荐直接用 Orchestrator 或 MGR。如需维护存量 MHA,要手动适配 MySQL 8.0 的认证插件(caching_sha2_password)与 GTID 表模式。
5. 半同步复制:AFTER_SYNC vs AFTER_COMMIT¶
MySQL 默认异步复制——主库提交即返回客户端,Binlog 是否到达从库完全看缘分。一旦主库宕机、从库没收到最后几笔 Binlog,就会直接丢数据。半同步复制(rpl_semi_sync)是弥补该缺口的内置机制,但其何时等待从库 ACK有两种模式,语义差异直接决定数据丢失风险。
sequenceDiagram
participant C as 客户端
participant M as 主库
participant S as 从库
participant L as 主库存储引擎
Note over M,S: AFTER_SYNC(MySQL 5.7+ 默认,推荐)
C->>M: COMMIT
M->>M: 写 Binlog
M->>S: 发 Binlog
S-->>M: ACK(已落 Relay Log)
M->>L: 引擎层提交(可见)
M-->>C: OK
Note over M,S: AFTER_COMMIT(5.5 老模式,不推荐)
C->>M: COMMIT
M->>M: 写 Binlog
M->>L: 引擎层提交(其他事务已可见)
M->>S: 发 Binlog
S-->>M: ACK
M-->>C: OK | 对比项 | AFTER_SYNC(无损半同步) | AFTER_COMMIT(有损半同步) |
|---|---|---|
| ACK 等待时机 | 引擎层提交前等 | 引擎层提交后等 |
| 主库崩溃后果 | 未 ACK 事务对其他会话不可见,切换后数据一致 | 未 ACK 事务已可见,但从库没收到 → 幻读 / 数据丢失 |
| 性能 | 略慢(多一次网络往返串行在提交路径上) | 略快(ACK 与提交并行) |
| 默认值 | MySQL 5.7+ rpl_semi_sync_master_wait_point = AFTER_SYNC | MySQL 5.5 的历史默认 |
| 生产推荐 | ✅ 必选 | ❌ 弃用 |
超时降级机制:
-- 从库 ACK 超时(默认 10 秒)后,主库自动降级为异步复制继续对外服务
-- 这是半同步的底线保护:避免从库全挂导致主库不可写
SET GLOBAL rpl_semi_sync_master_timeout = 10000; -- 单位 ms
-- ⚠️ 降级期间写入的事务在主库宕机时仍可能丢失
-- 金融场景可设置 rpl_semi_sync_master_wait_no_slave=ON,从库恢复才继续写
半同步不是强一致
半同步只保证 Binlog 到达从库 Relay Log,不保证从库 SQL 线程回放完成。切换到从库后,SQL 线程还有回放积压时,读到的仍是旧数据。要真正强一致,选 MGR。
6. Orchestrator:自动化故障切换¶
Orchestrator 是 GitHub 开源的 MySQL 拓扑管理工具,支持自动发现、可视化、自动故障切换。
flowchart LR
Orch["Orchestrator\n(Web UI + API)"]
M["主库"]
S1["从库1"]
S2["从库2"]
S3["从库3(级联)"]
Orch -->|监控| M
Orch -->|监控| S1
Orch -->|监控| S2
M --> S1
M --> S2
S2 --> S3 核心特性:
- 自动发现复制拓扑
- Web UI 可视化拓扑结构
- 支持复杂拓扑(级联复制、多从库)
- 与 Consul/ZooKeeper 集成实现分布式协调
- 支持 GTID 和传统复制
选主决策流程(Orchestrator 内部)¶
flowchart TD
A["主库心跳丢失<br/>超过 InstancePollSeconds"] --> B{"Raft 仲裁:<br/>多数 Orchestrator 节点<br/>都看不到主库?"}
B -->|否:单节点误判| X["忽略,不切换"]
B -->|是:确认宕机| C["扫描所有从库"]
C --> D{"候选从库评分:<br/>1. Binlog 位点最新<br/>2. 未配置 log_slave_updates=0<br/>3. 数据中心就近<br/>4. promotion_rule=prefer"}
D --> E["选出 Candidate"]
E --> F["补全 Binlog 差异<br/>(从其他从库拉最新 Relay Log)"]
F --> G["RESET SLAVE ALL<br/>提升为新主"]
G --> H["其他从库 CHANGE MASTER<br/>指向新主"]
H --> I["调用外部 Hook:<br/>切 VIP / 改 ProxySQL / 发钉钉"]
I --> J["完成,平均 10~30s"] 防误切的三道门:
- Raft 仲裁:Orchestrator 集群自身用 Raft 共识,需多数节点确认主库宕机才触发切换,避免单点网络抖动误判
- 反熵检查:切换前要求候选从库与其他从库 Binlog 位点差异不超过配置阈值,否则拒绝切换
- 冷却期(
RecoveryPeriodBlockSeconds):一次切换后进入冷却窗口,禁止连续切换造成雪崩
7. MGR:MySQL Group Replication¶
MGR 是 MySQL 官方的高可用方案,基于 Paxos 协议实现强一致性。
工作原理¶
flowchart LR
subgraph MGR["MGR 集群 单主模式"]
M["主节点<br/>读写"]
S1["从节点1<br/>只读"]
S2["从节点2<br/>只读"]
end
T["事务提交"] -->|1. 广播| M
M -->|2. Paxos 协议| S1
M -->|2. Paxos 协议| S2
S1 -->|3. 多数派确认| M
S2 -->|3. 多数派确认| M
M -->|4. 提交| T 两种模式:
- 单主模式:只有一个主节点可写,从节点只读,主节点故障自动选举新主
- 多主模式:所有节点都可写,需要处理写冲突,适合特殊场景
MGR 的要求:
- 至少 3 个节点(保证多数派)
- 网络延迟要低(Paxos 协议对网络敏感)
- 不支持外键级联操作(可能导致冲突)
Paxos 变种(XCom)协议细节¶
MGR 底层不是教科书 Paxos,而是 MySQL 自研的 XCom(eXtended Communication),属于 Multi-Paxos 变种。一次事务提交的共识过程可拆为 5 步:
sequenceDiagram
participant C as 客户端
participant P as Primary(Proposer)
participant A1 as Secondary1(Acceptor)
participant A2 as Secondary2(Acceptor)
C->>P: COMMIT
P->>P: ① 写 Binlog(未提交)<br/>生成 write_set(主键哈希集)
P->>A1: ② Propose:广播事务到组
P->>A2: ② Propose:广播事务到组
A1-->>P: ③ Accept(仅校验合法性,不冲突检测)
A2-->>P: ③ Accept
Note over P,A2: ④ 多数派达成 → 所有节点本地<br/>并行做冲突检测(write_set 比对)
P->>P: ⑤ 通过 → 引擎层提交<br/>未通过 → 回滚(仅多主模式会)
P-->>C: OK 关键设计:
| 设计点 | 说明 |
|---|---|
| Write Set 冲突检测 | 多主模式下,MGR 收集事务修改的主键哈希集合,提交时比对——谁先到达多数派谁赢,后到的同主键事务回滚("乐观冲突") |
| 证书数据库(Certification DB) | 每个节点保存近期所有事务的 write_set 与 GTID,用于判断新事务是否与"并发已提交事务"冲突 |
| Binlog 在 Paxos 之后写 | 事务先达成共识再落 Binlog,因此所有节点的 Binlog 顺序全局一致(这是 MGR 与主从最本质的区别) |
| 流控(Flow Control) | 某节点 applier 或 certifier 队列积压时,主动降速 Proposer,避免慢节点脱群 |
| 仅支持 InnoDB + 主键 | 无主键表无法生成 write_set,直接拒绝写入 |
MGR vs Galera Cluster
两者都是 "Paxos 变种 + write_set 冲突检测" 思路:Galera 用 wsrep 协议,MySQL 官方未收编(走 MariaDB / Percona XtraDB Cluster 路线);MGR 是 Oracle 官方嫡系,8.0 后默认可用。选型原则:MySQL 8.4 LTS 用 MGR,Percona 生态可评估 PXC(Galera)。
8. 脑裂(Split-Brain)场景推演¶
脑裂 = 网络分区导致集群分裂成两个"自以为是主"的子集,是高可用系统最危险的故障形态——两个"主"各自接受写入,恢复后数据无法合并。
场景①:MHA / 主从架构下的脑裂¶
flowchart TB
subgraph 正常["正常状态"]
M1["主库 M(持有 VIP)"]
S1["从库 S"]
M1 -. 复制 .-> S1
end
subgraph 分区["网络分区后"]
MA["主库 M<br/>(VIP 还在,仍可写)"]
MHA["MHA<br/>(从监控侧看不到 M)"]
SB["从库 S → 被提升为新主<br/>(VIP 也被切到 S)"]
MHA -->|误判| SB
end
正常 -->|M 与 MHA 网络断开<br/>但 M 与客户端正常| 分区 | 阶段 | 两侧写入情况 | 后果 |
|---|---|---|
| 分区期间 | M 侧客户端继续写老主;MHA 侧应用被切到新主 S,也在写 | 两份分叉数据,无法合并 |
| 网络恢复 | 老主 M 发现自己不是主了,拒绝同步;人工介入 | 业务需要选择"保 M 的数据还是保 S 的数据",另一侧数据直接丢弃 |
预防手段(按有效性排序):
- Fencing(围栏):切换前先 SSH 登录老主执行
shutdown或iptables DROP 3306,确保老主彻底不可写——MHA 的master_ip_failover_script就要写这段 - VIP + 硬件仲裁:VIP 绑定由独立 Keepalived 集群管理,仅多数派可持有 VIP
- Quorum 检查:MHA / Orchestrator 切换前要求"能连上多数从库",避免单点误判
- Raft/Consul 仲裁:Orchestrator 集群本身走 Raft,单个 Orchestrator 看不到主库不触发切换
场景②:MGR 的脑裂免疫性¶
MGR 天生通过 Paxos 多数派规避脑裂——任何分区只有"含多数派的那一侧"能继续写:
flowchart LR
subgraph P1["分区 A(2 节点,少数)"]
direction TB
N1["Node1<br/>❌ 无法达成多数派<br/>进入 ERROR 状态,拒绝写"]
N2["Node2<br/>❌ 同上"]
end
subgraph P2["分区 B(3 节点,多数)"]
direction TB
N3["Node3 ✅"]
N4["Node4 ✅"]
N5["Node5 ✅ 持续对外写"]
end MGR 的偶数节点陷阱
部署 偶数节点(如 2、4、6)时,一旦 1:1 对半分区,双方都无法达成多数派,全集群变只读。这就是"MGR 至少 3 节点、推荐 5 节点"的根因——保证任意一次单机房故障仍有多数派存活。
场景③:读写分离下的脑裂放大¶
即使数据库层没脑裂,ProxySQL / VIP 侧的配置漂移也会造成"应用层脑裂"——一半应用连老主、一半连新主。根治手段:切换时同步刷新 ProxySQL mysql_servers + VIP + DNS,并短暂阻塞写入(几秒),让所有应用感知到切换。
9. 读写分离¶
方案一:应用层实现¶
// 使用 AbstractRoutingDataSource(Spring)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource routingDataSource() {
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put("master", masterDataSource());
dataSources.put("slave", slaveDataSource());
RoutingDataSource routing = new RoutingDataSource();
routing.setTargetDataSources(dataSources);
routing.setDefaultTargetDataSource(masterDataSource());
return routing;
}
}
// 注解标记读操作走从库
@ReadOnly // 自定义注解,AOP 切换数据源
public List<Order> queryOrders() { ... }
优点:灵活,可精细控制
缺点:业务代码侵入,需要处理主从延迟
方案二:ProxySQL(推荐)¶
ProxySQL 是一个高性能 MySQL 代理,支持读写分离、连接池、查询路由。
flowchart LR
App["应用"] --> ProxySQL["ProxySQL\n(代理层)"]
ProxySQL -->|写操作| M["主库"]
ProxySQL -->|读操作| S1["从库1"]
ProxySQL -->|读操作| S2["从库2"] -- ProxySQL 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup)
VALUES
(1, 1, '^SELECT.*FOR UPDATE', 0), -- SELECT FOR UPDATE 走主库(hostgroup 0)
(2, 1, '^SELECT', 1); -- 普通 SELECT 走从库(hostgroup 1)
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;
ProxySQL 核心功能:
- 自动读写分离(基于 SQL 规则)
- 连接池(减少连接开销)
- 查询缓存
- 慢查询监控
- 主从延迟检测(延迟过大时自动将从库摘除)
10. 主从延迟下的一致性读¶
读写分离后,写主库后立即读从库可能读到旧数据。
解决方案¶
flowchart TD
Write["写主库"] --> Strategy{"一致性要求?"}
Strategy -->|强一致| ReadMaster["直接读主库\n(支付、库存)"]
Strategy -->|最终一致| ReadSlave["读从库\n(商品列表、统计)"]
Strategy -->|等待同步| WaitGTID["WAIT_FOR_EXECUTED_GTID_SET\n等待从库追上再读"] -- 方案:写入后获取 GTID,读从库时等待该 GTID 被执行
-- 主库写入后
SELECT @@GLOBAL.gtid_executed; -- 获取当前 GTID
-- 从库读取前等待
SELECT WAIT_FOR_EXECUTED_GTID_SET('3E11FA47...:1-100', 5);
-- 返回 0 表示成功,返回 1 表示超时(5秒)
📖 术语家族:GTID
字面义:Global Transaction Identifier = 全局事务标识符,格式 <source_uuid>:<transaction_id>(例:3E11FA47-71CA-11E1-9E33-C80AA9429562:23)。 在 MySQL 中的含义:每个提交的事务在整个复制拓扑中拥有唯一 ID,从库以此为键判断"这个事务是否已经回放",取代了传统的《文件名 + 偏移量》位点。 同家族成员:
| 成员 | 语义 | 使用时机 |
|---|---|---|
gtid_executed | 当前实例已执行完成的所有 GTID 集合 | 从库判断自己追到哪里、选主时选「gtid_executed 最大的从库」 |
gtid_purged | 已被 主库 Binlog purge 掉的 GTID 集合 | 新从库加入时 gtid_executed ⊆ gtid_purged 会报错没法追 |
gtid_owned | 当前正在执行的事务 GTID | 实时监控、调试死锁时查谁持有哪些事务 |
WAIT_FOR_EXECUTED_GTID_SET(gtid, timeout) | 阻塞等待目标 GTID 被本实例执行完 | 读写分离强一致读:写完主库拿到 GTID,读前等从库追到 |
WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS | 5.7.7- 的旧名字 | 已废弃,8.0+ 统一用上者 |
sql_slave_skip_counter | 跳过从库下一个事务 | GTID 模式下已失效,改用 SET GTID_NEXT 人工注入空事务跳过 |
source_uuid / auto.cnf | MySQL 实例的全球唯一 ID | 写在 datadir/auto.cnf,克隆 datadir 后必须删此文件重生 |
命名规律:gtid_<状态名> = MySQL 全局变量观察 GTID 系统状态;WAIT_FOR_* = 阻塞等待类函数;*_uuid = 实例身份标识。看到 gtid_ 前缀就知道是复制位点信息,source_* 的翻译是"以前叫 master_*"(MySQL 8.0.22 开始替换 master/slave 为中性语)。
📖 GTID 的底层格式(
UUID:transaction_id)、gtid_executed/gtid_purged的差异与 Binlog 的写入时机详见 Binlog与主从复制 §GTID 模式,本文不再展开。📖 主从延迟常见的三种线上现象(从库卡在 SBM=0 却查不到最新数据 / SBM 持续增长 / 大事务阻塞并行复制)与具体排查 checklist详见 实战问题与避坑指南 §坑 15~17,本文仅讨论机制层的一致性读方案。
11. 连接池配置¶
# 连接池关键参数
最大连接数 = (CPU核数 * 2) + 磁盘数
# 例如:8核 + 1块磁盘 = 17个连接
# 常见连接池配置(HikariCP)
maximumPoolSize: 20 # 最大连接数
minimumIdle: 5 # 最小空闲连接
connectionTimeout: 30000 # 获取连接超时(30s)
idleTimeout: 600000 # 空闲连接超时(10min)
maxLifetime: 1800000 # 连接最大存活时间(30min)
为什么连接数不是越多越好:每个连接都消耗内存(约 1MB),过多连接导致上下文切换开销增大,反而降低吞吐量。
📖 术语家族:HikariCP 连接池参数
字面义:HikariCP = 日语「Hikari」(光,光之透明)+ Connection Pool,强调极高性能与极低开销。Spring Boot 2.0+ 默认连接池。 在连接池中的含义:所有参数绕「连接生命周期 + 池容量调度 + 负载测试」三条主线设计,理解每个参数的定位才能科学调优。 同家族成员:
| 成员 | 作用 | 取值建议 |
|---|---|---|
maximumPoolSize | 池内最大连接数(包含活跃与空闲) | (CPU核 × 2) + 磁盘数;忌设过大 |
minimumIdle | 保证在池的最少空闲连接 | 默认 = maximumPoolSize(官方建议固定大小) |
connectionTimeout | 从池借到连接的最大等待时间 | 30000ms;超时抛 SQLTimeoutException |
idleTimeout | 连接在池内空闲的最大时间,超过被回收 | 600000ms = 10 分钟 |
maxLifetime | 连接总存活时间,不管是否活跃到时强制重建 | < MySQL wait_timeout - 30s,默认 1800000ms = 30 分钟 |
leakDetectionThreshold | 怀疑泄漏的阈值,连接借出超过时写 warn 日志 | 30000ms,默认 0 (关闭),注意不是自动回收 |
validationTimeout | 健康检测超时 | 5000ms,一定小于 connectionTimeout |
connectionTestQuery | 手动指定健康检查 SQL | JDBC 4 下留空,用驱动原生 isValid() |
keepaliveTime | 对空闲连接定时跑健康检查,防网关断链 | 4.0.2+,120000ms,0 = 关闭 |
命名规律:*PoolSize = 池容量;*Timeout = 阻塞等待阈值;*Lifetime = 资源生命周期;*Threshold = 监控告警阈值。看后缀就知道参数是调容量、调超时还是调生命周期。
为什么 (核数 × 2) + 磁盘数?(来自 PostgreSQL 源码经验公式)¶
这个公式出自 HikariCP 作者 Brett Wooldridge 翻译的 PostgreSQL 研发经验 (HikariCP About Pool Sizing):
- CPU 核数 × 2:每个核同一时刻最多执行两个有意义的线程(一个跑 CPU、一个等 I/O)
- 磁盘数:补偿 I/O 等待时的额外并行度,SSD 可以算作 1 块磁盘以上,但不要超过
核数 / 2 - 核心反直觉结论:Oracle 官方测试过 10000 RPS 场景:连接数从 2048 降到 96 后,等待时间从 33ms 降到 < 1ms——连接不是越多越好,连接数超过 MySQL 能真正并行执行的范围时,上下文切换与锁竞争会把吞吐量拖垄
- 和 MySQL 竞争:
innodb_thread_concurrency默认 0(不限),但过多并发会触发内部锁争用;连接数 * 应用实例数 不能超过max_connections(默认 151)
maxLifetime 必须严格小于 MySQL wait_timeout
MySQL 服务端发现连接空闲超过 wait_timeout(默认 28800s)时会主动发 FIN 包关掉该连接;若连接池的 maxLifetime 设得比 wait_timeout 还长,池内就会存在"服务端已关、应用端以为还活着"的僵尸连接,下次借出执行 SQL 就报 Communications link failure。 铁律:maxLifetime ≤ wait_timeout - 30s(HikariCP 源码 HouseKeeper 在连接到期前主动 evict,避免边界竞争)。
12. 不理解底层会踩的坑¶
高可用架构的坑几乎都发生在"平时好好的、切换时才爆"——也就是说只有理解了底层机制才躲得掉。以下 5 个坑按"后果严重度"排序。
坑 1:Failover 不做 Fencing,老主复活写入造成数据分叉¶
现象:MHA / Orchestrator 把从库提升为新主后,老主机器重启或网络恢复,它不知道自己已被废黜,继续持有 VIP 之外的连接(比如内网直连 IP 的监控脚本、运维跳板机),继续接受写入。等运维人员发现时,老主与新主已经各写入了几百条分叉事务。
根因:MHA 的 master_ip_failover_script 默认只负责切 VIP,不负责关停老主 mysqld。VIP 切走后,老主的 mysqld 实例依然在监听 3306,任何绕过 VIP 的连接都能写入。
避坑:Failover 脚本必须内置 Fencing 三连击(见 §4):
- SSH 登录老主执行
mysqladmin shutdown或systemctl stop mysqld - 若 SSH 不通,调用 IPMI / 云厂商 API 强制断电(STONITH: Shoot The Other Node In The Head)
- 两者都失败则拒绝切换(宁可不可用也不要数据分叉)
坑 2:MGR 部署 2 / 4 / 6 偶数节点,对半分区全集群只读¶
现象:4 节点 MGR 集群跨两机房部署(A 机房 2 节点 + B 机房 2 节点),机房间专线中断,两侧都无法形成多数派(2 < 3),集群整体变为只读,业务写入全部失败。
根因:Paxos 需要 N/2 + 1 节点达成多数派,4 节点需要 3 节点在线。对半分区时两侧都只有 2 节点,两侧都不达成多数派——这正是 MGR 的防脑裂机制,但代价是可用性归零。
避坑:
- MGR 永远部署奇数节点(3 / 5 / 7)
- 跨 3 机房(2-2-1 或 1-1-1),避免任何单机房掌握多数派
- 预算只够 4 节点时,不如选 3 节点(容错能力相同、资源省 25%)
坑 3:Seconds_Behind_Master = 0 不等于"从库追上了主库"¶
现象:读写分离路由策略写死"SBM = 0 就路由读请求到从库",结果用户付款后立即查订单列表查不到刚下的单——主从复制明明显示 SBM = 0。
根因:Seconds_Behind_Master 的计算逻辑是「从库 SQL 线程正在执行的 event 的 timestamp 与 从库当前时间的差」——若 IO 线程都没把 Binlog 拉过来,SQL 线程根本没 event 可执行,SBM 直接显示 0,但数据其实还没到。
避坑:
- 强一致读不要信
SBM,改用WAIT_FOR_EXECUTED_GTID_SET('<主库GTID>', timeout)等待具体事务回放(见 §10) - ProxySQL 判断从库健康:同时看
SBM和Master_Log_File/Master_Log_Pos与主库的位点差 - 业务层:写后立即读的场景直接走主库,不要绕路
坑 4:连接池 maxLifetime 超过 MySQL wait_timeout,报 "connection reset"¶
现象:HikariCP 默认 maxLifetime = 30 分钟,而 MySQL 默认 wait_timeout = 28800s 看似没问题——但线上 MySQL 运维把 wait_timeout 改成了 600s(10 分钟)来释放闲置连接,忘了通知应用方。结果应用侧连接池里存着一堆"MySQL 端已经关闭、应用端还以为活着"的僵尸连接,下次借出去执行 SQL 时直接报 Communications link failure / Connection reset。
根因:MySQL 的 wait_timeout 是"非交互式连接被服务端强制关闭的空闲时间",一旦超时服务端发 FIN 包并清理该连接,但应用侧的 TCP 连接在某些 NAT / keepalive 场景下不会立刻感知到,仍以为连接可用。HikariCP 等连接池的 maxLifetime 是连接在池内的最大存活时间——如果比 wait_timeout 还长,就会把已经被服务端关掉的连接借给业务用。
避坑:
- 铁律:
maxLifetime必须严格< wait_timeout - 30s(留余量避免边界竞争) - HikariCP 开启
connectionTestQuery = SELECT 1或启用 JDBC 4 的isValid()自动借出前检测(默认已开) - 大规模场景接 ProxySQL 做连接池复用,减少应用侧 MySQL 直连数
坑 5:ProxySQL / VIP 配置漂移导致"应用层脑裂"¶
现象:数据库层 Failover 成功(老主被 Fencing、新主已起来),但一半应用仍然连着老主——是因为运维脚本只刷新了 ProxySQL 的 mysql_servers,忘了推 DNS 变更,部分应用使用 JVM 内置 DNS 缓存(networkaddress.cache.ttl=-1 永久缓存)解析到了旧地址。
根因:高可用链路上存在多个路由层(DNS → VIP → ProxySQL mysql_servers → MySQL 节点),任何一层配置不同步就会造成"一半连老主、一半连新主"的应用层脑裂;即使数据库层是安全的,业务也会看到部分写入成功、部分失败的诡异现象。
避坑:
- 切换流程原子化:用同一个切换脚本串行刷新所有路由层(ProxySQL → VIP → DNS),任一层失败就回滚
- 应用侧 JVM 参数显式配置:
-Dnetworkaddress.cache.ttl=60(DNS 缓存 60s,别用永久缓存) - 切换时短暂阻塞写入(2~5s),等所有路由层收敛再放流量
13. 常见问题¶
📖 排查题已外链:"主从延迟飙升怎么定位" / "从库读到旧数据根因分析" / "MGR 脑裂排查" 等排查类问题请见 实战问题与避坑指南 §坑 15~17;本文专注选型题。
Q:MHA 和 MGR 如何选择?
中小规模、成本敏感选 MHA,运维简单,社区成熟;对数据一致性要求高、能接受运维复杂度选 MGR,强一致性,官方支持。云上业务直接用 RDS,省去运维成本。
Q:读写分离后如何处理主从延迟导致的读旧数据问题?
① 强一致性场景(支付、库存)直接读主库;② 可接受延迟的场景读从库;③ 写后立即读的场景,用 GTID 等待从库同步完成;④ 业务层加缓存,减少对数据库的实时读依赖。
Q:ProxySQL 和应用层读写分离如何选择?
ProxySQL 对业务代码无侵入,支持动态配置,适合大多数场景;应用层读写分离更灵活,可以精细控制哪些查询走主库,适合有特殊需求的场景。两者也可以结合使用。
Q:AFTER_SYNC 半同步、MGR、RDS 三种强一致方案如何选型?
先看集群规模与一致性上限:① 单主 + 半同步
AFTER_SYNC适合"主从架构已经跑着、只想加一层数据不丢"的老系统,成本最低,但 ACK 超时会自动降级为异步,不是严格强一致;② MGR 适合新系统或对一致性/自动切换要求高的场景,Paxos 多数派天然免脑裂,但至少 3 节点、对网络抖动敏感;③ RDS/PolarDB 是云上首选,一致性/切换/备份全部托管,代价是被厂商锁定。金融核心走 MGR(至少 5 节点跨机房)+ 半同步做兜底。
Q:为什么偶数个 MGR 节点是反模式?
MGR 依赖 Paxos 多数派达成共识,3 节点容忍 1 个故障、5 节点容忍 2 个故障;但 2/4/6 偶数节点在"对半分区"场景下,两侧都无法形成多数派,整个集群退化为只读。同样的成本(比如 4 节点)不如布 3 节点(容错能力相同、资源更省)或 5 节点(容错能力翻倍)。
Q:WAIT_FOR_EXECUTED_GTID_SET 内部怎么等?是轮询还是条件变量?
条件变量——源码在
sql/rpl_gtid_state.cc::Gtid_state::wait_for_gtid_set。工作流程:① 线程调用时先拿commit_group_sidno_locks的对应 sidno 锁;② 比较目标 GTID 集合与gtid_executed,如果已包含直接返回 0;③ 否则在Gtid_state::sidno_locks的条件变量上wait;④ 每次事务提交时Gtid_state::update_on_commit会broadcast所有等待线程;⑤ 被唤醒后重新比较,不符合就继续等,符合就返回。因此它不占 CPU,也不会错过事件,timeout到时返回 1。
14. 一句话口诀(加强记忆版)¶
高可用架构方案 加强记忆版口诀
高可用三要素 = 监控 + 故障转移 + 数据冗余——缺一不可:没监控切不了,没冗余切过去也没数据,没自动化靠人工 30 分钟才反应过来。
选型口诀:"MHA 省钱、Orchestrator 省心、MGR 强一致、RDS 省运维"——按运维团队规模与一致性需求四选一。
AFTER_SYNC 是半同步的唯一正确答案——提交前等 ACK,老主崩溃后未 ACK 事务对其他会话不可见,避免幻读与数据丢失。
MGR 永远奇数节点:3 节点容忍 1 故障、5 节点容忍 2 故障,偶数节点在对半分区时全集群只读。
Failover 三保险 = Fencing + 仲裁 + 冷却期——老主必须被强制关停,单 Orchestrator 节点看不到主库不能触发切换,切换后进入冷却期禁止连续切换造成雪崩。
强一致读别信 SBM——Seconds_Behind_Master = 0 只说明 SQL 线程没积压,不保证 Binlog 已到达从库;用 WAIT_FOR_EXECUTED_GTID_SET 等具体事务。
连接池公式:(核数 × 2) + 磁盘数,且 maxLifetime < wait_timeout - 30s——MySQL 会主动断开闲置连接,连接池生命周期必须更短才能规避僵尸连接。