面试官:如何设计一个Mysql的主备并行复制策略

本文阅读 16 分钟
首页 代码,Java 正文

  如果备库执行日志的速度持续低于主库生成日志的速度,主备库数据的延迟有可能是小时级别。对于一个压力持续比较高的主库来说,备库很可能永远都追不上主库的节奏。

  这就需要借助备库的并行复制能力了。

img

  谈到主备的并行复制能力,要关注的是图中圈出来的地方。一个箭头代表了客户端写入主库,另一箭头代表的是备库上sql_thread执行中转日志(relay log)。如果用箭头的粗细来代表并行度的话,第一个箭头要明显粗于第二个箭头。

  在主库上,影响并发度的原因是各种锁。由于InnoDB引擎支持行锁,除了“热点行”这种极端场景外,对业务并发度的支持还是可以的。

  日志在备库上的执行,就是图中备库上sql_thread更新数据(DATA)的逻辑。如果是用单线程的话,就会导致备库应用日志不够快,造成主备延迟。

  在MySQL5.6之前,MySQL只支持单线程复制,由此在主库并发高、QPS高时就会出现严重的主备延迟问题。

  从单线程复制到最新版本的多线程复制,中间的演化经历了好几个版本。所有的多线程复制机制,都是要把图1中只有一个线程的sql_thread,拆成多个线程,符合多线程模型: img

  coordinator就是原来的sql_thread, 不过现在它只负责读取中转日志和分发事务。更新日志的变成了worker线程。参数slave_parallel_workers决定work线程的个数。

coordinator在分发的时候,需要满足以下这两个基本要求:

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

2.1 按表分发策略

  如果两个事务更新不同的表,它们就可以并行。因为可以保证两个worker不会更新同一行。

  如果有跨表的事务,还是要把两张表放在一起考虑的。

img

  每个 worker 线程对应一个 hash 表,用于保存当前正在这个 worker 的执行队列里的事务所涉及的表。hash表的key是库名.表名,value是一个数字,表示队列中有多少个事务修改这个表。

  在有事务分配给 worker 时,事务里面涉及的表会被加到对应的 hash 表中。worker 执行完成后,这个表会被从hash表中去掉。

  hash.table 1表示,现在 worker1 的待执行事务队列里,有4个事务涉及到 db1.t1 表,有1个事务涉及到 db2.t2 表;hash.table 2表示,现在worker2 中有一个事务会更新表 t3 的数据。

  假设在图中的情况下,coordinator从中转日志中读入一个新事务T,修改的行涉及到表 t1 和 t3。

  由于事务 T 中涉及修改表 t1,worker1 队列中有事务在修改表t1,事务 T 和队列中的某个事务要修改同一个表的数据,事务T 和 worker1 是冲突的。

  按照这个逻辑,顺序判断事务 T 和每个 worker 队列的冲突关系,会发现事务 T 跟 worker2 也冲突。

  事务 T 和多于1个 worker 冲突,coordinator线程进入等待。

  每个 worker 继续执行,同时修改 hashtable。假设 hash.table 2里面涉及到修改表 t3 的事务先执行完成,就会从hash.table 2中把 db1.t3 这一项去掉。

  这样 coordinator 会发现跟事务 T 冲突的只有worker1了,因此就把它分配给worker1。

每个事务在分发的时候,涉及到以下三种情况:

  1. 如果跟所有 worker 都不冲突,coordinator 线程就会把这个事务分配给最空闲的 woker;
  2. 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突关系的 worker 只剩下1个;
  3. 如果只和一个 worker 冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的 worker。

  这个按表分发的方案,在多个表负载均匀的场景里应用效果很好。但是,如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个worker中,就变成单线程复制了。

2.2 按行分发策略

  按行复制的核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。

  这时判断一个事务T和worker是否冲突,用的就规则就不是修改同一个表,而是修改同一行。

  按行复制和按表复制的数据结构差不多,也是为每个 worker 分配一个 hash 表。只是要实现按行分发,这时候的 key,就必须是库名+表名+唯一键的值。

  但这个唯一键只有主键id还是不够的,我们还需要考虑表中除了主键,还有唯一索引的情况:

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);

