主从复制

  1. 第一个黑色箭头:客户端写入主库,第二个黑色箭头:从库上sql_thread执行relaylog,前者的并发度大于后者
  2. 在主库上,影响并发度的原因是,InnoDB支持行锁,对业务并发度的支持还算比较友好
  3. 如果在从库上采用单线程(MySQL 5.6之前)更新DATA的话,有可能导致从库应用relaylog不够快,造成主从延迟

多线程模型

  1. coordinator就是原来的sql_thread,但不会再直接应用relaylog后更新DATA,只负责读取relaylog分发事务
  2. 真正更新日志的是worker线程,数量由参数slave_parallel_workers控制
1
2
3
4
5
6
mysql> SHOW VARIABLES LIKE '%slave_parallel_workers%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| slave_parallel_workers | 4 |
+------------------------+-------+

分发原则

  1. 不能造成更新覆盖,更新同一行的两个事务,必须被分到同一个worker
  2. 同一个事务不能被拆开,必须放到同一个worker

并行复制策略

MySQL 5.5

按表分发策略

  1. 基本思路:如果两个事务更新的是不同的表,那么就可以并行
  2. 如果有跨表的事务,还是需要将两张表放在一起考虑
具体逻辑
  1. 每个worker线程对应一个hash表,用于保存当前正在这个worker的执行队列里的事务所涉及的表
    • key库名.表名value是一个数字,表示队列中有多少事务修改这个表
  2. 在有事务分配给worker时,事务里面涉及到的表会被加到对应的hash表中
  3. worker执行完成后,这个表会从hash表中去掉
  4. hash_table_1表示:现在worker_1待执行事务队列中,有4个事务涉及到db1.t1,有1个事务涉及到db2.t2
  5. hash_table_2表示:现在worker_2待执行事务队列中,有1个事务涉及到db1.t3
  6. 现在coordinatorrelaylog中读入一个事务T,该事务修改的行涉及到db1.t1db1.t3
  7. 分配流程
    • 事务T涉及到修改db1.t1,而worker_1的队列中有事务在修改db1.t1,事务Tworker_1是冲突的
    • 按照上面的逻辑,事务Tworker_2也是冲突的
    • 事务T多于1worker冲突,coordinator线程进入等待
    • 每个worker继续执行,同时会修改hash_table
      • 假设hash_table_2里涉及到修改db1.t3先执行完,hash_table_2会把db1.t3去掉
    • coordinator发现跟事务T冲突的只有worker_1,因此直接将事务T分配给worker_1
    • coordinator继续读取下一个relaylog,继续分发事务
冲突关系
  1. 如果事务与所有worker都不冲突coordinator线程就会把该事务分发给最空闲worker
  2. 如果事务跟多于1个worker冲突,coordinator线程就会进入等待状态,直到和该事务存在冲突关系的worker只剩下一个
  3. 如果事务只跟1个worker冲突,coordinator线程就会把该事务分发给该worker
小结
  1. 适用于在多个表负载均匀的场景
  2. 如果碰到热点表,有可能退化为单线程复制

按行分发策略

  1. 核心思路:如果两个事务没有更新相同的行,它们是可以在从库上并行执行
  2. 要求:binlog必须采用ROW格式
  3. 事务Tworker是否冲突的判断依据:修改同一行
  4. 为每个worker分配一个hash表,key库名+表名+唯一键的值
唯一键
1
2
3
4
5
6
7
8
9
CREATE TABLE `t1` (
`id` INT(11) NOT NULL,
`a` INT(11) DEFAULT NULL,
`b` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `a` (`a`)
) ENGINE=InnoDB;

INSERT INTO t1 VALUES (1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);
session A session B
UPDATE t1 SET a=6 WHERE id=1;
UPDATE t1 SET a=1 WHERE id=2;
  1. 如果两个事务被分发到不同的workersession B的事务有可能先执行,报唯一键冲突错误
  2. 因此基于按行分发的策略,事务hash还需要考虑唯一键,key库名+表名+索引a的名字+a的值
  3. coordinator在执行UPDATE t SET a=1 WHERE id=2binlog时,hash表的内容
    • key=hash_func(db1+t1+"PRIMARY"+2), value=2
      • value=2:修改前后的行id值不变,出现了2次
    • key=hash_func(db1+t1+"a"+2), value=1
      • 影响到a=2的行
    • key=hash_func(db1+t1+"a"+1), value=1
      • 影响到a=1的行
  4. 相对于按表分发的策略,按行分发的策略在决定线程分发的时候,需要消耗更多的计算资源

