跳转至

高可用架构方案

高可用架构方案 一句话口诀

单点不可怕,可怕的是没切换方案——高可用 = 监控 + 自动故障转移 + 数据冗余。

MHA / Orchestrator / MGR / RDS 四选一:按运维成本、一致性、切换时间三维度选型。

读写分离必处理主从延迟——强一致读走主库,写后读用 GTID 等待。

GTID 是跨主从一致性读的通用抓手——WAIT_FOR_EXECUTED_GTID_SETSeconds_Behind_Master 可靠。

连接池不是越大越好——(核数 × 2) + 磁盘数 是经验公式,过多连接只会拖垮主库。

📖 边界声明:本文聚焦 "MySQL 架构层的高可用选型与读写分离机制",以下主题请见对应专题:


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_logsMHA::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=3ssecondary_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),新项目推荐直接用 OrchestratorMGR。如需维护存量 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"]

防误切的三道门

  1. Raft 仲裁:Orchestrator 集群自身用 Raft 共识,需多数节点确认主库宕机才触发切换,避免单点网络抖动误判
  2. 反熵检查:切换前要求候选从库与其他从库 Binlog 位点差异不超过配置阈值,否则拒绝切换
  3. 冷却期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) 某节点 appliercertifier 队列积压时,主动降速 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 的数据",另一侧数据直接丢弃

预防手段(按有效性排序):

  1. Fencing(围栏):切换前先 SSH 登录老主执行 shutdowniptables DROP 3306,确保老主彻底不可写——MHA 的 master_ip_failover_script 就要写这段
  2. VIP + 硬件仲裁:VIP 绑定由独立 Keepalived 集群管理,仅多数派可持有 VIP
  3. Quorum 检查:MHA / Orchestrator 切换前要求"能连上多数从库",避免单点误判
  4. 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):

  1. SSH 登录老主执行 mysqladmin shutdownsystemctl stop mysqld
  2. 若 SSH 不通,调用 IPMI / 云厂商 API 强制断电(STONITH: Shoot The Other Node In The Head)
  3. 两者都失败则拒绝切换(宁可不可用也不要数据分叉)

坑 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_commitbroadcast 所有等待线程;⑤ 被唤醒后重新比较,不符合就继续等,符合就返回。因此它不占 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 会主动断开闲置连接,连接池生命周期必须更短才能规避僵尸连接。