编程 MySQL 半同步复制:`enabled=1` 但 `status=0`,四处静默降级与 ACK 的真实边界

2026-11-14 00:03:55

MySQL 半同步复制:enabled=1 但 status=0,四处静默降级与 ACK 的真实边界

先看两个对不上的值

mysql> SELECT @@rpl_semi_sync_source_enabled;
+--------------------------------+
| @@rpl_semi_sync_source_enabled |
+--------------------------------+
|                              1 |
+--------------------------------+

mysql> SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_source_status';
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| Rpl_semi_sync_source_status | 0     |
+-----------------------------+-------+

开着,但没在跑。同一台实例上,某次写入在 COMMIT 处卡了整整 10 秒,然后照常返回 OK,业务侧只看到一次偶发慢查询。

这两件事是同一条链路的两端:10 秒是 rpl_semi_sync_source_timeout 的默认值 10000 毫秒走到了头;status=0 是到点之后源端切回了异步复制。整个过程中 enabled 变量一次都没变。

官方接口文档把这件事写得很直白:源在超时或副本追平后自动切换时,只改 status 变量;rpl_semi_sync_source_enabled 可能仍然是 1,而半同步实际并未运行。

参考:MySQL 8.4 Semisynchronous Replication Interface、semisync_source_plugin.cc 源码。

ACK 确认的是 relay log,不是表

一次提交在半同步下的完整路径:

prepare
  → 写 binlog(fsync)
  → 等 binlog 发到备库(AFTER_SYNC)
  → dump 线程发送
  → 备库 IO 线程写入 relay log
  → 按 binlog 文件名 + 位点回 ACK
  → 主库 Semisync Ack Receiver 线程按位点唤醒事务
  → 引擎层提交,返回 OK

ACK 到达的位置是“至少一个副本的 relay log 里有了这个事件”。applier 线程可能还没开始跑,表里没有,索引里没有,只读副本上查不到。所谓“已确认”,边界就是 relay log。

由此延伸出两件事:

主库 crash 后立刻切到那个回了 ACK 的副本,客户端马上读新主,可能读不到刚提交的那行。切换工具认为数据在,客户端认为数据丢了,两边都没错。

relay log 本身也不保证落盘。它的刷盘频率受 sync_relay_log 控制,默认 10000,也就是每 10000 次才 fsync 一次。机房掉电时,已经 ACK 过的事务仍然可能丢。这个参数和 innodb_flush_log_at_trx_commit、sync_binlog 是三条独立的线。

同类结论在 mysmartcopilot 的半同步与故障切换笔记和 oneuptime 的半同步文章里是一致的:ACK ≠ apply,ACK ≠ 持久化。

两组变量怎么读

SHOW VARIABLES LIKE 'rpl_semi_sync%';
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync%';

变量名在 8.0.26 起改过一次:rpl_semi_sync_source_enabled / _timeout / _wait_for_replica_count / _wait_point,旧名是 master_*。副本侧是 rpl_semi_sync_replica_enabled。

关键状态变量:

  • Rpl_semi_sync_source_status:1 表示半同步在运行,0 表示已降级。
  • Rpl_semi_sync_source_yes_tx:被副本确认过的事务数。
  • Rpl_semi_sync_source_no_tx:降级之后以异步方式提交的事务数。
  • Rpl_semi_sync_source_clients:当前连上来的半同步副本数量。
  • Rpl_semi_sync_replica_status:副本侧的状态。

no_tx 单调增长而 yes_tx 不动,说明已经掉进异步在跑了。clients=0 而 enabled=1 是最常见的组合:配置看着正常,实际一个半同步副本都没接上。

副本侧改完 rpl_semi_sync_replica_enabled 必须 STOP REPLICA IO_THREAD 再 START REPLICA IO_THREAD,否则变量显示的状态和服务端实际状态对不上,看到的又是一层假象。

四处静默降级

一、超时兜底,变量不动

rpl_semi_sync_source_timeout 默认 10000 毫秒。等待超过这个时间就转异步,提交继续往下走,变量保持原样。开头那次 10 秒卡顿就是这么来的:业务无感,日志里只有一条慢 SQL。

二、恢复不是自动的

文档描述是源在副本追平后可以自动恢复半同步。Percona 在《Face to Face with Semi-Synchronous Replication》里的实测结果是:所有测试场景都没有观察到自动恢复。原因是 semisync_source.cc 里检测副本“已追平”的代码路径不可靠——复制协议本身是异步的,副本不会主动上报自己的位点进度,源端只能靠推测。

