并行复制是 mysql 复制技术中一个至关重要的性能特性,它主要用于解决主从延迟(replication lag)问题。它的核心思想是:在从库上,使用多个工作线程来并发应用(apply)从主库接收到的二进制日志(binary log)事件,从而大幅提升从库的数据回放速度。
一、为什么需要并行复制?—— 问题的根源
在传统的单线程复制模式下(sql 线程为单线程),从库的 sql 线程严格按照主库二进制日志(binlog)中事件的顺序,逐一地、串行地执行 sql 事件。
- 主库:在高并发环境下,多个客户端连接同时操作,可以并行提交事务,吞吐量很高。
- 从库:无论主库多么繁忙,从库都只能用单个线程去重放这些事件,就像一个狭窄的单车道,很容易造成交通堵塞。
这就导致了:从库的应用速度远远跟不上主库的写入速度,数据延迟(seconds behind master)会越来越大。并行复制就是为了将这个“单车道”改造成“多车道”。
二、并行复制的核心:如何判断哪些事务可以并行?
并行复制并非简单地将所有事件乱序并行执行,因为存在依赖关系的事务(例如,对同一行的修改)必须按顺序执行,否则会导致数据不一致。因此,并行复制的核心技术在于如何准确地识别出哪些事务之间没有依赖关系,可以安全地并行执行。
mysql 的并行复制技术经历了多个版本的演进,其算法也在不断优化:
2.1 mysql 5.6:按数据库(schema)并行
- 策略:如果两个事务操作的是不同的数据库,则认为它们没有冲突,可以并行执行。
- 优点:实现简单,开销小。
缺点:粒度太粗。绝大多数应用通常只使用一个数据库,或者不同数据库的负载不均衡,导致无法有效并行。这对于现代微服务或多租户架构来说效果有限。
slave_parallel_workers = 4 -- 设置并行工作线程数 slave_parallel_type = database -- 并行策略为 database
2.2 mysql 5.7:logical_clock(基于组提交)
这是里程碑式的改进,极大地提升了并行复制的效率和通用性。
策略:基于主库的组提交(group commit) 信息。
- 在主库上,高并发时为了提高效率,会将多个不同事务的
commit操作打包成一个组(group)一起写入 binlog 并刷盘。 - 被分到同一个组内提交的事务,意味着它们在主库上是并行执行的,因此它们之间没有最后一序约束(no locking constraint),在从库上也可以安全地并行回放。
- 主库会在 binlog 中为每个事件记录一个
last_committed和sequence_number。last_committed相同的事务,表示它们属于同一个组,可以在从库并行执行。
工作原理:
主库:事务在 prepare 阶段会进入一个队列。当某个事务要刷盘时,它会将其队列中所有已经 prepare 好的事务一起打包成一个组。
binlog:记录形式如下。last_committed 相同的事务(如 6、7、8)可以并行。
# sequence_number=6 last_committed=5 ... # sequence_number=7 last_committed=5 ... # sequence_number=8 last_committed=5 ... # sequence_number=9 last_committed=8 ... -- 必须等8提交后才能执行
从库:协调线程(coordinator thread)会读取这些信息,将 last_committed 相同的事务分发给不同的工作线程(worker thread)并行执行。只有 last_committed 小于当前已执行事务 sequence_number 的事务才能被分发,以保证依赖关系。
优点:并行粒度从数据库级别细化到了事务级别,只要事务在主库上是并行提交的,在从库就能并行回放,效果非常好。
配置:
slave_parallel_workers = 8 slave_parallel_type = logical_clock -- 控制组提交的积极性,值越小组提交越频繁,意味着从库并行度可能更高 binlog_group_commit_sync_delay = 100 -- 微秒级延迟,以等待更多事务成组 binlog_group_commit_sync_no_delay_count = 10 -- 达到一定数量立即成组
2.3 mysql 8.0:writeset(基于事务冲突)
在 5.7 的基础上,mysql 8.0 引入了更先进的 writeset 策略,进一步提升了并行效率。
策略:不再依赖主库的组提交时机,而是直接分析事务本身修改了哪些数据。
- 每个事务都会有一个
writeset,这是一个集合,包含了本事务修改的所有行的唯一哈希值(由库名、表名、主键或唯一索引键值计算得出)。 - 如果两个事务的
writeset没有交集,即它们修改的不是同一行,就说明它们没有冲突,可以并行。 - 主库会维护一个
writeset历史映射(在内存中),记录最近修改过某行数据的事务的sequence_number。当一个新事务到来时,系统会检查其writeset中的每一行,找出所有修改过这些行的最新事务的sequence_number,其中最大的那个数,就是这个新事务的last_committed值。
优点:
- 更高的并行度:即使主库并发很低、组提交很少发生,8.0 也能通过分析行冲突来创造并行机会。它可以让更多的事务被标记为相同的
last_committed。 - 降低延迟敏感:从库的并行不再完全依赖于主库的组提交设置(如
binlog_group_commit_sync_delay),即使主库没有故意延迟提交,从库也能获得很好的并行效果。
配置:
slave_parallel_workers = 16 slave_parallel_type = logical_clock -- writeset 是 logical_clock 的增强子功能 -- 在主库上开启 writeset 识别(也用于增强半同步复制) transaction_write_set_extraction = xxhash64 -- 计算 writeset 的哈希算法 binlog_transaction_dependency_tracking = writeset -- 依赖跟踪模式:commit_order(5.7默认), writeset, writeset_session
三、并行复制的架构
在启用并行复制后,从库的 sql 线程演变为一个协调者线程(coordinator) 和多个工作线程(worker threads) 的架构:
i/o thread:保持不变,负责从主库拉取 binlog 事件并写入本地的中继日志(relay log)。
coordinator thread:
- 负责读取 relay log。
- 根据配置的并行复制策略(database/logical_clock)来判断事务的依赖关系。
- 将可以并行执行的事务分发给不同的 worker thread。
- 维护事务的执行顺序,确保有依赖关系的事务被正确地串行化。
worker threads:多个工作线程,真正负责执行被分配到的 relay log 中的事务。它们是并行的执行单元。
四、举例组提交和writeset提交
想象一下,主库是一个接一个地发出指令的指挥官(串行提交事务),从库是一队士兵。
- 指令1:士兵a,去占领山头x。
- 指令2:士兵b,去占领山头y。
- 指令3:士兵c,去山头x挖战壕。(这个指令依赖于指令1,必须等山头x被占领后才能执行)
4.1 组提交(logical_clock)模式的做法:
指挥官发出指令时没有特意 grouping(因为压力小,指令是串行发出的)。那么从库的士兵们就会认为:“指令1、2、3是在不同时间收到的,它们必须按顺序执行。”
所以执行顺序是:a完成 -> b开始并完成 -> c开始并完成。
结果: 士兵b明明可以去独立完成任务,却被迫要等士兵a完成后才能开始。效率低下。
4.2 writeset模式的做法:
从库这边有一个超级智能的参谋长。他拿到指令清单后,会分析每条指令的具体内容:
- 指令1:修改了地点x。
- 指令2:修改了地点y。
- 指令3:修改了地点x。(同时依赖指令1)
参谋长的推理:
- “指令2(修改y)和指令1(修改x)毫无关系。它们可以同时执行!”
- “指令3(修改x)和指令1(修改x)强相关,必须先执行完指令1,才能执行指令3。”
- “指令3和指令2毫无关系,但指令3依赖于指令1,所以只要指令1完成,指令3就可以和指令2同时执行。”
于是,参谋长制定了这样一个并行执行计划:
- 时间点t1:同时派出士兵a(执行指令1)和士兵b(执行指令2)。
- 时间点t2:士兵a完成任务,成功占领山头x。士兵b还在前往y的路上。
- 时间点t2+:由于指令1已完成,指令3的依赖已解决。参谋长立即派出士兵c(执行指令3),让他和士兵b并行工作。
最终结果:
- 保证了正确性:士兵c是在山头x被占领后才去挖战壕的,逻辑正确。
- 最大化并行:士兵b和士兵a/c的大部分工作都是并行的。整体完成时间远小于串行执行。
- 没有“不必要的等待”:士兵b没有浪费任何时间等待士兵a。
技术原理解析:last_committed的魔法
在writeset模式下,主库在写binlog时,会基于writeset历史映射表,为每个事务重新计算一个last_committed值。
- 对于指令1(修改x):它是第一个修改x的事务,它依赖于之前的所有事务。它的
last_committed值会被设为上一个全局事务的序号(比如0)。 - 对于指令2(修改y):智能算法发现y之前没人修改过,它和指令1没有冲突。因此,它的
last_committed值会被设置为和指令1相同(也就是0)。 - 对于指令3(修改x):智能算法发现它修改了x,而最后一次修改x的是指令1。因此,它的
last_committed值会被设置为指令1的sequence_number(比如1)。
从库的协调线程看到的是:
事务1: last_committed=0, sequence_number=1 事务2: last_committed=0, sequence_number=2 // 与事务1的last_committed相同! 事务3: last_committed=1, sequence_number=3 // 必须等seq_num=1的事务完成
协调规则没变:last_committed相同的事务可以并行。
- 所以,事务1和事务2可以并行执行。
- 事务3必须等待
sequence_number <= 1的事务(即事务1)完成后才能开始。(事务2是否完成不影响事务3的开始)
到此这篇关于详解mysql并行复制的原理的文章就介绍到这了,更多相关mysql并行复制内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论