一句话结论(30s)
MHA 是”主从 + 外部管理器”的异步/半同步高可用方案,本质是用 Manager 脚本驱动五阶段故障切换,因为 MGR 的 Paxos 多数派要求零丢失且复杂度高,MHA 则简单、支持老版本。关键设计是三层脑裂防护:secondary_check_script 多路径仲裁防误切、STONITH 物理断电旧主、VIP 漂移路由流量。权衡在于:MHA 可能丢数据(异步窗口),RPO=0 的金融场景必须换 MGR。
核心原理(2min)
MHA 切换时间线约 8-15 秒:Manager 连续 3 次探测 Master 无响应后,先跑 secondary_check_script 从另一网络路径再确认 Master 是否真死(网络分区则不切换、避免脑裂),再选同步延迟最小的 Slave 为新主并补全差异 binlog,随后所有从库 CHANGE MASTER 指向新主,最后 VIP 漂移 + ARP 广播刷新 MAC 表。防脑裂靠 STONITH:切换后旧 Master 可能复活形成双主,需通过 IPMI 断电、云 API 关机或 iptables 封 3306 把旧主 MySQL 进程彻底杀死,因为 VIP 飘走不代表旧主停止接受直连写入。半同步复制在 Master 宕机后会超时降级为异步,降级窗口内的写入可能丢失。
底层深入(5-10min)
一、MHA vs MGR:架构哲学的根本差异
MHA(Master High Availability)和 MGR(MySQL Group Replication)代表了 MySQL 高可用的两条路线:
| 维度 | MHA | MGR |
|---|---|---|
| 架构 | 主从 + 外部管理器 | 多主/单主 + 内置 Paxos |
| 同步机制 | 异步/半同步复制 | 组复制(Paxos 共识) |
| 数据一致性 | 可能丢数据(异步) | RPO=0(Paxos 多数派) |
| 故障切换 | MHA Manager 脚本驱动 | 组内自动选举 |
| 脑裂防护 | 依赖外部机制(STONITH/VIP) | 多数派自动处理 |
| MySQL 版本 | 5.5-8.0 全部支持 | 5.7+ 支持,8.0 成熟 |
| 复杂度 | 简单、外部依赖少 | 较复杂、网络延迟敏感 |
| 适用规模 | 小到中型(3-10 节点) | 中小型(3-9 节点) |
选型原则:如果业务能接受秒级数据丢失(RPO=0 不是硬要求),且团队对 Paxos 不熟悉,MHA 更简单可靠。如果零数据丢失是硬约束(金融、交易),MGR 是必然选择。
💭 想一想:为什么 MHA 简单却”可能丢数据”?——因为它的复制是异步/半同步的:主库写 binlog 后未必等从库确认就返回了成功,切换时新主可能还缺主库最后那一小段日志。这份”秒级窗口”就是 RPO 不为零的代价;MGR 用 Paxos 多数派强制”多数节点确认才提交”,所以 RPO=0 但复杂度高。
二、MHA 故障切换的完整时间线
T+0s Master 宕机(进程崩溃 / 网络不可达 / 硬件故障)
T+1s MHA Manager 连续 3 次探测 Master 无响应
T+2s MHA Manager 进入故障切换流程
T+3s 【阶段1:确认 Master 确实死亡】
├── 执行 secondary_check_script
│ └── 从另一台机器再次 ping Master → 确认不可达
└── 判断:是网络分区还是 Master 真的crash?
├── 网络分区 → 不做切换(避免脑裂)
└── Master 真死 → 继续
T+4s 【阶段2:选择新 Master】
├── 选出同步延迟最小的 Slave(最接近 Master binlog 位置)
├── 检查半同步状态 → 可能有未应用到 relay log 的事件
└── 对比所有 Slave 的 Relay_Master_Log_File + Exec_Master_Log_Pos
T+5s 【阶段3:补全差异日志】
├── 从存活的 Slave 上拉取最新的 binlog → 应用到新 Master
└── 补全其他 Slave 缺失的日志(通过 relay log 或 binlog)
T+6s 【阶段4:主从切换】
├── 新 Master 执行 CHANGE MASTER → 重置为主库
├── 所有 Slave 执行 CHANGE MASTER TO 新 Master
└── 启动复制
T+7s 【阶段5:VIP 漂移】
├── 旧 Master 的 VIP 释放(如果可达)
├── 新 Master 绑定 VIP
└── 发送 ARP 广播刷新交换机 MAC 表
T+8-10s 应用恢复连接(VIP 流量路由到新 Master)
总体耗时:8-15 秒(取决于配置和数据量)。关键在于阶段 3(补差异日志)——如果旧 Master 的 binlog 还没来得及同步到 Slave,这部分数据将丢失。
💭 想一想:为什么”丢数据”偏偏卡在阶段 3?——因为阶段 3 是从存活的 Slave 上往回捞旧主最后的 binlog;如果旧主崩溃前那几条日志压根没复制出去,任何存活节点上都没有,就”无源可补”。这正对应半同步/异步复制的那段窗口,也是 MHA 与 MGR 在 RPO 上分道扬镳的地方。
三、secondary_check_script:网络分区的仲裁者
MHA Manager 自己可能也处于网络分区中,因此不能仅凭 Manager 一侧的判断决定是否切换。secondary_check_script 提供双重验证:
# secondary_check_script 典型实现
#!/bin/bash
MASTER_HOST=$1
# 从至少两个不同网络路径探测 Master
ping -c 3 -W 1 $MASTER_HOST || exit 1 # ICMP
mysqladmin -h $MASTER_HOST ping || exit 1 # MySQL 协议
ssh $MASTER_HOST "echo alive" || exit 1 # SSH(如有)
exit 0
执行逻辑:
- 脚本返回 0 → Master 从另一网络路径也无法连通 → 确认 Master 宕机 → 执行切换
- 脚本返回非 0 → Master 从另一网络路径可连通 → 这是网络分区,Manager 自己失联 → 不做切换,避免脑裂
💭 想一想:为什么 Manager 不能只靠自己一侧的判断就切?——因为 Manager 自己也可能跟 Master 一起被网络分区隔开:Manager 连不上 Master,不代表 Master 真的死了,可能只是 Manager 自己掉线。若此时强行切换,就会出现两个 Master 同时对外服务(脑裂)。所以必须从另一条网络路径二次确认,多路径仲裁才能区分”Master 真死”和”Manager 自己失联”。
多 Manager 部署
生产环境建议至少 2 个 MHA Manager 实例,部署在不同机房或不同机架。它们共享配置但只有 一个活跃 Manager(通过文件锁或 etcd 选举)。当活跃 Manager 宕机时,备用 Manager 接管。
四、STONITH:防止脑裂的物理机制
STONITH = Shoot The Other Node In The Head,是防止脑裂的终极手段。
场景:MHA 切换后旧 Master 复活
时刻 T0:Master A 宕机,MHA 将 Slave B 提升为新 Master
时刻 T1:Master A 恢复(网络恢复、进程重启)
结论:如果不做处理,存在两个 Master → 脑裂 → 数据冲突
STONITH 的三种实现
| 方式 | 机制 | 实现难度 | 可靠性 |
|---|---|---|---|
| IPMI/电源控制 | 物理断电旧 Master | 高(需 BMC/IPMI) | 极高 |
| Fence Agent | 通过 VMWare/云 API 强制关机 VM | 中 | 高 |
| iptables 封锁 | 旧 Master 启动时自动 reject 3306 | 低 | 中(用户自行启动仍可绕过) |
MHA 内置的 Fence 机制:master_ip_failover_script 可以在切换后 SSH 到旧 Master,执行以下操作:
ssh old_master "
systemctl stop mysql; # 停 MySQL
iptables -A INPUT -p tcp --dport 3306 -j REJECT; # 封端口
ip addr del $VIP/24 dev eth0; # 释放 VIP
"
为什么不能只靠 VIP 避免脑裂?
VIP 飘走不代表旧 Master 不再接受写入。如果旧 Master 网卡恢复但 VIP 已漂移到新 Master,旧 Master 仍然可以通过真实 IP 接受应用直连的写入(如果应用配置了双 IP)。STONITH 确保旧 Master 的 MySQL 进程被 杀死,从根本上杜绝写入。
💭 想一想:VIP 已经飘走了,为什么还挡不住旧主写入?——因为 VIP 只是”应用该连谁”的路由约定,约束不了旧主本身;旧主网卡一恢复,它照样能用真实 IP 接收那些绕过 VIP 的直连写入。脑裂的本质是”两个节点都在写”,光改路由不够,必须物理/进程层面让旧主彻底闭嘴,这就是 STONITH 存在的理由。
五、VIP 切换的三种方案
方案 A:Keepalived + VIP
┌─────────────┐
Client ──────► │ VIP: 10.0.0.100 │
└──────┬──────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Master A│ │Slave B │ │Slave C │
└────────┘ └────────┘ └────────┘
MHA 通过 master_ip_failover_script 触发 VIP 迁移,同时 Keepalived 作为备用。优点:对应用透明、切换快。缺点:ARP 广播延迟导致部分客户端缓存旧 MAC 地址数秒。
方案 B:DNS + 短 TTL
VIP 替换为域名,DNS TTL 设 5-10 秒。MHA 切换后更新 DNS 解析。优点:无 ARP 问题。缺点:客户端 DNS 缓存可能超过 TTL,切换延迟 > TTL。
方案 C:ProxySQL / HAProxy 中间层
应用连接 ProxySQL → ProxySQL 感知 MHA 切换 → 自动切换读写端口。优点:应用零改动。缺点:引入中间层性能损耗和单点问题(需部署 ProxySQL Cluster)。
六、半同步复制降级
半同步复制在 Master 宕机后有一个重要行为:
aftersync 模式:Master 等待至少 1 个 Slave 确认收到 binlog
→ 如果 Slave 不可达 → 等待 rpl_semi_sync_master_timeout(10s)
→ 超时后降级为异步复制 → 可继续写入
降级期间,如果 Master 宕机,这期间的写入可能丢失。MHA 无法恢复这部分数据。解决方案:
-- 设置超时时间较大,减少降级窗口
SET GLOBAL rpl_semi_sync_master_timeout = 60000; -- 60s
-- 但要避免长期阻塞写入主库
结合 rpl_semi_sync_master_wait_for_slave_count = 2(等待 2 个 Slave 确认)可进一步降低丢失风险,代价是写入延迟增加。
七、脑裂的完整场景与防御矩阵
| 场景 | 描述 | MHA 防御 | 残余风险 |
|---|---|---|---|
| Manager 网络分区 | Manager 与 Master 在同一网络分区 | secondary_check_script | 多 Manager 部署可缓解 |
| Master-Slave 网络分区 | Master 可达但 Slave 不可达 | 不触发切换 | 写入全丢在旧 Master |
| 切换中旧 Master 复活 | STONITH 延迟/失败 | master_ip_failover_script | 应用双写旧 Master + 新 Master |
| 旧 Master 复活后应用直连旧 IP | 应用配置了双 IP 地址 | STONITH 杀死 MySQL 进程 | 需确保 STONITH 执行成功 |
最佳实践:STONITH(物理/API 级别) + VIP 漂移 + ProxySQL 中间层 + 半同步复制 ≥ 2 副本。四层防御可压降脑裂概率到 99.99% 以下。
章末提问
追问 1:MHA 切换为什么可能丢数据?丢在哪个环节?
回答思路:结论先行——丢在「旧主最后一段日志还没复制到从库」的窗口,集中在补差异日志阶段。因为 MHA 是异步/半同步复制,主库写 binlog 后不保证立刻同步给从库;主库一宕,这段日志若没到任何存活节点就再也捞不回来,于是 RPO 不为零。要 RPO=0 只能换 MGR 这类多数派方案。
追问 2:为什么不能只靠 VIP 漂移防脑裂,还必须有 STONITH?
回答思路:结论先行——因为 VIP 只改路由、不杀进程,堵不住旧主的直连写入。因为脑裂的本质是两个 Master 同时在写,VIP 飘到新主后,旧主网卡一旦恢复仍能用真实 IP 接应用直连的写;STONITH 通过 IPMI 断电、云 API 关机或封 3306 把旧主 MySQL 进程彻底杀死,从根源上消除第二个写入口。
追问 3:secondary_check_script 解决的是什么问题?没有它会怎样?
回答思路:结论先行——解决「Manager 自己身处网络分区时误判 Master 已死」的问题,防误切。因为 Manager 连不上 Master 可能是网络分区而非 Master 真死,单侧判断会导致脑裂;secondary_check_script 从至少两条网络路径再探测,只有多路径都不可达才确认 Master 真死、才切。没有它,Manager 一失联就切,极易造成双主。