编程 Galera 下 causal_reads 等满 10 秒 timeout:换成 wsrep_sync_wait 位掩码

2026-11-04 00:04:36

Galera 下 causal_reads 等满 10 秒 timeout:换成 wsrep_sync_wait 位掩码

三节点 MariaDB Galera + MaxScale readwritesplit 做读写分离,应用写完立刻读,读到旧值。按异步复制集群的经验配了 causal_reads=local,没有任何效果。

causal_reads 在 Galera 上为什么会白等

causal_reads 的实现是 MASTER_GTID_WAIT 函数加服务端会话状态变量追踪(last_gtid)。可选模式:none(默认)、local、global、fast、fast_global、universal、fast_universal。

问题在于 Galera 不更新 gtid_slave_pos,MASTER_GTID_WAIT 等不到目标 GTID,于是每个读都要等满 causal_reads_timeout(默认 10s)才回主库执行。24.02.5 起 fast / fast_global / fast_universal 三种模式可用于 Galera——它们依赖 monitor 的复制状态判断,不做 GTID 等待;非 fast 模式仍然不支持 Galera。

文档:

wsrep_sync_wait:位掩码,按需要开位

文档:

取值 0–15,默认 0,即不做任何因果校验。

位值覆盖语句
1READSELECT、BEGIN / START TRANSACTION
2UPDATEUPDATE、DELETE
4INSERTINSERT、REPLACE
8SHOWSHOW(MariaDB 10.2.9 / 10.1.27 起)

组合按位或:3 = READ+UPDATE+DELETE,5 = READ+INSERT+REPLACE,7 = READ+UPDATE+DELETE+INSERT+REPLACE,9 / 11 / 13 / 15 再叠加 SHOW。实际用得多的是 1(只读路径)、7(读写都覆盖)、15(连诊断 SHOW 也等)。

语义是:语句开始前,本节点要 apply 到「语句开始时全集群已认证」的写集位置;校验期间该节点新查询被阻塞,所以是在拿延迟换一致性。作用域 Global、Session,动态可改。

旧变量 wsrep_causal_reads=ON 等价于 wsrep_sync_wait=1,已不推荐,两者不要叠加。

让校验「静默跳过」的四个条件

源码 sql/wsrep_mysqld.cc 里 wsrep_must_sync_wait 的判定条件不止位掩码:

显式事务直接跳过。 条件里有一项 !thd->in_active_multi_stmt_transaction(),意味着 BEGIN 之后、COMMIT 之前的语句不走 sync wait。位 1 里对 BEGIN 本身是生效的——BEGIN 等了一次,BEGIN 之后的 SELECT 不再等。所以「每请求一个事务」的 ORM 默认模式下,写后读仍在同一事务里,因果校验整段被跳过。

已有等待中的 GTID。 thd->wsrep_cs().sync_wait_gtid().is_undefined() 为假则跳过,避免重复等待。

非客户端线程。 wsrep_client_thread 不成立时不算,复制线程和内部线程不在覆盖范围内。

wsrep_dirty_reads=ON 时整个 wsrep_sync_wait 失效。 值设多少都不起作用。

诊断类 SHOW 有白名单,即使设了 8 位也不等待:SHOW PROCESSLIST、SHOW ENGINE STATUS、SHOW ENGINE INNODB STATUS、SHOW ENGINE MUTEX、SHOW ENGINE LOGS、SHOW PRIVILEGES、SHOW SLAVE STATUS、SHOW BINLOG STATUS、SHOW PROFILE(S)、SHOW SLAVE HOSTS、SHOW RELAYLOG EVENTS 等。

分区时的读阻塞

节点失去 Primary Component 时,因果校验无法完成。带非零 sync_wait 的读会阻塞,可能直接报:

ERROR 1205 (HY000): Lock wait timeout exceeded

监控和健康检查连接要单独设 wsrep_sync_wait=0 或 wsrep_dirty_reads=1,否则探针会跟着卡住并误判节点故障。

配置:会话级,用完复位

SET SESSION wsrep_sync_wait = 1;
UPDATE profiles SET display_name='Ada' WHERE id=42;
SELECT * FROM profiles WHERE id=42;
SET SESSION wsrep_sync_wait = 0;

MaxScale listener 侧用 connection_init_sql_file 注入(),文件里写一行:

SET SESSION wsrep_sync_wait=1

连到该 listener 的连接默认就带 1 位。

连接池注意:SET SESSION 只作用于当前连接,归还再借出时必须显式复位或重设,否则「带 1 位的连接被后台任务借走」,后台任务莫名其妙继承等待。

全局开着不可取——会把 p99 变成 apply lag,所有报表、后台任务、定时脚本都继承等待。生产做法是只在「写后立即读」的流程里按会话打开,这类请求占比很小。

观测手段:

SELECT @@global.wsrep_sync_wait, @@session.wsrep_sync_wait;
SHOW STATUS LIKE 'wsrep_local_recv_queue%';
SHOW STATUS LIKE 'wsrep_flow_control_paused%';
SHOW STATUS LIKE 'wsrep_ready';

慢日志里 SELECT 变慢且伴随 flow control,先确认是不是同步位打开的。

它不防 lost update

wsrep_sync_wait 只保证读到的数据不落后,不解决 P4(Lost Update)异常。读-改-写路径仍需要 SELECT ... FOR UPDATE 之类的显式加锁。

和 causal_reads 的取舍

  • 异步复制 + GTID:causal_reads=local / global 走 MASTER_GTID_WAIT,先看本连接,再看服务级。
  • Galera:wsrep_sync_wait 位掩码,等的是 apply 队列,粒度更细,只按需要开位。
  • 两者不能互替。causal_reads 在 Galera 上的非 fast 模式等于白等一个 timeout。

备选方案:MaxScale 的 CCRFilter(Consistent Critical Read Filter)把写后短时间内的读重定向到写入发生的节点;24.02.5+ 也可以用 causal_reads=fast 系列。

推荐文章

程序员茄子在线接单