现象描述
Slave在开启并行复制后, 默认会乱序提交事务, 可能会引起同步中断;
Slave端表现为同步的SQL线程抛出异常, 为主键重复, 修改的数据行不存在等;
GTID信息类似于: 9a2a50aa-5504-11e7-9e59-246e965d93f4:1-1371939844:1371939846
其中1371939845为报错的事务, 直观上看, Slave端先提交了1371939846事务;
解决办法
MySQL version >= 5.7.5
slave_preserve_commit_order: OFF(default) -> ON
注: binlog_order_commits = ON(default)
问题分析
参考官方的WL#6314和WL#7165, 这里对原文内容进行简单的归纳, 有兴趣的可以看看原文的High Level Architecture;
注: 英文原文中的commit-parent transaction, sequence number指的就是binlog中的last_commited和sequence_number; 即简单翻译中的"逻辑时间戳标记"
WL#6314 关于slave端的并行applier
当事务进入prepare阶段(组提交流程的某一个阶段)时, 这些事务都会获得一个逻辑时间戳的标记, 用来标记最新提交的事务是哪个;
在master端, 有关流程如下:
- 在prepare阶段, 从commit_clock中获取时间戳并存储下来, 用来标记最新提交的事务;
- 在commit阶段(事务已经写入binlog, 但是在引擎层提交前), 对commit_clock执行步进操作;
在Slave端, 有关流程如下:
- coordinate线程会读取relaylog的event, 如果这些event都有相同的逻辑时间戳(last_commited), 那么这些event就可以由worker并行执行;
WL#7165 有关并行复制的并行度优化
背景
参照WL#6314的描述, 虽然已经实现了并行复制, 但是并没有达到预期的程度;
举例: 下图代表各个事务的执行顺序与时间线, 其中P代表单个事务的prepare阶段, 在这个阶段会获取到commit_clock的时间戳, C代表这个事务的写binlog的阶段, 在这里会对commit_clock进行步进操作;
Trx1 ------------P----------C-------------------------------->
|
Trx2 ----------------P------+---C---------------------------->
| |
Trx3 -------------------P---+---+-----C---------------------->
| | |
Trx4 -----------------------+-P-+-----+----C----------------->
| | | |
Trx5 -----------------------+---+-P---+----+---C------------->
| | | | |
Trx6 -----------------------+---+---P-+----+---+---C---------->
| | | | | |
Trx7 -----------------------+---+-----+----+---+-P-+--C------->
| | | | | | |
如上图所示, Trx1, Trx2, Trx3的P阶段获取到的都是同一个last_commited值(比如说是1), 因此这三个事务可以在Slave端并行执行; 同理, Trx4不能和< Trx1, Trx2, Trx3 > 一起并行回放, 因为Trx4的P阶段, 获取到的last_commited值是Trx1执行完步进以后的值(步进之后变成了2);
按照WL#6314的逻辑, Slave端可以发现这七个事务分成了四个事务组, 分别是< Trx1, Trx2, Trx3 >, < Trx4 >, < Trx5, Trx6 >, < Trx7 >;
但是需要注意的是, 对于不同的事务组, < Trx4 > 和 < Trx5, Trx6 > 是能并发执行的, 因为从时间线上看, < Trx4 > 和 < Trx5, Trx6 > 的prepare阶段在时间线上是有重叠的, 这也就意味着这两组事务并不存在锁的冲突, 那么就可以在Slave并行执行;
对于并行度的优化
改进后的并行复制使用锁来判断是否可以进行并发;
基本逻辑如下:
L代表锁阶段开始, C代表锁阶段结束;
A. 可以并行执行:
Trx1 -----L---------C------------>
Trx2 ----------L---------C------->
B. 不可以并行执行:
Trx1 -----L----C----------------->
Trx2 ---------------L----C------->
A中的Trx1和Trx2由于锁阶段存在重合, 也没有发生冲突, 说明Trx1和Trx2是可以并行执行的, 但是B不行, 因为Trx1和Trx2的锁阶段没有重合, 所以无法确认是不是可以并行执行(不做额外的判断, 直接当做不可并行处理, 节约性能开销);
关于锁阶段的判断, WL中明确表示没有进行锁分析, 而是直接把事务提交的一些阶段作为加锁与释放锁的时间点(从事务提交的阶段来看, 也没什么问题);
- 假设在进行存储引擎层的提交之前, 所有的锁都已已经释放(锁阶段结束的时间点);
- 假设在prepare阶段开始的时候, 所有需要的锁已经全部获取到(锁阶段开始的时间点);
原文
- The lock interval ends when the first lock is released in the
storage engine commit. For simplicity, we do not analyze the lock
releases inside the storage engine; instead, we assume that locks
are released just before the storage engine commit.
- The lock interval begins when the last lock is acquired. This may
happen in the storage engine or in the server. For simplicity, we
do not analyze lock acquisition in the storage engine or in the
server; instead, we assume that the last lock is acquired at the end
of the last DML statement, in binlog_prepare. This works correctly
both for normal transactions and for autocommitted transactions.
在MySQL的binlog中, L所指的标记就是last_commited, C所指的标记就是sequence_number;
关于last_commited和sequence_number, WL#7165有做如下描述
- 在事务进入flush阶段前, 会步进transaction.sequence_number的值 --> 显示为sequence_number
在事务进入引擎层提交之前, 会修改 global.max_committed_transaction的值
- = max(global.max_committed_timestamp, transaction.sequence_number)
- = transaction.sequence_number (如果binlog_order_commits使用默认值ON)
因此, Slave端在决定SQL是否可以并发执行时, 参考如下原则:
Slave can execute a transaction if the smallest sequence_number
among all executing transactions is greater than transaction.last_committed.
伪代码会更直观一些:
Slave logic:
- before scheduler pushes the transaction for execution:
wait until transaction_sequence[0].sequence_number >
transaction.last_committed
所以使用基于锁的并行度优化后, 确实可以让WL#6314的< Trx4 > 和 < Trx5, Trx6 > 并发执行;
故障场景还原
Slave上报错的事务为1371939845, binlog内容如下, 事务缺少1371939845;
Master上的事务序列如下:
参考WL#6314的格式, 根据Master的事务序列绘制事务序列图, GTID, last_commited, sequence_number均使用最后两位数作为标记;
由于Slave是乱序提交的, 所以这些事务在Slave的binlog中并非严格按照GTID递增的顺序出现
(78)
Trx N ------C----------------------------------------------->
... | (78) (84)
Trx41 ------+---P--------C---------------------------------->
| (78) | (85)
Trx42 ------+----P-------+-----C---------------------------->
| (78) | | (86)
Trx43 ------+-----P------+-----+---C------------------------>
| (78) | | | (87)
Trx44 ------+------P-----+-----+---+----C------------------->
| (78) | | | | (88)
Trx45 ------+-------P----+-----+---+----+----C-------------->
| |(84) | | | | (89)
Trx46 ------+------------+-P---+---+----+----+----C--------->
| | (84)| | | | | (90)
Trx47 ------+------------+--P--+---+----+----+----+--C------>
| | | | | | |
根据WL#7165的描述, 可以得出: 在Slave上, 当Trx41执行完毕之后, Slave认为, Trx46与Trx47已经可以由coordinate进行调度, 与< Trx42, Trx43, Trx44, Trx45 > 并行执行了, 但是Trx45与Trx46, Trx47 存在业务上的先后顺序(且确实存在锁冲突), 所以先执行的Trx46删除了Trx45需要的数据, 导致同步中断;
既然Trx45和Trx46有锁冲突, 为什么Trx46会拿到84作为last_commited, 而不是88?
参考WL#7165的伪代码,
When @@global.binlog_order_commits is true, in principle we could reduce
the max to an assignment:
global.max_committed_transaction = transaction.sequence_number
MySQL-5.7.21的源代码:
MYSQL_BIN_LOG::ordered_commit -->
process_commit_stage_queue -->
update_max_committed
因此推测主库当时候是如下场景:
< Trx35 ~ Trx45 > 作为一个事务组, 进入到了存储引擎的commit阶段前, 会递增sequence_number, 而不是一次到位的全部加上, 所以Trx46进入prepare阶段时, 刚好是Trx41完成了commit阶段, 所以拿到的是84, 而不是88;
虽然官方描述中, 认为会达到最终一致的状态, 但是同步过程中会存在短暂的不一致现象, 这种现象被描述为"GAP";