生产上能用的恢复手段是把开关推一遍:

SET GLOBAL rpl_semi_sync_source_enabled=OFF;
SET GLOBAL rpl_semi_sync_source_enabled=ON;

MySQL 8.4 之后这块的行为是否变化,需要自己在目标版本上复现验证。

同一篇文章还给了一个数字:开启半同步后 10k tps 掉到 3k tps。单个副本挂掉触发超时,整个机制就退化了;三个节点里只有一个副本开了半同步也不够。要强一致应该上 Group Replication 或 PXC,半同步不是那个层次的方案。

三、wait_no_replica 默认 ON

rpl_semi_sync_source_wait_no_replica 默认 1/ON,含义是:只有在达到超时时间时才转异步。设成 OFF,连接副本数低于 wait_for_replica_count(默认 1)就立刻转异步。

默认配置下的行为是:一个半同步副本都没连上,主库照样老老实实等满 10 秒。写入被拖慢,但不会提前放弃。这个默认值偏向“多等一会儿也不降级”,代价是故障时每个事务都要吃一次超时。

四、崩溃重启后的自动提交

宕机重启后,已经写入 binlog 但还没复制出去的事务会被自动提交。这一条不受任何半同步参数控制,只能改代码或在 binlog 层面做实时传输才能挡住。前面三条还能靠参数和监控兜住,这条不行。

wait_point:AFTER_SYNC 与 AFTER_COMMIT

rpl_semi_sync_source_wait_point 默认 AFTER_SYNC。

  • AFTER_SYNC:事务写 binlog 并 fsync 后等待 ACK,收到才在引擎层提交。其他会话看不到未复制的事务,源端本地也没有幻读窗口。
  • AFTER_COMMIT:先引擎提交再等 ACK。其他会话可能在 ACK 到达之前就读到这行;主库此刻 crash,这行没有任何副本持有,切过去就是丢。

MariaDB 的默认值是后者。从 MariaDB 迁到 MySQL,或者两边混部,wait_point 要显式写进配置,别依赖默认值——默认值不同意味着语义不同,不是性能参数差异。

大事务把串行链路堵死

binlog 由单个 dump 线程串行发送。songlibing 在大事务 binlog 传输的实测里模拟了 2GB 的 binlog events:后续所有小事务全部陪跑,实例写入跌到 0,一直持续到半同步超时。

慢日志里的表现是一条 INSERT 耗时 1.3s,或者 COMMIT 耗时 1.3s,看不到任何锁等待——等的是 ACK,不是锁。用锁等待去排查会一无所获。

RPO=0 在这里有两个退化点:一是上面说的崩溃重启自动提交,二是超时退化为异步。后者只能把 timeout 设得极大,但大事务加上大 timeout,会让整个集群长时间不可写。两难,没有参数能同时满足。

切换时主从视角的偏差

无损半同步下客户端视角是一致的,但主从视角未必。旧主可能多出 GTID:binlog 已经发出、relay log 还没收全的阶段(2aa),主库挂了,这部分事务在新主上不存在。

基础配置是双一:sync_binlog=1、innodb_flush_log_at_trx_commit=1。sync_binlog!=1 时,极端情况下从库可能比主库还新。

修复有两条路线:从库补数,新主在提供服务之前远程拉取旧主的 binlog;或者旧主 flashback 回退多余的 GTID。actionsky 的半同步切换实验里还记录了一个细节:MHA 在 GTID 模式下不执行第 5 步补数,是个已知 bug。

三条可执行判据

-- 半同步是否真的在跑,0 就是已降级
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_source_status';

-- 降级后有多少事务走了异步,单调增长就是掉线了
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_source_no_tx';

-- 副本 relay log 的刷盘频率,默认 10000
SHOW VARIABLES LIKE 'sync_relay_log';

再补一条辅助判断:

SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_source_clients';

返回 0 而 enabled 仍是 1,说明当前没有任何半同步副本接入,等待全靠 timeout 兜底。

半同步约束的是等待上限,不是风险上限。超时到点、副本掉线、重启恢复,任意一条都会把它变回异步,而 rpl_semi_sync_source_enabled 这个变量从头到尾不会告诉你发生了什么。

站内相关但切面不同:#8163 讨论应用层位点/GTID 等待与 SBM 失真,#8313 讨论 MTS 并行度,#8120 是 MaxScale causal_reads 的七种模式,#8376 是 Galera 的 wsrep_sync_wait。

推荐文章

程序员茄子在线接单