编程 MySQL 多线程复制延迟排查:worker 没并行、WRITESET 回退、大事务各查什么

2026-11-01 00:03:57

MySQL 多线程复制延迟排查:worker 没并行、WRITESET 回退、大事务各查什么

replica_parallel_workers(旧名 slave_parallel_workers)设成大于 1 只代表启用了 MTR,不代表事务真的在并行。SQL 线程拆成 coordinator 和多个 worker:coordinator 从 relay log 读事件、分析依赖、分配给 worker 执行,而依赖信息是源库写进 binlog 的。也就是说,并行度的上限在源库就决定了一半,副本端只是照着执行。

下面按排查顺序走:先确认 worker 有没有在并行,再按依赖密度、WRITESET 回退、提交顺序等待、大事务占位、worker 过载五类分型定位。

第一步:确认 worker 是否真在并行

SHOW REPLICA STATUS 里 Replica_SQL_Running_State 常见三种值:

  • Waiting for replica workers to process their queues
  • Waiting for dependent transaction to commit
  • Waiting for preceding transaction to be committed

这些状态本身不是错误,但频繁出现就说明依赖太密,事务该拆了。

两个容易踩的坑:

  • Last_SQL_Errno / Last_SQL_Error 只反映 coordinator 线程的错误,worker 线程的错误看不到。worker 的错误要去 performance_schema.replication_applier_status_by_worker 看 LAST_ERROR_NUMBER / LAST_ERROR_MESSAGE,0 与空串表示无错。
  • Seconds_Behind_Source 在 MTR 下仍可用,但基于 Exec_Source_Log_Pos,可能不反映最近提交事务的位置。做 cutover 时不要依赖它,也不要依赖云监控的 ReplicaLag。

看 worker 实际状态:

SELECT WORKER_ID, ACTIVE, LAST_ACTIVE,
       TIME_APPLYING_CURRENT_TRX, TIME_APPLYING_LAST_TRX,
       APPLYING_TRANSACTION,
       LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE
FROM performance_schema.replication_applier_status_by_worker;

理想情况是所有 worker 都 ACTIVE。差异明显时的典型对应关系:依赖冲突导致串行、缺索引导致单事务太久、多进程等同一把锁、正在应用 DDL。只有 1 个 active 线程且 TIME_APPLYING_CURRENT_TRX 很高,往往是串行化的大事务。

coordinator 侧看队列:

SELECT * FROM performance_schema.replication_applier_status_by_coordinator;

这张表显示 coordinator 最近缓冲给某个 worker 队列的事务,START / END 时间戳分别是读第一个事件、缓冲完最后一个事件的时间。

SHOW REPLICA STATUS 只能看到 IO 和 SQL 两个 running 状态,看不到 coordinator 状态。要用 sys 视图补:

SELECT * FROM sys.replication_status;
SELECT * FROM sys.replication_lag;

sys.replication_lag 有 channel_name、max_lag_from_original、max_lag_from_immediate,时间粒度到微秒,按 worker 取最大延迟;sys.replication_status 有 channel、io_state、co_state(coordinator)、w_state(worker)、lag_from_original、lag_from_immediate,并行复制的 worker 会以 channel 名 (1) (2) (3) (4) 的形式逐行列出。

类型一:依赖密度过高

binlog 里每个事务带两个逻辑时间戳:sequence_number(每个 binlog 文件从 1 重新计数)和 last_committed(与之冲突的最近提交事务的 sequence_number,总是小于 sequence_number)。last_committed 相同的事务可以并行。

例如 sequence_number 为 2347、2348、2349、2351 的事务 last_committed 都指向这组之前的事务,可并行;2350 依赖 2348,不能与它们并行。

COMMIT_ORDER 依赖的是组提交时间。源库高并发,或调大 binlog_group_commit_sync_delay 制造更大组提交时效果好;但它只看时序,不考虑数据访问的真实独立性,实际并行度常比预期低。另外 binlog_group_commit_sync_delay 对 WRITESET 无效,且会增加源库事务延迟。

判据:coordinator 侧 Waited at clock conflicts 计数表示事务/事件依赖。多数情况下这个值高且持续增长是正常的,靠 WRITESET + LOGICAL_CLOCK 减少。修法就是把大事务拆小,并把 binlog_transaction_dependency_tracking 设为 WRITESET。

类型二:WRITESET 回退

WRITESET 从 MySQL 8.0.1 引入:对事务修改的每一行生成 hash 组成 writeset,writeset 不重叠的事务可并行,与提交时间窗口、会话无关。writeset 在源库生成后写入 binlog,副本不需要解析行数据。官方示例里,一组在 COMMIT_ORDER 下需要 5 个串行步骤的负载,用 WRITESET 可减到 3 步,并行度从最多 2 个事务提到 4 个事务。

WRITESET 永远不会比 COMMIT_ORDER 更差:只有能给出更优 commit_parent 时才替换。

前提是 transaction_write_set_extraction 必须设为 XXHASH64 或 MURMUR32(不能是 OFF),并且表上要有主键或唯一键来唯一标识行。

