MySQL 高可用集群这个话题,我在不同团队、不同场合被问过不下十次,问法几乎一模一样:主从加个 VIP 算不算高可用?组复制是不是比半同步高级?三节点够不够?每次我都得先把问题拆开,因为这些问题背后藏着同一个误区——把“高可用”当成一个可以买到的成品,而不是一组需要量化和演练的工程指标。这篇就把我这些年搭过、迁过、也救过场的几类方案摊开来对比:异步与半同步复制、外部管理器接管、组复制类方案,以及最容易被忽略的流量入口层。每个方案解决什么问题、付出什么代价、在什么场景下会被迫换掉,我会尽量讲清楚。适合正在做选型的技术负责人、需要接手存量集群的 DBA,以及想把“知道名词”变成“能判断好坏”的后端同学。
1. 拆开“高可用”这三个字:指标先于方案
1.1 RPO 与 RTO 决定了方案的天花板
很多人选型的第一步是列工具,正确的第一步是定指标。**RPO(恢复点目标)**回答的是“允许丢多少数据”,**RTO(恢复时间目标)**回答的是“允许停多久”。这两个数字不是拍脑袋写的,它们直接决定了你只能在哪个区间里选方案。
异步复制的 RPO 取决于复制延迟,主库崩溃时,还没传到从库的 binlog 就永远丢了,业务高峰期延迟几秒,这几秒的写入就是损失。半同步把 RPO 压到接近零,但代价是每次提交多一个网络往返,并且是对性能最敏感的提交路径上加往返。组复制在多数派存活的前提下 RPO 可以做到零,但 RTO 受选举和成员追平数据的影响,节点越多、数据量越大,恢复越慢。
我习惯让业务方给出具体数字而不是形容词。说“尽量少丢”没有意义,说“最多丢 3 秒的订单”才能落到方案上。同一个系统里,订单表和日志表可以走不同等级:订单走半同步或组复制单主,日志表走异步完全够用。一刀切的方案通常是浪费。
| 指标档位 | 典型 RPO | 典型 RTO | 常见对应方案 |
|---|---|---|---|
| 尽力而为 | 秒级到分钟级 | 5 到 30 分钟 | 异步复制加人工介入切换 |
| 业务可感知 | 1 秒以内 | 30 秒到 3 分钟 | 半同步复制加自动切换管理器 |
| 强一致要求 | 0(多数派内) | 10 到 60 秒 | 组复制单主加代理层 |
| 双活要求 | 0 | 秒级 | 需要跨单元拆分写入,单套数据库难以承载 |
最后一行要特别注意:单套 MySQL 集群很难真正做到双活写入,绝大多数所谓双活其实是“一主一备、备库只读、切换时改指向”。如果你看到方案里写着“两地三中心双写”,先问一句:冲突怎么解。答案往往是业务层自己保证不冲突,而不是数据库解决了。
1.2 故障域没划清,方案再漂亮也白搭
高可用效果的上限由故障域决定。我见过把三个节点全部放在同一台物理机的不同虚拟机上,还配了组复制,号称“三副本零丢失”,结果宿主机一重启,三个节点一起掉,多数派瞬间不存在,集群直接不可写。
划故障域的顺序是这样的:先看机柜和供电,再看网络交换路径,最后看云上的可用区。三个节点至少要在两个故障域上,最好三个。同一个交换机下的三个节点,交换机挂了照样全灭。如果预算只够两个可用区,就要接受“一个区挂了,集群降为只读或者需要人工介入”这个现实,并且把它写进预案。
还有一类隐性故障域是依赖服务。切换动作依赖 VIP 漂移,VIP 依赖网络设备;切换依赖管理器的仲裁,仲裁依赖一个独立的小集群;组复制依赖成员之间的内网互通,而内网和业务流量共用一块网卡。这些耦合点要一个个列出来,逐个问:它挂了会怎样。
1.3 “切换成功”不等于“业务恢复”
这是我最想强调的一点。切换脚本把新主指过去,屏幕上显示 success,不代表业务恢复了。应用侧连接池里还握着旧主的连接,DNS 缓存还没过期,代理层的后端状态还没刷新,写入语句打到旧主上直接报错,读请求打到还没追平的从库上读到了旧数据。
真正的恢复判定应该包含四项:新主可写、旧主已隔离、应用连接全部重建、数据校验通过。少了任何一项,都只能叫“切换动作完成”。我在演练里会刻意在切换后立刻跑一批业务校验 SQL,对比切换前后的关键表行数和汇总值,这比看集群状态面板可靠得多。
注意:旧主必须被强制隔离,而不是“等它自己发现”。被降级的旧主如果恢复后还认为自己能写,就会产生双写,此时再想合并数据,工作量比重建还大。
2. 异步复制与半同步复制:地基最便宜,坑也最密
2.1 异步复制的工作链路与丢数窗口
异步复制的链路是:主库写 binlog,提交事务,立刻返回客户端;从库的 IO 线程拉取 binlog 写入 relay log,SQL 线程回放。整条链路上,主库返回客户端这个时间点,从库可能什么都没收到。
丢数窗口等于“主库已提交但还没被从库接收”的那部分事务。这个窗口的大小等于复制延迟,而复制延迟在大事务、批量更新、从库单线程回放、从库上有慢查询时会被放大得离谱。我见过一次批量归档任务把延迟推到 40 分钟,那 40 分钟里主库的写入在故障时全部不可恢复。
异步复制的优势也很明确:对主库写入性能影响最小,网络抖动不会阻塞提交,架构简单,出问题好排查。它适合读多写少、能接受短时间数据回退、或者有上游消息队列可以补偿的场景。如果你的业务本来就靠消息表加补偿任务保证最终一致,异步复制是完全合理的底座,没必要为了“看起来更高级”换成组复制。
2.2 半同步复制的两种模式与退化陷阱
半同步的核心改动是:主库提交后要等至少一个从库确认收到 binlog 才返回。MySQL 8.0 里对应参数是rpl_semi_sync_source_enabled(8.0.26 之前叫rpl_semi_sync_master_enabled),从库侧是rpl_semi_sync_replica_enabled。
关键参数是超时时间,默认 10 秒:
[mysqld] rpl_semi_sync_source_enabled = ON rpl_semi_sync_source_timeout = 1000 rpl_semi_sync_source_wait_for_replica_count = 1 rpl_semi_sync_source_wait_point = AFTER_SYNCAFTER_SYNC和AFTER_COMMIT的差别很大。AFTER_SYNC是在存储引擎提交之前等确认,拿不到确认就回滚,主从数据一致;AFTER_COMMIT是提交之后再等,拿不到确认时不回滚,其他会话能看到这个事务,但备库可能没有,切换后这部分数据就丢了。除非有历史包袱,一律用AFTER_SYNC。
真正的陷阱在超时退化。从库响应慢或者网络抖动超过超时时间,主库会自动降级为异步复制,而且这个降级是静默的,业务侧完全无感,只有看状态变量才知道:
SHOW STATUS LIKE 'Rpl_semi_sync_source_status'; SHOW STATUS LIKE 'Rpl_semi_sync_source_no_tx';Rpl_semi_sync_source_status变成 OFF,就说明当前实际是异步在跑。no_tx计数器增长,说明有事务因为没等到确认而回退成异步提交。这两个指标必须进监控,并且配告警。我见过集群跑了大半年,一直以为自己是半同步,实际上因为某次网络抖动退化后就没恢复,直到演练才发现。
2.3 复制延迟的度量口径要对齐
Seconds_Behind_Master这个值不能直接当延迟用。它的计算基准是 relay log 中事件的时间戳,主库空闲时它是 0,主库有大事务时它可能瞬间跳到几千,而从库其实只是单线程回放慢。更麻烦的是并行复制开启后,这个值的语义更模糊。
可靠的做法是用心跳表。启一个定时任务,每秒往主库写一行时间戳,从库读出来和自己当前时间对比,差值就是真实延迟:
CREATE TABLE heartbeat ( id INT PRIMARY KEY, ts BIGINT NOT NULL ) ENGINE=InnoDB;-- 每秒执行一次 REPLACE INTO heartbeat (id, ts) VALUES (1, UNIX_TIMESTAMP()); -- 从库上查询 SELECT UNIX_TIMESTAMP() - ts AS lag_seconds FROM heartbeat;这个口径简单、可跨版本、能直接进监控。用它来做切换判断也更稳:代理层健康检查读这个值,超过阈值就把节点摘掉,而不是依赖Seconds_Behind_Master的瞬时抖动。
顺带说一句并行复制。MySQL 8.0 基于 write set 的并行复制(binlog_transaction_dependency_tracking=WRITESET)在大多数 OLTP 场景下效果不错,能把回放能力提升数倍,但前提是主库上的事务尽量不冲突。如果业务里有大量更新同一批热点行的操作,并行度上不去,延迟还是会堆。这时候该做的是优化业务写热点,而不是继续调参数。
3. 外部管理器接管:它是裁判,不是复制本身
3.1 管理器的职责边界
MHA、Orchestrator 这类工具的定位经常被误解。它们不复制数据,不做一致性保证,只做三件事:探测主库是否存活、在候选从库里选一个、执行提升和拓扑重写。数据一致性完全由底层的复制方式决定。所以“上了 Orchestrator 就高可用了”这句话是错的,它只是把人工切换的几分钟压缩到几十秒,顺便减少误操作。
选管理器时我看三个点:探测机制是否可调、选主逻辑是否可控、自身是否高可用。探测太敏感会在网络抖动时误切,太迟钝会让 RTO 变长。选主逻辑要能表达业务偏好,比如优先选同机房、延迟最小、数据最新的节点,而不是简单地“第一个活着的”。
Orchestrator 的 Raft 模式和共享后端模式差别很大。Raft 模式自带仲裁,不需要额外数据库,但节点数是奇数且不建议频繁变更;共享后端模式部署简单,但后端数据库本身成了单点。生产上如果只有三个数据库节点,我倾向单独部署一套小的仲裁环境,不要和管理器混在同一批机器上。
3.2 选主逻辑与脑裂防护
选主最怕的是“两个都觉得自己是主”。防护手段有三层:第一层是旧主隔离,切换脚本里第一件事应该是把旧主的写入口关掉,可以通过改端口、加防火墙规则、或者直接停服务实现;第二层是候选节点数据校验,通过 GTID 集合比较,选出包含事务最多的那个,避免选到一个落后很多的节点;第三层是应用侧只认一个入口,VIP 或者代理层保证写流量只到一个地方。
GTID 在这里的作用很大。开启 GTID 后,gtid_executed集合可以直接比较包含关系,判断哪个节点数据更全:
SHOW REPLICA STATUS\G -- 关注 Retrieved_Gtid_Set 和 Executed_Gtid_Set如果候选节点的Executed_Gtid_Set是主库的子集,提升它就要接受数据丢失。此时要么拒绝切换等人工决策,要么走“补数据后切换”的流程。这个决策点必须提前写进预案,不能等故障发生时现场讨论。
[mysqld] gtid_mode = ON enforce_gtid_consistency = ON log_replica_updates = ONlog_replica_updates打开是为了让级联复制链条上的中间节点也记录变更,否则某些拓扑下 GTID 集合会对不上。
3.3 管理器的隐性维护成本
这类工具最大的成本不在部署,在版本维护和脚本适配。切换脚本里通常要处理 VIP 漂移、代理层后端更新、只读设置、监控静默、告警抑制,每一步都依赖具体环境的细节。MySQL 版本升级、操作系统升级、网络策略变更,都可能让脚本失效。
我的做法是把切换拆成可单独测试的原子步骤,每一步都能手动执行、有明确的成功判定,然后用一个编排脚本串起来。这样某一步失败时可以停下来修,而不是整条链路黑盒失败。演练时按季度做一次全链路,包括“管理器自身挂掉”这个场景。
4. 组复制类方案:一致性的价格标在哪
4.1 多数派提交与网络抖动的放大
组复制(MGR)和 PXC 都属于“多数派确认”模型:事务要在多数节点上达成一致才算提交。三节点容忍一个故障,五节点容忍两个。代价是每次提交至少要多一轮节点间通信,跨机房部署时这个往返直接体现在写入延迟上。
网络抖动在这个模型下会被放大。异步复制里一次网络抖动只是延迟变大,组复制里如果抖动导致多数派暂时不可达,写入会直接阻塞,客户端看到的是提交超时。跨可用区部署时必须先测网络抖动分布,P99 往返、丢包率、重传情况都要看。如果两个可用区之间的 P99 往返超过 5 毫秒,写入延迟的感知会很明显。
参数上有个关键点:
group_replication_consistency = BEFORE_ON_READ这个参数控制读的一致性等级。默认EVENTUAL下,从节点读到的不一定是最新数据。设成BEFORE_ON_READ会让读请求先等自己追平到最新事务,读延迟变高但不会读到旧数据。设成BEFORE或AFTER更严格,但写入代价也更大。选哪个取决于业务能接受哪种不一致。
4.2 单主与多主的真实取舍
单主模式下只有一个节点接受写入,其他节点只读,故障时自动选举新主。这是绝大多数业务的正确选择,因为写入路径简单,不需要处理冲突。
多主模式允许所有节点写入,但冲突检测是基于行和认证数据库的,两个节点同时改同一行,后提交的会被回滚并报错。业务层必须处理这种错误重试,而且重试逻辑要幂等。我见过业务把多主当成分片用,结果热点行冲突频繁,重试风暴比原来的容量问题更严重。
多主适合的场景其实很窄:写热点天然分散、有明确的租户隔离、或者需要就近写入降低延迟。如果你的业务是典型的订单、账户、库存,别碰多主。
还有个常被忽略的点:自增主键。多主模式下每个节点的auto_increment_increment和auto_increment_offset要配置成互不冲突的组合,否则会出现主键冲突。
-- 节点 1 auto_increment_increment = 3 auto_increment_offset = 1 -- 节点 2 auto_increment_increment = 3 auto_increment_offset = 2 -- 节点 3 auto_increment_increment = 3 auto_increment_offset = 3这样做的好处是没有冲突,坏处是主键不连续、跨度大,对某些依赖连续 ID 的业务有影响,而且索引页的空间利用率会下降。换成 UUID 或者雪花 ID 也有各自的成本,需要权衡。
4.3 流控参数与写入吞吐的平衡
组复制的流控是个双刃剑。当某个节点回放跟不上时,流控会主动限制主节点的写入速度,防止积压。这个机制保护了集群,但也会让业务侧感受到“明明主库资源还有,写入却变慢了”。
相关参数:
group_replication_flow_control_mode = QUOTA group_replication_flow_control_applier_threshold = 25000 group_replication_flow_control_certifier_threshold = 25000 group_replication_flow_control_min_quota = 0 group_replication_flow_control_min_recovery_quota = 0阈值调大,流控更宽松,积压更多,恢复时间更长;调小,更容易触发限流,写入更平滑但吞吐下降。我的经验是先设默认值,观察一段时间的 applier 队列深度和业务写入延迟,再决定往哪个方向调。不要一上线就调到极限。
另外要注意大事务。一个几十万行的 UPDATE 在组复制里会被当成一个事务在认证数据库里处理,内存占用和认证时间都会飙升,严重的会触发成员被踢出。批量操作一定要拆小,比如按主键区间每 5000 行一批,中间留出间隔。这一条不是建议,是必须。
5. 流量入口层:最容易被低估的一段
5.1 VIP 漂移的收敛时间
用 Keepalived 做 VIP 是最常见的做法,配置简单,成本低。但收敛时间不是零,取决于advert_int、vrrp_script的检测间隔和失败次数。
vrrp_script chk_mysql { script "/usr/local/bin/chk_mysql.sh" interval 2 fall 3 rise 2 timeout 2 }按这个配置,检测到故障至少要 6 秒,加上切换和 ARP 广播,客户端侧感知通常在 10 秒左右。这段时间里的连接全部失败,应用要有重连和重试逻辑。
chk_mysql.sh的写法很关键。只检测端口通不通是不够的,端口通但数据库已经只读或者卡住的情况很常见。至少要检测可写性:
#!/bin/bash mysql -uhealth -p"$PASS" -N -e "SELECT 1" >/dev/null 2>&1 || exit 1 READONLY=$(mysql -uhealth -p"$PASS" -N -e "SELECT @@global.read_only" 2>/dev/null) [ "$READONLY" = "0" ] || exit 1 exit 0还有一种更隐蔽的问题:VIP 漂移后,旧主上的 ARP 缓存没有及时更新,部分客户端仍然把流量发到旧主。这在虚拟化环境里出现过,需要配合arp_ignore、arp_announce参数处理,或者干脆不用 VIP,改用代理层。
5.2 代理层的健康检查与重试风暴
ProxySQL、HAProxy 这类代理的价值在于:后端节点变化对应用透明,可以做读写分离、连接池、查询路由。但配置不当会引入新问题。
健康检查太频繁会压垮数据库,太稀疏会滞后。建议把检查间隔设在 1 到 2 秒,超时设在 1 秒以内,连续失败 2 到 3 次才摘除,避免抖动导致节点反复进出。同时要监控后端节点的状态变化次数,短时间内频繁变化说明网络或者实例本身有问题。
重试风暴是另一个坑。主库切换瞬间,所有连接同时失败,应用层如果配置了激进的重试策略,会在几秒内产生巨量连接请求,把新主直接压垮。正确做法是加指数退避和抖动,并且限制并发重连数。
提示:连接池的
max_connections要和数据库的max_connections对齐计算,代理层每个后端连接数乘以实例数,很容易超过数据库上限。切换后新主要承接所有连接,这个数字一定要提前算。
5.3 应用侧连接串该怎么写
不管下面用什么方案,应用侧都要遵守几条规则:连接串里配置多个地址而不是单个 VIP;开启自动重连;把“不可重试错误”和“可重试错误”区分开;写操作和读操作的连接分开配置。
一个常见的写法是把读写分离放在代理层,应用只连代理的一个地址,代理根据语句类型路由。这样应用改动最小,但代理成为新的关键路径,必须做高可用。
另一个思路是应用直连多个地址,自己维护健康状态。灵活度高,但每个应用都要实现一遍,容易出现实现不一致。我倾向中小规模用代理,规模大了、有统一的基础设施团队时再考虑应用侧 SDK。
6. 落到选型:场景映射与演练清单
6.1 从业务特征倒推方案
选型不是选最强的,是选最匹配的。我把常见场景整理成下面这张表,实际决策时对照着看:
| 业务特征 | 推荐底座 | 切换方式 | 主要代价 |
|---|---|---|---|
| 读多写少,能容忍短时数据回退 | 异步复制 | 外部管理器或脚本 | 故障时可能丢最近写入 |
| 交易类,丢数不可接受,写入量中等 | 半同步复制 | 外部管理器 | 提交延迟增加,需监控退化 |
| 强一致,节点数可控,写入量不大 | 组复制单主 | 自动选举加代理 | 写入延迟随节点距离上升 |
| 已有分库分表,单分片容量小 | 每分片异步或半同步 | 分片级切换 | 运维复杂度高,需要统一管控 |
| 云上托管,运维人力有限 | 托管高可用版 | 由平台负责 | 可控性下降,成本上升 |
表格里没有“最佳方案”这一列,因为不存在。同一家公司不同业务线用不同方案是正常的,强行统一反而会在成本和稳定性之间两头不讨好。
6.2 演练比方案文档重要十倍
我参与过的故障里,方案本身设计错误的其实不多,更多是“方案对,但没人真正跑通过”。演练要覆盖的场景至少包括:主库进程被杀、主库整机断电、从库延迟很大时主库故障、网络分区导致多数派丢失、管理器自身故障、切换过程中再次故障。
每次演练记录四个数据:检测耗时、决策耗时、执行耗时、业务恢复耗时。这四个数字加起来才是真实的 RTO,和方案文档上写的往往差好几倍。演练后要做复盘,把发现的问题变成脚本改动或者监控项,否则下次还是同样的坑。
演练环境要和生产尽量一致,尤其是网络拓扑和节点分布。在小规模测试环境里切换只要 5 秒,生产上因为数据量大、连接多,可能要 50 秒,这个差距必须提前知道。
6.3 上线前值得逐条过的检查项
下面这些是我每次上线前都会过一遍的,按优先级排列:
- 复制链路是否开启 GTID,
gtid_executed是否可比较 - 半同步状态变量是否进监控,退化是否有告警
- 心跳表是否部署,延迟监控是否用的是心跳口径
- 切换脚本的每一步是否可单独执行、有明确成功判定
- 旧主隔离手段是否可用,是否演练过
- 代理层健康检查间隔、超时、失败次数是否经过压测验证
- 应用连接池上限与数据库
max_connections是否算过总量 - 大事务是否有拆分规范,是否有监控发现超长事务
- 备份是否独立于复制链路,恢复演练是否做过
- 告警是否有抑制规则,切换期间不会淹没在告警里
最后一条经常被忽略。切换时几十条告警同时触发,值班同学根本看不出来哪条是根因。提前配好告警抑制和聚合,比多装一个监控系统有用。
我个人在实际操作中的体会是,高可用方案的价值不在于它多先进,而在于团队对它有多熟。一个被反复演练过、每个人都能说清楚边界的三节点半同步集群,比一个配置漂亮但没人敢碰的组复制集群可靠得多。选型时多问一句“出问题的时候谁来处理、怎么处理”,往往比对比参数表更能决定最终答案。