主备延迟以及备库并行复制策略

保证mysql高可用

  • 主备延迟

    • 原因:运维主动操作,软件升级,主库机器掉电。
  • 同步延迟

    1. 主库A执行完成一个事务写入binlog,时刻T1;
    2. 传给备库,备库B接收到binlog时刻为T2;
    3. 备库B执行完成这个事物,时刻T3.
    • 同步延迟,即同一个事物,T3-T1之间的差值。show slave status可以显示当前备库延迟了多少秒。
  • 主备延迟的来源

    • 有些部署条件下,备库所在机器性能比主库所在性能差。在更新过程会触发大量的读操作。所以,当备库主机上的多个备库都在争抢资源的 时候,就可能会导致主备延迟了。
    • 备库的压力大,备库执行了很多分析语句,备库的查询占用了大量的CPU资源,造成了主备延迟。处理方法:一主多从,分担读的压力。
    • 大事物,如果一个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟。

可靠性优先策略

  • 在双M结构下,两个数据库主备切换的过程如下:

    1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否 则持续重试这一步;

    2. 把主库A改成只读状态,即把readonly设置为true;

    3. 判断备库B的seconds_behind_master的值,直到这个值变成0为止;

    4. 把备库B改成可读写状态,也就是把readonly 设置为false;

    5. 把业务请求切到备库B

  • 可以看到,这个切换流程中是有不可用时间的。因为在步骤2之后,主库A和备库B都处于 readonly状态,也就是说这时系统处于不可写状态,直到步骤5完成后才能恢复。

    在这个不可用状态中,比较耗费时间的是步骤3,可能需要耗费好几秒的时间。这也是为什么需 要在步骤1先做判断,确保seconds_behind_master的值足够小。

可用性优先策略

  • 如果强行把上述步骤4,5最开始执行,系统几乎没有不可用时间,但是可能会出现数据不一致

    • 假设一个表的初始化

      mysql> CREATE TABLE `t` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `c` int(11) unsigned DEFAULT NULL,
      PRIMARY KEY (`id`)
      ) ENGINE=InnoDB;
      insert into t(c) values(1),(2),(3);
      
      insert into t(c) values(4);
      insert into t(c) values(5);
      

      假设,现在主库有大量更新,导致主备延迟达到5秒,在插入一条c=4的语句后,发生了主备切换。如图:采用可用性优先策略,且binlog_format=mixed时的切换流程和数据结果。

    1. 步骤2中,主库A执行完insert语句,插入了一行数据(4,4),之后开始进行主备切换。
    2. 步骤3中,由于主备之间有5秒的延迟,所以备库B还没来得及应用“插入c=4”这个中转日志,就开始接收客户端“插入 c=5”的命令。
    3. 步骤4中,备库B插入了一行数据(4,5),并且把这个binlog发给主库A。
    4. 步骤5中,备库B执行“插入c=4”这个中转日志,插入了一行数据(5,4)。而直接在备库B执行的“插入c=5”这个语句,传到主库A,就插入了一行新数据(5,5)。
    • 最后的结果就是,主库A和备库B上出现了两行不一致的数据。可以看到,这个数据不一致,是 由可用性优先流程导致的。
  • 如果我还是用可用性优先策略,但设置binlog_format=row,情况又会怎样呢?

    因为row格式在记录binlog的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致。而且,两边的主备同步的应用线程会报错duplicate key error并停止。也就是说,这种情况下,备库B的(5,4)和主库A的(5,5)这两行数据,都不会被对方执行。

在可靠性优先的情况下,异常切换会是什么效果

  • 假设,主库A和备库B间的主备延迟是30分钟,这时候主库A掉电了,HA系统要切换B作为主库。 我们在主动切换的时候,可以等到主备延迟小于5秒的时候再启动切换,但这时候已经别无选择了。
  • 采用可靠性优先策略的话,你就必须得等到备库B的seconds_behind_master=0之后,才能切换。但现在的情况比刚刚更严重,并不是系统只读、不可写的问题了,而是系统处于完全不可用的状态。因为,主库A掉电后,我们的连接还没有切到备库B。
  • 但是保持B只读呢?这样也不行。这段时间内,中转日志还没有应用完成,如果直接发起主备切换,客户端查询看不到之前 执行完成的事务,会认为有“数据丢失”。 虽然随着中转日志的继续应用,这些数据会恢复回来,但是对于一些业务来说,查询到“暂时丢 失数据的状态”也是不能被接受的。
  • 在满足数据可靠性的前提下,MySQL高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

备库的并行复制

  • 一个箭头代表了客户端写入主库,另一个箭头代表备库sql_thread执行中转日志。箭头粗细来表示并行度的话,第一个箭头要粗于第二个箭头。

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

  • coordinator就是原来的sql_thread, 不过现在它不再直接更新数据了,只负责读取中转 日志和分发事务。真正更新日志的,变成了worker线程。而work线程的个数,就是由参数 slave_parallel_workers决定的。根据我的经验,把这个值设置为8~16之间最好(32核物理机的情况),毕竟备库还有可能要提供读查询,不能把CPU都吃光了。

    • 不能造成更新覆盖,要求更新同一行的两个事物,必须分发到一个worker中(因为无法决定那个worker先执行)
    • 同一个事物不能被拆开,必须放到同一个worker中(保持事物的原子性)

并行策略一:按表分发策略

  • 按表分发事务的基本思路是,如果两个事务更新不同的表,它们就可以并行。因为数据是存储在 表里的,所以按表分发,可以保证两个worker不会更新同一行。当然,如果有跨表的事务,还是要把两张表放在一起考虑的。

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

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

  • 事物T的分配流程,和应当遵循的原则

    1. 假设在图中的情况下,coordinator从中转日志中读入一个新事务T,这个事务修改的行涉及到表 t1和t3。
    2. 由于事务T中涉及修改表t1,而worker_1队列中有事务在修改表t1,事务T和队列中的某个事 务要修改同一个表的数据,这种情况我们说事务T和worker_1是冲突的。
    3. 按照这个逻辑,顺序判断事务T和每个worker队列的冲突关系,会发现事务T跟worker_2也 冲突。
    4. 事务T跟多于一个worker冲突,coordinator线程就进入等待。
    5. 每个worker继续执行,同时修改hash_table。假设hash_table_2里面涉及到修改表t3的事务 先执行完成,就会从hash_table_2中把db1.t3这一项去掉。
    6. 这样coordinator会发现跟事务T冲突的worker只有worker_1了,因此就把它分配给 worker_1。
    7. coordinator继续读下一个中转日志,继续分配事务。
    1. 如果跟所有worker都不冲突,coordinator线程就会把这个事务分配给最空闲的worker;

    2. 如果跟多于一个worker冲突,coordinator线程就进入等待状态,直到和这个事务存在冲突 关系的worker只剩下1个;

    3. 如果只跟一个worker冲突,coordinator线程就会把这个事务分配给这个存在冲突关系的 worker。

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

按行分发策略

  • 按行复制的核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。显然,这个模式要求binlog格式必须是row。

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

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

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

    这是可能会报唯一键冲突,这是应该是 库名+表名+索引a的名字 +a的值

  • 比如,在上面这个例子中,我要在表t1上执行update t1 set a=1 where id=2语句,在binlog里面记录了整行的数据修改前各个字段的值,和修改后各个字段的值。

  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. 恢复并行模式。

原文地址:https://www.cnblogs.com/jimmyhe/p/11137328.html