以下情况用不了 writeset,会回退:

  • 无主键或唯一键的表
  • 含 DDL 语句的事务
  • 访问外键关系中的父表(级联更新的行不进 binlog,冲突检测不准)
  • 空 writeset,或只有部分 writeset

WRITESET_SESSION 额外要求同一会话提交的两个事务不能在副本并行。

版本差异:MySQL 8.4 起 binlog_transaction_dependency_tracking 默认就是 WRITESET。

类型三:提交顺序等待

replica_preserve_commit_order=ON 时强制保持源库提交顺序,跨会话也保持,前提是副本要开 binlog。状态里反复出现 Waiting for preceding transaction to be committed 就是这一类。

约束关系:replica_preserve_commit_order=1 时 replica_parallel_type 必须是 LOGICAL_CLOCK;修改 replica_parallel_type 前须停止所有 apply 线程。

官方不建议关闭 replica_preserve_commit_order:有下游副本、或者要做主备提升时,关闭会让副本的 binlog 顺序与源库不同。

这条路径上还有一个已修的问题值得记:MySQL 8.0.23 changelog 修了一个 bug——在 preserve commit order 的多线程副本上,若等待提交的后序事务锁住了前序事务需要的行而死锁,且没有事务重试,回滚的 worker 会直接退出而不通知其它 worker,导致复制停滞。修复后该 worker 会等到轮到自己再调用 rollback。

类型四:大事务占位

单事务几十 MB 到 GB 级时,coordinator 和其中一个 worker 会长时间只处理这一个事务,其它 worker 处理完前面的事务后全部空闲。判定很直接:看 ACTIVE 列,只有一个 worker 活跃,且它的 TIME_APPLYING_CURRENT_TRX 一直很高。修法是拆小事务,靠调 worker 数是绕不过去的。

coordinator 侧的等待计数按这个顺序读:

  • Waited due the total size 理想为 0。非 0 且在两个采样间隔之间增长、同时伴随延迟,先查大事务;确认没有大事务,再考虑调大 replica_pending_jobs_size_max。
  • Waited (count) when workers occupied 应尽量低。两个采样间隔之间变化明显,考虑调大 worker 数。
  • Waited at clock conflicts 归到类型一处理。

类型五:worker 过载与副本侧串行点

worker 数不是越多越好,超过某个点会因为锁竞争等并发效应降低性能。起点可以按副本 vCPU 数定,或者大于 2xlarge 的实例从 4 开始观察。合适值取决于副本规格和负载,需自行验证。

副本侧还有几处天然串行或放大:

  • 副本上的长查询、表扫描、InnoDB history list 过长、大量行锁等待都会拖慢 apply。副本上出现大量锁等待时,反而应考虑降低并行度。
  • DDL 在副本上由单线程应用,其余事件要等它完成,即使是 online DDL 也会造成延迟。
  • 无主键表在 ROW 格式下,源库一次全表扫描就能完成的多行语句,在副本上会对每一行做一次全表扫描。sql_require_primary_key=ON(MySQL 8+)可以强制要求主键。

先排除 IO 线程滞后:主库的 binlog 文件名比副本的 Master_Log_File 新,说明 IO 落后而不是 apply 慢;Cloud SQL 上 network_lag ≈ 0 而 replica_lag 高,说明瓶颈在 apply。

修完上述问题后,若仍有大量 worker 长时间不活跃(看 LAST_ACTIVE 列),可考虑降低 replica_parallel_workers;若所有 worker 一直忙,则可提高。

参数默认值与版本差异

  • replica_parallel_type:取值为 LOGICAL_CLOCK(推荐)或 DATABASE。所有 RDS MySQL 8.0 与 Aurora MySQL 3 默认 LOGICAL_CLOCK。
  • replica_parallel_workers:RDS MySQL 8.0.27 与 Aurora 3.04 之前默认 0(单线程),之后默认 4。设置后不会立即生效,需要 STOP/START 复制,RDS 上用 mysql.rds_stop_replication / mysql.rds_start_replication。
  • MySQL 8.4:WL#13955: Remove replica_parallel_type 移除了 replica_parallel_type 系统变量及相关功能(Percona XtraBackup 仓库提交 83e8d48),applier 只保留 logical clock 算法。
  • MySQL 8.4 起 binlog_transaction_dependency_tracking 默认 WRITESET。

一个案例:PXC 当异步副本

配置 replica-parallel-workers=96、LOGICAL_CLOCK、preserve-commit-order=ON、WRITESET、history-size=250000,96 个复制线程各承担约 1% 的事务,延迟仍然持续增长、追不上主库。关掉集群里另外两个节点后,处理速度提高 3–4 倍,延迟回到 0;重新加入节点时,处理速率从约 140k handlers/s 掉到约 40k handlers/s。

原因是 PXC 作为异步副本时,每个 binlog 事件要重新生成 writeset,再走认证与组通信,写放大和同步开销叠加,多节点下每笔事务还要等所有节点确认。这类瓶颈不在 applier 侧,调 worker 数没有用。

前四类在副本端就能动手,WRITESET 相关的那部分取决于源库的写入方式和表结构,最后一类要回到复制拓扑本身。

推荐文章

程序员茄子在线接单