本文介绍下MySQL 5.7中针对主从并行复制的演变过程,首先MySQL主从延迟是一个永远都无法彻底解决的问题,我们只能通过优化参数来减小延迟,其中一个大的问题是主库可以并发执行事务,但是从库无法完全按着主库的并发度来并行复制,好在MySQL官方也在不断优化主从复制的逻辑,旨在提高从库并行复制的粒度,首先MySQL 5.6支持库级别的并行复制,MySQL 5.7 开始支持基于组提交的COMMIT_ORDER方式的并行复制,以及后面的writeset方式的并行复制,接下来我们展开介绍:
并行复制两个基本原则:
1.有锁冲突的事务,必须被分发到同一个worker中!
2.同一个事务不能被拆开,必须放到同一worker中!
一、MySQL5.7.22之前的COMMIT_ORDER并行复制方式:
1、COMMIT_ORDER并行复制方式的理念:
5.7.22之前的COMMIT_ORDER并行复制方式,借助mysql的组提交,根据mysql两阶段提交,可以知道能达到redo log prepare阶段以及commit阶段的事务,就表示事务已经通过锁冲突的检验了!只要没有锁冲突,就可以并行复制!
MySQL 5.7 commit_order的并行复制策略的思想是:
1)同时处于 prepare 状态的事务,在备库执行时是可以并行的;
2)处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的,
2、COMMIT_ORDER并行复制方式的相关参数
1)由参数 slave-parallel-type 来控制MySQL 5.7.22之前的并行复制策略:
配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
配置为 LOGICAL_CLOCK,表示开启COMMIT_ORDER的并行复制
2)关于参数slave-parallel-type的介绍:
在使用多线程复制(slave_parallel_workers大于0)时,此变量指定用于决定哪些事务可以在副本上并行执行的策略。该变量对未启用多线程的副本没有影响。可能的值包括:
LOGICAL_CLOCK:源端二进制日志组提交中的事务在副本上并行应用。根据其时间戳跟踪事务之间的依赖关系,以提供额外的并行化。设置此值后,可以使用源端的binlog_transaction_dependency_tracking系统变量指定使用writeset(写集)代替时间戳进行并行化,如果事务有可用的写集且与时间戳相比效果更好。
DATABASE:更新不同数据库的事务并行应用。此值仅适用于数据被分区到多个独立并发更新的数据库的源端。必须没有跨数据库约束,因为这些约束可能会在副本上被违反。
当slave_preserve_commit_order为1时,slave_parallel_type必须是LOGICAL_CLOCK。
在设置slave_parallel_type之前,所有复制应用线程必须停止。
当您的复制拓扑使用多级副本时,LOGICAL_CLOCK对于每个远离源的副本级别可能实现较少的并行化。您可以通过在源端使用binlog_transaction_dependency_tracking指定在可能的情况下使用写集代替时间戳进行并行化来减少这种影响。
2)COMMIT_ORDER方式下,
如何影响并发的事务个数!来提高并发度
1) binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
2) binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用fsync。这两个参数是用于故意拉长 binlog 从 write 到 fsync的时间,以此减少 binlog 的写盘次数。在 MySQL 5.7 的并行复制策略里(COMMIT_ORDER方式),它们可以用来制造更多的“同时处于 prepare 阶段的事务”。这样就增加了备库复制的并行度
在 MySQL 5.7 处理备库延迟的时候,可以考虑调整这两个参数值,来达到提升备库复制并发度的目的
二、mysql 5.7.22开始支持基于writeset的并行复制策略:
1、writeset并行复制的设计理念:
在 2018 年 4 月份发布的 MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。
相应地,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。
COMMIT_ORDER,依赖两阶段的组提交,根据同时进入 prepare 和 commit 来判断是否可以并行的策略。
WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
当然为了唯一标识,这个 hash 值是通过“库名 + 表名 + 索引名 + 值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert 语句对应的 writeset 就要多增加一个 hash 值。
writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量;
不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;MySQL 5.7.22 的并行复制策略在通用性上还是有保证的。
2、开启writeset并行复制:
set global slave_parallel_workers=N (N>0)
set global slave-parallel-type=LOGICAL_CLOCK
set global transaction_write_set_extraction=XXHASH64;
set global binlog_transaction_dependency_tracking=WRITESET;
set global binlog_transaction_dependency_history_size=35000;
1) transaction_write_set_extraction参数:
定义用于生成哈希值的算法,该哈希值用于标识与事务相关的写操作。如果你使用的是组复制(Group Replication),则该哈希值用于分布式冲突检测和处理。
在运行组复制的64位系统上,我们建议将此设置为XXHASH64,以避免不必要的哈希碰撞,这些碰撞会导致认证失败并回滚用户事务。参见第17.3.1节,“组复制要求”。要更改此变量的值,必须将binlog_format设置为ROW。如果更改了值,则新值不会立即在副本上生效,直到停止并重新启动副本,使用STOP SLAVE和START SLAVE语句。
当将binlog_transaction_dependency_tracking设置为WRITESET或WRITESET_SESSION时,必须将transaction_write_set_extraction设置为指定算法(不能设置为OFF)。在当前binlog_transaction_dependency_tracking的值为WRITESET或WRITESET_SESSION的情况下,无法更改transaction_write_set_extraction的值。
2) writeset方式下,如何影响并发的事务个数!来提高并发度
通过调整binlog_transaction_dependency_history_size参数:
即便是主库是单线程执行的,那么从库也可能是并行的,只要没有冲突!之前COMMIT_ORDER方式是判断同时处于redo prepare或者commit的事务可以一同提交,这些事务的last committed一样。现在基于writeset的并发复制,会依照事务的 write_set 中的HASH值与已提交的事务的 write_set 进行比对,如果没有冲突,则当前事务与最后一个已提交的事务共享相同的 last_commited,
否则将从全局已提交的 write_set 中删除那个冲突的事务以及之前提交的所有write_set,并退化到 COMMIT_ORDER 计算last_committed 。 每次计算完事务的 last_committed 值以后,检测当前全局已提交事务的 write_set 是否已经超过了 binlog_transaction_dependency_history_size 设置的值,如果超过,则清空已提交事务的全局 write_set,从新开始记录,清空之后意味着下一个也就是第一个事务肯定是COMMIT_ORDER方式计算last_committed,之后发生的事务再次对比已提交的事务的write_set,所以从库并行执行的最大事务个数还受这个binlog_transaction_dependency_history_size 参数影响!
binlog_transaction_dependency_history_size参数:在代码实现上,MySQL采用一个vector的变量存储已经提交的事务的HASH值(也就是writeset方式),所有已经提交的事务的所修改的主键和非空的UniqueKey的值经过HASH后与该vector中的值对比,由此来判断当前提交的事务是否与最近一段时间已经提交的事务更新了同一行,并以此确定依赖关系。
该向量的大小由参数binlog_transaction_dependency_history_size控制,取值范围为1-1000000 ,初始默认值为25000
3)为啥计算writeset需要加唯一值?
在计算writeset的时候,库名+表名+主键+唯一键(有的话),为什么需要加唯一键,因为如果某个表主键为id ,唯一键为a, 此时库中有a=1的数据 主库执行:session A先 执行update t1 set a=6 where id=1; session B 执行update t1 set a=1 where id=2,, 那么如果你在计算writeset的时候没有考虑唯一键,那么这两个session的事务可能会在从库并行执行,就有可能sesstion B先执行而导致唯一键冲突而失败!
4)writeset方式并行复制的限制:
对于没有主键的表 ,或者是有外键约束的表,writeset策略是没法并行的,也会暂时退化为单线程模型!因为wirteset是通过 库名+表名+主键值+(唯一键名字和非空键值)来确定的唯一性的,
原因:如果你没有主键,那么你不能保证算出来的hash 值是唯一的,因为唯一键可以为空,所以没有主键是不能用writeset的。
又因为如果表上有外键,级联更新的行不会记录到binlog中,这样的冲突检测就不准确,所以有外键的表也是无法使用writeset并行复制的。
总结:
MySQL writeset方式的并行复制,writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容(event 里的行数据),节省了很多计算量;不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;但是无主键、有外键的表无法使用writeset方式的并行复制,好在dba制定的开发规范一般都有 强制有主键+无外键的约束。