MySQL高可用集群选型:RPO/RTO、复制方案与流量入口实战
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 logSQL 线程回放。整条链路上主库返回客户端这个时间点从库可能什么都没收到。丢数窗口等于“主库已提交但还没被从库接收”的那部分事务。这个窗口的大小等于复制延迟而复制延迟在大事务、批量更新、从库单线程回放、从库上有慢查询时会被放大得离谱。我见过一次批量归档任务把延迟推到 40 分钟那 40 分钟里主库的写入在故障时全部不可恢复。异步复制的优势也很明确对主库写入性能影响最小网络抖动不会阻塞提交架构简单出问题好排查。它适合读多写少、能接受短时间数据回退、或者有上游消息队列可以补偿的场景。如果你的业务本来就靠消息表加补偿任务保证最终一致异步复制是完全合理的底座没必要为了“看起来更高级”换成组复制。2.2 半同步复制的两种模式与退化陷阱半同步的核心改动是主库提交后要等至少一个从库确认收到 binlog 才返回。MySQL 8.0 里对应参数是rpl_semi_sync_source_enabled8.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 ) ENGINEInnoDB;-- 每秒执行一次 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_trackingWRITESET在大多数 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 21 || 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 上线前值得逐条过的检查项下面这些是我每次上线前都会过一遍的按优先级排列复制链路是否开启 GTIDgtid_executed是否可比较半同步状态变量是否进监控退化是否有告警心跳表是否部署延迟监控是否用的是心跳口径切换脚本的每一步是否可单独执行、有明确成功判定旧主隔离手段是否可用是否演练过代理层健康检查间隔、超时、失败次数是否经过压测验证应用连接池上限与数据库max_connections是否算过总量大事务是否有拆分规范是否有监控发现超长事务备份是否独立于复制链路恢复演练是否做过告警是否有抑制规则切换期间不会淹没在告警里最后一条经常被忽略。切换时几十条告警同时触发值班同学根本看不出来哪条是根因。提前配好告警抑制和聚合比多装一个监控系统有用。我个人在实际操作中的体会是高可用方案的价值不在于它多先进而在于团队对它有多熟。一个被反复演练过、每个人都能说清楚边界的三节点半同步集群比一个配置漂亮但没人敢碰的组复制集群可靠得多。选型时多问一句“出问题的时候谁来处理、怎么处理”往往比对比参数表更能决定最终答案。