MYSQL主从一致

毫不夸张地说,MySQL能够成为现下最流行的开源数据库,binlog功不可没。
在最开始,MySQL是以容易学习和方便的高可用架构,被开发人员青睐的。而它的几乎所有的高可用架构,都直接依赖于binlog。虽然这些高可用架构已经呈现出越来越复杂的趋势,但都是从最基本的一主一备演化过来的。

Mysql主备基本原理

image.png

其中主备内部完整流程图如下:


主备流程图.png

备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程是这样的:

  1. 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量。
  2. 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_thread和sql_thread。其中io_thread负责与主库建立连接。
  3. 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B。
  4. 备库B拿到binlog后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread读取中转日志,解析出日志里的命令,并执行。

binlog 三种格式在主备中

delete 带上limit会导致报warning 因为可能导致主备不一致问题

delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;

由于statement格式下,记录到binlog里的是语句原文,因此可能会出现这样一种情况:在主库执行这条SQL语句的时候,用的是索引a;而在备库执行这条SQL语句的时候,却使用了索引t_modified。因此,MySQL认为这样写是有风险的。

所以要选择row格式么?

因为有些statement格式的binlog可能会导致主备不一致,所以要使用row格式。
但row格式的缺点是,很占空间。比如你用一个delete语句删掉10万行数据,用statement的话就是一个SQL语句被记录到binlog中,占用几十个字节的空间。但如果用row格式的binlog,就要把这10万条记录都写到binlog中。这样做,不仅会占用更大的空间,同时写binlog也要耗费IO资源,影响执行速度。
所以,MySQL就取了个折中方案,也就是有了mixed格式的binlog。mixed格式的意思是,MySQL自己会判断这条SQL语句是否可能引起主备不一致,如果有可能,就用row格式,否则就用statement格式。

循环复制问题

实际生产上使用比较多的是双M结构,双M结构和M-S结构,其实区别只是多了一条线,即:节点A和B之间总是互为主备关系。这样在切换的时候就不用再修改主备关系。

image.png
双M结构还有一个问题需要解决:循环复制问题

业务逻辑在节点A上更新了一条语句,然后再把生成的binlog 发给节点B,节点B执行完这条更新语句后也会生成binlog。(我建议你把参数log_slave_updates设置为on,表示备库执行relay log后生成binlog)。
那么,如果节点A同时是节点B的备库,相当于又把节点B新生成的binlog拿过来执行了一次,然后节点A和B间,会不断地循环执行这个更新语句,也就是循环复制了。这个要怎么解决呢?
解决方案:

  1. 规定两个库的server id必须不同,如果相同,则它们之间不能设定为主备关系;
  2. 一个备库接到binlog并在重放的过程中,生成与原binlog的server id相同的新的binlog;
  3. 每个库在收到从自己的主库发过来的日志后,先判断server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。按照这个逻辑,如果我们设置了双M结构,日志的执行流就会变成这样:
  4. 从节点A更新的事务,binlog里面记的都是A的server id;
  5. 传到节点B执行一次以后,节点B生成的binlog 的server id也是A的server id;
  6. 再传回给节点A,A判断到这个server id与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了。

主备延迟情况

1.同步延迟

与数据同步有
关的时间点主要包括以下三个:

  1. 主库A执行完成一个事务,写入binlog,我们把这个时刻记为T1;
  2. 之后传给备库B,我们把备库B接收完这个binlog的时刻记为T2;
  3. 备库B执行完成这个事务,我们把这个时刻记为T3。
    所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是T3-T1。

seconds_behind_master,用于表示当前备库延迟了多少秒。
seconds_behind_master的计算方法是这样的:

  1. 每个事务的binlog 里面都有一个时间字段,用于记录主库上写入的时间;
  2. 备库取出当前正在执行的事务的时间字段的值,计算它与当前系统时间的差值,得到
    seconds_behind_master。
    可以看到,其实seconds_behind_master这个参数计算的就是T3-T1。所以,我们可以用
    seconds_behind_master来作为主备延迟的值,这个值的时间精度是秒。

主备延迟的来源

1.备库机器差
解决方法:对称部署
2.备库压力大
解决方法:1. 一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。

  1. 通过binlog输出到外部系统,比如Hadoop这类系统,让外部系统提供统计类查询的能力。
    3.大事务
    不要一次性地用delete语句删除太多数据
    要控制每个事务删除的数据量,分成多次删除。
    4.大表DDL
    处理方案就是,计划内的DDL,建议使用ghost方案

可靠性优先策略

image.png

从状态1到状态2切换的详细过程是这样的:

  1. 判断备库B现在的seconds_behind_master,如果小于某个值(比如5秒)继续下一步,否则
    持续重试这一步;
  2. 把主库A改成只读状态,即把readonly设置为true;
  3. 判断备库B的seconds_behind_master的值,直到这个值变成0为止;
  4. 把备库B改成可读写状态,也就是把readonly设置为false;
  5. 把业务请求切到备库B。
    这个切换流程,一般是由专门的HA系统来完成的,我们暂时称之为可靠性优先流程
    这个流程中有不可用时间 步骤2-步骤5,其中步骤3最好时间。

