WAIT_FOR_EXECUTED_GTID_SET 当探针用:三个位置会返回 0,但复制并没有追平
应用层用这个函数做读己之写判据的写法另文写过(站内 #8163)。这里只说另一件事:用同一个函数做外部探针,判断「这条从库到底追平没有」,以及在哪几个地方会被它骗。
函数本身做什么
GTID 开启的实例上执行,阻塞到 gtid_executed 包含给定集合,或者超时:
SELECT WAIT_FOR_EXECUTED_GTID_SET('3E11FA47-...:1-100', 2);
返回 0 表示已执行,1 表示超时,NULL 表示函数出错。它只等条件,不判断复制链路是否健康。这是后面所有误判的来源。
假 0 之一:探针连到了主库
最常犯的一个。探针的连接串指向主库,主库的 gtid_executed 必然包含它刚提交的 GTID,条件立即满足,返回 0,耗时 0ms。指标图上「追平耗时」长期是 0,看起来非常健康。
探针必须连从库,并且连接建立后要核对 @@hostname / @@server_id / @@read_only,把「连错实例」挡在采集之前。
假 0 之二:拿从库自己的 gtid_executed 去等
脚本图省事,直接 SELECT @@global.gtid_executed,再喂给同一个连接上的 WAIT_FOR_EXECUTED_GTID_SET。这是自己等自己,永远返回 0。
等待集合只能来自写侧,也就是源库。可以随写请求带下来,也可以由探针自己在源库写一行心跳、再从源库读回集合。
假 0 之三:NULL 被当成 0
PDO 默认取回字符串 "0"/"1",mysqli 可能取回 int,函数出错时是 NULL。用 $ret == 0 比较,NULL 和 "0" 都会通过;换成 === 0,字符串 "0" 又不相等,把成功判成失败。
统一 (string) $ret === '0',或者先显式判 null 再比较。注意不是所有驱动都把 1 当整数返回。
空转集群:心跳事件不产生 GTID
主库空闲时没有新事务,gtid_executed 不前进。复制心跳(SOURCE_HEARTBEAT_PERIOD,默认 30 秒)只让从库 IO 线程知道源端还活着,不进 GTID 集合。
所以空闲期「等主库的 gtid_executed」等于等一个不动的值,探针无法区分「链路断了」和「根本没写入」。探针必须自己制造一条事务。
探针写法
源库建一张只有一行的表,每次插一行再删掉,拿这次的 GTID 集合(这里用 @@global.gtid_executed 变通,集合可能包含其它并发事务,等待偏保守,但方向安全),到从库上等,记录耗时:
CREATE TABLE IF NOT EXISTS probe.heartbeat (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
ts DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB;
# 源库:写一行,取集合
gset=$(mysql -h"$SRC" -N -e "INSERT INTO probe.heartbeat () VALUES (); SELECT @@global.gtid_executed;")
# 从库:等它,输出毫秒耗时与返回值
mysql -h"$REPL" -N -e "SELECT WAIT_FOR_EXECUTED_GTID_SET('$gset', 2);"
返回值要落成指标:0 / 1 / NULL 三态分开计数,0 的耗时做直方图。只报「是否成功」,会让 2 秒超时和 5ms 追平在报表上看起来一样。
顺带必须一起看的两项
- 复制线程状态。SQL/applier 线程停掉时函数不会报错,只会等到超时返回 1。对照
performance_schema.replication_applier_status和SHOW REPLICA STATUS里的Replica_SQL_Running,别把「等待超时」直接当成「延迟高」,根因可能是线程停了。 - 权限与只读。探针写心跳需要写权限,云上只读实例通常写不了。这时改成在源库侧取业务表真实写入的集合、从库只负责等待;或者退回用
SHOW REPLICA STATUS的Executed_Gtid_Set与源端集合作包含判断(GTID_SUBSET)。
和「读己之写」的区别
应用层那次等待是请求路径上的同步点,超时要回落主库并限流;探针是旁路,超时只应该变成告警,不参与业务决策。两者共用同一个函数,但超时值、失败处理、指标口径都不同,混在一套配置里通常两边都不好用。
参考: