马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
MySQL 半同步复制治理实战:从静默退化异步丢数据到 wait_point 与超时监控双线收敛
一、具体的问题
一套 5.7.36 的一主两从,主库上 rpl_semi_sync_master_enabled 一直是 ON,my.cnf 里配得明明白白。例行高可用演练做了一次"杀主库、提升从库"的切换,业务当场报错:新主库上找不到 11 分钟前的订单。回查发现从库落后主库 11 分钟——也就是说,这 11 分钟里主库的所有提交都在走异步复制,半同步的保护早就不存在了。
更难受的是时间点:主库状态里 Rpl_semi_sync_master_status 已经是 OFF,Rpl_semi_sync_master_no_tx 累计 41 万多笔。往前翻监控,这个值在两个月前从库一次例行重启后就没再回到 ON。两个月里没有任何告警,因为所有人都盯着 SHOW VARIABLES 里的 rpl_semi_sync_master_enabled——那个值确实是 ON,但它只说明"插件启用了",不说明"保护生效了"。整个排查用了不到半小时,真正花时间的是复盘:为什么两个月的退化无人察觉,监控抓错了指标,预案里没有覆盖退化路径,都是体系问题,不是某个人手抖。
二、核心原理
半同步的语义是:主库把事务写入 binlog 并落盘(sync binlog)之后,等至少 N 个从库的 IO 线程把 binlog 收进 relay log 的 ACK,才向客户端返回提交成功。注意 ACK 的时点是"从库收到并写入 relay log",不是"从库已回放"——所以半同步防的是主库整机故障时的数据丢失,并不保证从库已可读。
第一个关键机制是退化。rpl_semi_sync_master_timeout 默认 10000 毫秒,超时等不到 ACK,主库不是报错,而是整个服务端静默降级为异步复制,Rpl_semi_sync_master_status 翻成 OFF,之后所有提交照常成功,业务完全无感。只有等某个从库重新注册 ACK,主库才会自动切回半同步;而如果从库侧的半同步插件没启用(rpl_semi_sync_slave_enabled=0),它永远不会注册,主库就永远停在异步上。enabled=ON 但 status=OFF,就是这个局面的标准特征。
第二个关键机制是等待点。rpl_semi_sync_master_wait_point 有两个值。AFTER_COMMIT(5.7 的默认值)在引擎提交之后才等 ACK——提交与 ACK 之间有个窗口,其他会话已经能看到这条事务,如果这时主库崩溃、事务没传到从库,切换后这些数据就"消失"了,即别人已读到的已提交数据在故障后丢失;AFTER_SYNC(5.7.2 引入,8.0 起默认)在 binlog 落盘后、引擎提交前等 ACK,所有客户端在从库确认且主库提交后才同时可见,主库故障切换是无损的。一个常见误解是"开了半同步就零丢失",准确说法是:只有 AFTER_SYNC 加双 1(sync_binlog=1、innodb_flush_log_at_trx_commit=1),且超时内确实有 ACK,才接近零丢失;AFTER_COMMIT 只防住"提交者拿到假成功",防不住其他会话的幻读窗口。
两个配套参数也值得知道。rpl_semi_sync_master_wait_for_slave_count 默认 1,要更强的一致性可以设成 2,即至少两个从库都 ACK 才算提交成功,代价是提交延迟随最慢的那个走;rpl_semi_sync_master_wait_no_slave 默认 ON,从库数掉到低于阈值时主库还会等满超时再退化,设成 OFF 则立刻退化。另外注意版本差异:8.0.26 起插件改为内置实现,名字从 rpl_semi_sync_master/slave 换成了 rpl_semi_sync_source/receiver,timeout 变量相应叫 rpl_semi_sync_source_timeout、等待点叫 rpl_semi_sync_source_wait_point,本文以 5.7 旧名叙述,8.0.26+ 对应替换即可,语义一致。
三、实例参考(动手步骤)
步骤 0:确认现状基线
主库上把变量和状态变量一起抓,别只看一半:
- SHOW VARIABLES LIKE 'rpl_semi_sync%';
- SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync%';
复制代码
重点三个:Rpl_semi_sync_master_status(OFF 即保护失效)、Rpl_semi_sync_master_clients(已注册半同步从库数,0 说明没有从库握手成功)、Rpl_semi_sync_master_no_tx 与 yes_tx(no_tx 持续增长、yes_tx 停滞,就是退化运行中)。本例基线:status=OFF,clients=0,no_tx=418612,yes_tx 停在 2302177。
步骤 1:把从库侧修回
检查从库插件是否安装、是否启用:
- SELECT PLUGIN_NAME, PLUGIN_STATUS FROM information_schema.PLUGINS
- WHERE PLUGIN_NAME LIKE '%semi%';
复制代码
没装就装:INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; 然后 SET GLOBAL rpl_semi_sync_slave_enabled=1。本例根因正是半年前从库重启后,my.cnf 里漏了 rpl_semi_sync_slave_enabled=1,重启即失效——修完必须写进配置文件固化。
步骤 2:重启从库 IO 线程完成握手
半同步注册发生在 IO 线程启动时,光开变量不重启线程,从库不会注册:
- STOP SLAVE IO_THREAD;
- START SLAVE IO_THREAD;
复制代码
回主库确认 clients 从 0 变 1、status 回 ON,此时退化自动解除。注意别用整库的 STOP SLAVE / START SLAVE,只需要动 IO 线程,SQL 线程保持运行可以避免回放中断;一主多从的场合,每个从库都要单独走一遍这个动作,确认主库 clients 数量与从库台数对得上再收工。
步骤 3:等待点改 AFTER_SYNC
- SET GLOBAL rpl_semi_sync_master_wait_point = 'AFTER_SYNC';
复制代码
全局动态生效,不用重启实例;同时写进 my.cnf。改完观察 Rpl_semi_sync_master_tx_avg_wait_time(每笔事务平均等 ACK 时间)确认延迟仍在可接受范围。
步骤 4:超时改合理值并加监控
默认 10 秒偏长:跨机房 RTT 正常几十毫秒的拓扑,ACK 超 1 秒就该查了。
- SET GLOBAL rpl_semi_sync_master_timeout = 3000;
复制代码
监控做三件事:status=OFF 立即告警(核心指标);no_tx 按 5 分钟增量告警;clients 少于预期从库数即告警。脚本每分钟抓一次 SHOW GLOBAL STATUS 就够。告警阈值上别太抠:退化本身不致命,致命的是退化后没人知道,所以哪怕一天只退化一次也要响,响一次查一次根因,比事后对账划算。
步骤 5:补齐性能与持久性配套
多从库场景打开 replication_sender_observe_commit_only 与 replication_optimize_for_static_plugin_config(5.7.33+ 可用),减少提交路径上的锁争用;确认主库双 1 在位——半同步只保证 binlog 到了从库 relay log,若主库本机 binlog 没落盘,保护仍不成立。另注意 AFTER_SYNC 模式下主库故障切换后,原主库 binlog 可能残留未提交事务,按官方文档要求原主库不能直接拉起回加,应重建后再加入。
治理前后对比
| 指标 | 治理前 | 治理后 | | Rpl_semi_sync_master_status | OFF(持续 2 个月) | ON | | Rpl_semi_sync_master_clients | 0 | 2 | | no_tx 日增量 | 约 6900 笔 | 0 | | 演练切换丢数据 | 11 分钟 | 0 笔 | | 每笔提交平均等 ACK | 0(异步) | 1.8ms | | 提交 P99 延迟 | 6.2ms | 9.4ms |
四、实操检查清单
- Rpl_semi_sync_master_status 每分钟采集,OFF 即告警,不等故障演练才发现。
- Rpl_semi_sync_master_clients 与半同步从库实际台数比对,少一台即告警。
- no_tx 与 yes_tx 只看增量方向,不看累计绝对值。
- 从库 my.cnf 显式写 rpl_semi_sync_slave_enabled=1,不依赖运行时 SET。
- wait_point 明确写 AFTER_SYNC 并固化,主从升级、重装插件后核对默认值变化。
- timeout 按拓扑 RTT 设定(3 秒量级起步),不用默认 10 秒裸奔。
- 主库双 1 落实:sync_binlog=1、innodb_flush_log_at_trx_commit=1。
- 故障切换预案写明 AFTER_SYNC 模式下原主库必须重建后才能回加。
- 演练加一项"断网 30 秒再恢复",验证退化与自动切回两条路径都走通。
- 监控把 SHOW VARIABLES 与 SHOW STATUS 一起抓,防"enabled=ON 的假象"。
几个容易踩的坑
- 盯 enabled 不盯 status:enabled 只说明插件加载并启用,status 才是当前保护是否生效,两个必须一起看。生产上最常见的假安全就是这个组合:变量 ON、状态 OFF,监控抓的是前者。
- 只开从库变量不重启 IO 线程:注册发生在 IO 线程启动时,不重启握手永远完不成,clients 一直是 0,主库仍按退化处理。CHANGE MASTER 换主后同样要重启 IO 线程。
- 以为退化会报错:退化不给业务任何错误,提交全部成功,痕迹只在状态变量和错误日志里,业务侧唯一的感知是切换那天丢数据。
- 以为半同步等于从库已回放:ACK 只表示进了 relay log,SQL 线程可能仍落后,读从库的报表照样读到旧数据,半同步不管回放。
- 5.7 默认 AFTER_COMMIT 直接上生产:幻读窗口在,切换可能丢"别人已经看到"的数据,升级 8.0 前要显式确认此项,别假设新版本默认值等于老版本行为。
- timeout 设 0 或超长:0 表示无限等,从库全断时提交集体挂起,等于业务自杀;超长则退化前白等,提交延迟毛刺明显,按 RTT 定值。
- AFTER_SYNC 切换后原主库直接拉起回加:binlog 里可能有未提交事务造成冲突,必须重建,这一点官方文档写得明确,切换预案里要有对应条目。
- 只配主库不配从库:主库装了插件、从库没装没启用,clients 永远是 0,半同步形同虚设,主从两侧都要落配置。
|