可用性优先策略

如果我强行把步骤4、5调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库B,并且让备库B可以读写,那么系统几乎就没有不可用时间了。
我们把这个切换流程,暂时称作可用性优先流程。这个切换流程的代价,就是可能出现数据不一致的情况。
在可用性优先策略如果使用row格式的binlog时,数据不一致的问题更容易被发现。而使用mixed或者statement格式的binlog时,数据很可能悄悄地就不一致了。如果你过了很久才发现数据不一致的问题,很可能这时的数据不一致已经不可查,或者连带造成了更多的数据逻辑不一致。
所以主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。

分场景使用,有些场景下可用性优先策略更好,比如主库突然挂掉

并行复制

官方MySQL5.6版本,支持了并行复制,只是支持的粒度是按库并行。用于决定分发策略的hash表里,key就是数据库名

这个策略有两个优势:

  1. 构造hash值的时候很快,只需要库名;而且一个实例上DB数也不会很多
  2. 不要求binlog的格式。因为statement格式的binlog也可以很容易拿到库名。
    但是,如果你的主库上的表都放在同一个DB里面,这个策略就没有效果了;或者如果不同DB的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果。理论上你可以创建不同的DB,把相同热度的表均匀分到这些不同的DB中,强行使用这个策略。
    不过由于需要特地移动数据,这个策略用得并不多。

MariaDB的并行复制策略

MariaDB的并行复制策略利用的redo log组提交(group commit)优化

  1. 能够在同一组里提交的事务,一定不会修改同一行;
  2. 主库上可以并行执行的事务,备库上也一定是可以并行执行的。
    在实现上,MariaDB是这么做的:
  3. 在一组里面一起提交的事务,有一个相同的commit_id,下一组就是commit_id+1;
  4. commit_id直接写到binlog里面;
  5. 传到备库应用的时候,相同commit_id的事务分发到多个worker执行;
  6. 这一组全部执行完成后,coordinator再去取下一批。
    当时,这个策略出来的时候是相当惊艳的。因为,之前业界的思路都是在“分析binlog,并拆分到worker”上。而MariaDB的这个策略,目标是“模拟主库的并行模式”。
    但是,这个策略有一个问题,它并没有实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在commit的时候,下一组事务是同时处于“执行中”状态的。


    主库.png

    从库并行复制.png

    这个方案很容易被大事务拖后腿。假设trx2是一个超大事务,那么在备库应用的时候,trx1和trx3执行完成后,就只能等trx2完全执行完成,下一组才能开始执行。这段时间,只有一个worker线程在工作,是对资源的浪费。不过即使如此,这个策略仍然是一个很漂亮的创新。因为,它对原系统的改造非常少,实现也很优雅。

MySQL 5.7的并行复制策略

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

在MariaDB并行复制实现之后,官方的MySQL5.7版本也提供了类似的功能,由参数slaveparallel-type来控制并行复制策略:

  1. 配置为DATABASE,表示使用MySQL 5.6版本的按库并行策略;
  2. 配置为 LOGICAL_CLOCK,表示的就是类似MariaDB的策略。不过,MySQL 5.7这个策略,针对并行度做了优化。
    其实,不用等到commit阶段,只要能够到达redo log prepare阶段,就表示事务已经通过锁冲突的检验了


    image.png

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

  1. 同时处于prepare状态的事务,在备库执行时是可以并行的;
  2. 处于prepare状态的事务,与处于commit状态的事务之间,在备库执行时也是可以并行的。在binlog的组提交的时候有两个参数:
  3. binlog_group_commit_sync_delay参数,表示延迟多少微秒后才调用fsync;
  4. binlog_group_commit_sync_no_delay_count参数,表示累积多少次以后才调用fsync。这两个参数是用于故意拉长binlog从write到fsync的时间,以此减少binlog的写盘次数。在MySQL
    5.7的并行复制策略里,它们可以用来制造更多的“同时处于prepare阶段的事务”。这样就增加了备库复制的并行度。也就是说,这两个参数,既可以“故意”让主库提交得慢些,又可以让备库执行得快些。在MySQL
    5.7处理备库延迟的时候,可以考虑调整这两个参数值,来达到提升备库复制并发度的目的。
MySQL 5.7.22的并行复制策略

在2018年4月份发布的MySQL 5.7.22版本里,MySQL增加了一个新的并行复制策略,基于WRITESET的并行复制。
相应地,新增了一个参数binlog-transaction-dependency-tracking,用来控制是否启用这个新策略。这个参数的可选值有以下三种。

  1. COMMIT_ORDER,表示的就是前面介绍的,根据同时进入prepare和commit来判断是否可以并行的策略。
  2. WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的hash值,组成集合writeset。如果两个事务没有操作相同的行,也就是说它们的writeset没有交集,就可以并行。
  3. WRITESET_SESSION,是在WRITESET的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。当然为了唯一标识,这个hash值是通过“库名+表名+索引名+值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert语句对应的writeset就要多增加一个hash值。

MySQL官方的这个实现还是有很大的优势:

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

你可能感兴趣的:(MYSQL主从一致)