对比

  1. 按表分发或按行分发的约束
    • 能够从binlog解析出表名,主键值和唯一索引的值,因此必须采用ROW格式的binlog
    • 必须有主键,因为隐含主键是不会在binlog中体现
    • 不能有外键,因为级联更新的行是不会记录在binlog中,这样冲突检测是不准确的
  2. 按行分发策略的并发度更高
  3. 如果操作很多行的大事务,按行分发策略的问题
    • 耗费内存:如果要删除100W行数据,hash表就要记录100W个记录
    • 耗费CPU:解析binlog,然后计算hash
    • 优化:设置行数阈值,当单个事务超过设置的行数阈值,就退化为单线程模式,退化过程
      • coordinator暂时先hold住这个事务
      • 等待所有worker执行完成,变成了空队列
      • coordinator直接执行这个事务
      • 恢复并行模式

MySQL 5.6

  1. MySQL 5.6版本,支持粒度为按库分发的并行复制
  2. 在决定分发策略的hash表里,key数据库名
  3. 该策略的并行效果,取决于压力模型,如果各个DB的压力均匀,效果会很好
  4. 相比于按表分发按行分发,该策略的两个优势
    • 构造hash值很快,只需要数据库名,并且一个实例上DB数不会很多
    • 不要求binlog的格式,因为STATEMENT格式的binlog也很容易拿到数据库名
  5. 如果主库上只有一个DB或者不同DB的热点不同,也起不到并行的效果

MariaDB

  1. MariaDB的并行复制策略利用了redolog组提交(group commit)
    • 能够在同一组里提交的事务,一定不会修改同一行
    • 主库上可以并行执行的事务,在从库上也一定可以并行执行
  2. 具体做法
    • 在一组里面提交的事务,有一个相同的commit_id,下一组就是commit_id+1
    • commit_id直接写到binlog里面
    • 传到从库应用的时候,相同commit_id的事务可以分发到多个worker上执行
    • 这一组全部执行完成后,coordinator再去取下一批
  3. MariaDB目标:模拟主库的并发行为
    • 问题:并没有真正的模拟主库并发度,在主库上,一组事务在commit的时候,下一组事务可以同时处于执行中的状态

主库并发事务

  1. 在主库上,在trx1trx2trx3提交的时候,trx4trx5trx6是在执行
  2. 在第一组事务提交完成后,下一组事务很快就会进入commit状态

从库并发复制

  1. 在从库上,必须等第一组事务完全执行完成后,第二组事务才能开始执行,与主库相比,吞吐量是下降的
  2. 并且很容易被大事务拖后腿
    • 假设trx2是一个超大事务trx1trx3执行完成后,只能等trx2完全执行完成,下一组才能开始执行
    • 这段期间,只有一个worker线程在工作,是对资源的浪费

MySQL 5.7

  1. slave_parallel_type=DATABASE,使用MySQL 5.6的按库分发的并行复制策略
  2. slave_parallel_type=LOGICAL_CLOCK,使用类似MariaDB的策略,但针对并行度做了优化
1
2
3
4
5
6
mysql> SHOW VARIABLES LIKE '%slave_parallel_type%';
+---------------------+----------+
| Variable_name | Value |
+---------------------+----------+
| slave_parallel_type | DATABASE |
+---------------------+----------+

LOGICAL_CLOCK

  1. 并不是所有处于执行状态的事务都可以并行的
    • 因为里面可能包括由于锁冲突而处于锁等待状态的事务
    • 如果这些事务在从库上被分配到不同的worker,会出现主从不一致的情况
  2. MariaDB的并行复制策略:所有处于redolog commit状态都事务是可以并行的
    • 事务处于redolog commit状态,表示已经通过了锁冲突的检验
  3. MySQL 5.7的并行复制策略
    • 同时处于redolog prepare fsync状态的事务,在从库执行时是可以并行的
    • 处于redolog prepare fsync状态和redolog commit状态之间的事务,在从库上执行时也是可以并行的
  4. binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count
    • 故意拉长binlogwritefsync的时间,以此来减少binlog的写盘次数
    • 在MySQL 5.7,可以制造更多同时处于redolog prepare fsync阶段的事务,增加从库复制的并行度
    • 故意让主库提交慢些让从库执行快些

只要达到redolog prepare fsync阶段,就已经表示事务已经通过了锁冲突的检验

参考资料

《MySQL实战45讲》