假设要在主库执行这两个事务: img

  这两个事务要更新的行的主键值不同,但是如果它们被分到不同的 worker,就有可能 session B 的语句先执行。这时候id=1的行的 a 的值还是 1,就会报唯一键冲突。

  因此,基于行的策略,事务hash表中还需要考虑唯一键,即key应该是库名+表名+索引a的名字+a的值。

  要在表 t1 上执行update t1 set a=1 where id=2语句,在binlog里面记录了数据修改前后各个字段的值。

  因此,coordinator在解析这个语句的 binlog 的时候,这个事务的hash 表就有三项:

  1. key=hash_func(db1+t1+“PRIMARY”+2), value=2 这里value=2是因为修改前后的行id值不变,出现了两次。
  2. key=hash_func(db1+t1+“a”+2), value=1,表示会影响到这个表 a=2 的行。
  3. key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表 a=1 的行。

  相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源。这两个方案其实都有一些约束条件:

  1. 要能够从binlog里面解析出表名、主键值和唯一索引的值。即主库的binlog格式必须是row;
  2. 表必须有主键;
  3. 不能有外键。表上如果有外键,级联更新的行不会记录在binlog中,这样冲突检测就不准确。

  按行分发策略的并行度更高,如果是要操作很多行的大事务的话,按行分发的策略有两个问题:

  1. 耗费内存。比如一个语句要删除100万行数据,这时候hash表就要记录100万个项。
  2. 耗费CPU。解析binlog,然后计算hash值。

  所以在实现这个策略的时候会设置一个阈值,单个事务如果超过设置的行数阈值(比如,如果单个事务更新的行数超过10万行),就暂时退化为单线程模式,退化过程的逻辑:

  1. coordinator 暂时 hold 住这个事务;
  2. 等待所有worker都执行完成,变成空队列;
  3. coordinator直接执行这个事务;
  4. 恢复并行模式。

  MySQL5.6版本支持了并行复制,只是支持的粒度是按库并行。用于决定分发策略的hash表里,key就是数据库名。   如果在主库上有多个DB,并且各个DB的压力均衡,使用这个策略的效果会很好。

相比于按表和按行分发,这个策略有两个优势:

  1. 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多,不会出现需要构造100万个项这种情况。
  2. 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名。

  但如果主库上的表都放在同一个DB里面,这个策略就没有效果了;或者如果不同DB的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。

MariaDB的并行复制策略利用了redo log组提交(group commit)优化这个特性:

  1. 能够在同一组里提交的事务,一定不会修改同一行;
  2. 主库上可以并行执行的事务,备库上也一定是可以并行执行的。

具体实现:

  1. 在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1; commit_id直接写到binlog里面;
  2. 传到备库的时候,相同commit_id的事务分发到多个worker执行;
    <li>这一组全部执行完成后,coordinator再去取下一批。 MariaDB的这个策略,目标是“模拟主库的并行模式”。但它并没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的。</li>

img

  假设有三组事务在主库执行,在trx1、trx2和trx3提交的时候,trx4、trx5和trx6是在执行的。这样,在第一组事务提交完成的时候,下一组事务很快就会进入commit状态。

  按照MariaDB的并行复制策略,备库上的执行效果如下图: img

  在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。

  另外,这个方案很容易被大事务拖后腿。假设trx2是一个超大事务,那么在备库应用的时候,trx1和trx3执行完成后,就只能等trx2完全执行完成,下一组才能开始执行。这段时间,只有一个worker线程在工作,是对资源的绝对浪费。

  MySQL5.7版本也提供了类似的功能,由参数slave-parallel-type来控制并行复制策略:

  • 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
  • 配置为LOGICAL_CLOCK,表示的就是类似MariaDB的策略。

同时处于“执行状态”的所有事务,是不是可以并行?

  不能。

  因为,这里面可能有由于锁冲突而处于锁等待状态的事务。如果这些事务在备库上被分配到不同的worker,就会出现备库跟主库不一致的情况。

  MariaDB这个策略的核心是“所有处于commit”状态的事务可以并行。事务处于commit状态,表示已经通过了锁冲突的检验了。

img

  不用等到commit阶段,只要能够到达redo log prepare阶段,就表示事务已经通过锁冲突的检验

MySQL 5.7并行复制策略的思想:

  1. 同时处于prepare状态的事务,在备库执行时是可以并行的;
  2. 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。

binlog的组提交有两个参数:

  1. binlog_group_commit_sync_delay,表示延迟多少微秒后才调用fsync;
  2. binlog_group_commit_sync_no_delay_count,表示累积多少次以后才调用fsync。

  这两个参数是用于故意拉长binlog从write到fsync的时间,以此减少binlog的写盘次数,可以用来制造更多的“同时处于prepare阶段的事务”。这样就增加了备库复制的并行度。

  这两个参数,既可以故意让主库提交得慢些,又可以让备库执行得快些,有点意思。

  MySQL 5.7.22增加了一个新的并行复制策略 —— 基于WRITESET的并行复制。

  新增了一个参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略,可选值:

  1. COMMIT_ORDER,表示的就是根据同时进入prepare和commit来判断是否可以并行的策略。
  2. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。
  3. WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。

  当然为了唯一标识,这个hash值是通过“库名+表名+索引名+值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增加一个hash值。

这个实现相比前面的按行分发的策略有很大的优势:

  1. writeset是在主库生成后直接写入到binlog里面的,这样在备库执行的时候,不需要解析binlog内容(event里的行数据),节省了很多计算量;
  2. 不需要把整个事务的binlog都扫一遍才能决定分发到哪个worker,更省内存;
  3. 由于备库的分发策略不依赖于binlog内容,所以binlog是statement格式也是可以的。

  对于“表上没主键”和“外键约束”的场景,WRITESET策略也是没法并行的,也会暂时退化为单线程模型。

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://wangjiawei.blog.csdn.net/article/details/113648508
-- 展开阅读全文 --
安全面试之XSS(跨站脚本攻击)
« 上一篇 07-24

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复