《MySQL实战45讲》实践篇 24-29 学习笔记 (主备篇)

图片来自于极客时间,如有版权问题,请联系我删除。
在这里插入图片描述

24 | MySQL是怎么保证主备一致的?

主备切换流程如下:
在这里插入图片描述
建议把节点 B(也就是备库)设置成只读(readonly)模式。
1.有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;
2.防止切换逻辑有 bug,比如切换过程中出现双写,造成主备不一致;
3.可以用 readonly 状态,来判断节点的角色

readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新数据的线程,就拥有超级权限。(所以一个readonly的从库还是可以同步主库的数据的)

主从同步的流程:
在这里插入图片描述
一个事务日志同步的完整过程是这样的:

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及文件名和日志偏移量。
  2. 在备库 B 上执行 start slave 命令,启动两个线程, io_thread 和 sql_thread
  3. 主库 A 校验完用户名、密码后,按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  4. 备库 B 拿到 binlog 后,写到本地文件–中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

binlog 的三种格式对比

当 binlog_format=statement 时,binlog 里面记录的就是 SQL 语句的原文
但是在statement,如果一个更新SQL使用了limit,在反复执行的时候可能实际用到的索引并不一样,会导致结果可能出现不一致

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

mysql会产生一个warning
在这里插入图片描述
binlog_format=row, row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event:Table_map 和 Delete_rows(这里是针对delete语句)。
Table_map event,用于说明接下来要操作的表是 test 库的表 t;
Delete_rows event,用于定义删除的行为

当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除 id=4 的行,不会有主备删除不同行的问题

为什么会有 mixed 这种 binlog 格式的存在场景?

为了避免主从不同步的同时,节省存储空间和IO资源浪费
mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。

在越来越多的场景要求把 MySQL 的 binlog 格式设置成 row。这么做的理由有很多,我来给你举一个可以直接看出来的好处:恢复数据 (statement的格式用来恢复数据比较困难,因为不会记录到底删除了什么数据)

有人 mysqlbinlog 解析出日志,然后把里面的 statement 语句直接拷贝出来执行 这是不提倡的
因为有些语句的执行结果是依赖于上下文命令的,直接执行的结果很可能是错误的。
比如:
insert into t values(10,10, now());
在这里插入图片描述
它用 SET TIMESTAMP命令约定了接下来的now()函数的返回时间,这样如果这个语句在从库上是什么时候执行的,其插入的数据和主库都是一致的

所以,用 binlog 来恢复数据的标准做法是,用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行
类似如下:

mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;

主库 A 从本地读取 binlog,发给从库 B;
老师,请问这里的本地是指文件系统的 page cache还是disk呢?
作者回复: 好问题,
是这样的,对于A的线程来说,就是“读文件”,

  1. 如果这个文件现在还在 page cache中,那就最好了,直接读走;
  2. 如果不在page cache里,就只好去磁盘读
    这个行为是文件系统控制的,MySQL只是执行“读文件”这个操作

双主架构下的循环复制问题

节点 A 上更新了一条语句,然后再把生成的 binlog 发给节点 B,节点 B 执行完这条更新语句后也会生成 binlog。(我建议你把参数 log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog)。

那么,如果节点 A 同时是节点 B 的备库,相当于节点A又把节点 B 新生成的 binlog 拿过来执行了一次,就会出现循环复制的问题了

解决思路是利用MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的 server id

具体流程:一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog;而每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志

课后问题:我们说 MySQL 通过判断 server id 的方式,断掉死循环。但是,这个机制其实并不完备,在某些场景下,还是有可能出现死循环。

一种场景是,在一个主库更新事务后,用命令 set global server_id=x 修改了 server_id。等日志再传回来的时候,发现 server_id 跟自己的 server_id 不同,就只能执行了。

另一种场景是,有三个节点的时候,trx1 是在节点 B 执行的,因此 binlog 上的 server_id 就是 B,binlog 传给节点 A,然后 A 和 A’搭建了双 M 结构,就会出现循环复制。
在这里插入图片描述

这种三节点复制的场景,做数据库迁移的时候会出现。如果出现了循环复制,可以在 A 或者 A’上,执行如下命令:

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=(server_id_of_B);
start slave;

这样这个节点收到日志后就不会再执行。过一段时间后,再执行下面的命令把这个值改回来。

stop slave;
CHANGE MASTER TO IGNORE_SERVER_IDS=();
start slave;

25 | MySQL是怎么保证高可用的?(主备延迟)

与数据同步有关的时间点主要包括以下三个:
1.主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
2.之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
3.备库 B 执行完成这个事务,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1
在网络正常的时候,日志从主库传给备库所需的时间是很短的,即 T2-T1 的值是非常小的。也就是说,网络正常情况下,主备延迟的主要来源是备库接收完 binlog 和执行完这个事务之间的时间差。
所以说,主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。

主备延迟的来源

首先,有些部署条件下,备库所在机器的性能要比主库所在的机器性能差

做这种部署时,一般都会将备库设置为“非双 1”的模式。

第二种常见的可能了,即备库的压力大
将大量查询工作交给了备库
我们一般可以这么处理:

  1. 一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。2.通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的能力

这就是第三种可能了,即大事务
主库上必须等事务执行完成才会写入 binlog,再传给备库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。

不要一次性地用 delete 语句删除太多数据 其实,这就是一个典型的大事务场景

另一种典型的大事务场景,就是大表 DDL。这个场景,我在前面的文章中介绍过。处理方案就是,计划内的 DDL,建议使用 gh-ost 方案(这里,你可以再回顾下第 13 篇文章《为什么表数据删掉一半,表文件大小不变?》中的相关内容)。

*备库的并行复制能力。

在主备切换的时候,有相应的不同策略*

可靠性优先策略

在双 M 结构下,从状态 1 到状态 2 切换的详细过程是这样的:
1.判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步;
2.把主库 A 改成只读状态,即把 readonly 设置为 true;
3.判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;4.把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
5.把业务请求切到备库 B

在切换过程中,从步骤2-5期间,MySQL集群都不可用的

可用性优先策略

如果强行把步骤 4、5 调整到最开始执行,也就是说不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统几乎就没有不可用时间了。

这个切换流程的代价,就是可能出现数据不一致的情况。
在这里插入图片描述

如果使用可用性优先策略,建议设置 binlog_format=row,情况又会怎样呢?因为 row 格式在记录 binlog 的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致。而且,两边的主备同步的应用线程会报错 duplicate key error 并停止。
也就是说,这种情况下,备库 B 的 (5,4) 和主库 A 的 (5,5) 这两行数据,都不会被对方执行。

主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,我都建议你使用可靠性优先策略(如果一定要使用可用性优先策略,建议选择binlog_format=row))。毕竟对数据服务来说的话,数据的可靠性一般还是要优于可用性的。

有没有哪种情况数据的可用性优先级更高呢?
答案是,有的。
有一个库的作用是记录操作日志。这时候,如果数据不一致可以通过 binlog 来修补,而这个短暂的不一致也不会引发业务问题。同时,业务系统依赖于这个日志写入逻辑,如果这个库不可写,会导致线上的业务操作无法执行。

改进措施就是,让业务逻辑不要依赖于这类日志的写入。也就是说,日志写入这个逻辑模块应该可以降级,比如写到本地文件,或者写到另外一个临时库里面。

按照可靠性优先的思路,异常切换会是什么效果?

假设,主库 A 和备库 B 间的主备延迟是 30 分钟,这时候主库 A 掉电了,HA 系统要切换 B 作为主库。我们在主动切换的时候,可以等到主备延迟小于 5 秒的时候再启动切换,但这时候已经别无选择了。

采用可靠性优先策略的话,你就必须得等到备库 B 的 seconds_behind_master=0 之后,才能切换。但现在系统处于完全不可用的状态(主库 A 掉电后,我们的连接还没有切到备库 B)

"我"认为这种情形下应该也只能使用可用性优先策略了,但是由于主备延迟,导致客户端查询可能看不到之前执行完成的事务,会认为有“数据丢失”。但是对于一些业务来说,查询到“暂时丢失数据的状态”也是不能被接受的。(比方说订单的情形)

在满足数据可靠性的前提下,MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。

在实际的应用中,我更建议使用可靠性优先的策略。毕竟保证数据准确,应该是数据库服务的底线。在这个基础上,通过减少主备延迟,提升系统的可用性。

课后问题:
一般现在的数据库运维系统都有备库延迟监控,其实就是在备库上执行 show slave status,采集seconds_behind_master的值。

假设,现在你看到你维护的一个备库,它的延迟监控的图像类似图6,是一个45°斜向上的线段,你觉得可能是什么原因导致呢?你又会怎么去确认这个原因呢?

在这里插入图片描述
备库的同步在这段时间完全被堵住
生这种现象典型的场景主要包括两种:
1.一种是大事务(包括大表 DDL、一个事务操作很多行);
2.还有一种情况比较隐蔽,就是备库起了一个长事务,比如

begin;
select * from t limit 1;

然后就不动了

这时候主库对表 t 做了一个加字段操作,即使这个表很小,这个 DDL 在备库应用的时候也会被堵住,也能看到这个现象

26 | 备库为什么会延迟好几个小时?

如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能不断增加,然后变成了小时级别

在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出现严重的主备延迟问题。

多线程复制

在这里插入图片描述
1.事务能不能按照轮询的方式分发给各个 worker?
不行的。因为,事务被分发给 worker 以后,不同的 worker 就独立执行了。但是,由于 CPU 的调度策略,很可能第二个事务最终比第一个事务先执行。而如果这时候刚好这两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序相反,会导致主备不一致的问题。

2.同一个事务的多个更新语句,能不能分给不同的 worker 来执行呢?
也不行。一个事务更新了表 t1 和表 t2 中的各一行,如果这两条更新语句被分到不同 worker 的话,虽然最终的结果是主备一致的,但如果表 t1 执行完成的瞬间,备库上有一个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的隔离性。

coordinator 在分发的时候,需要满足以下这两个基本要求:
1.不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中。
2.同一个事务不能被拆开,必须放到同一个 worker 中。

MySQL 5.5 版本的并行复制策略

自己实现的两个并行复制策略

按表分发策略

每个 worker 线程对应一个 hash 表,用于保存当前正在这个 worker 的“执行队列”里的事务所涉及的表。hash 表的 key 是“库名. 表名”,value 是一个数字,表示队列中有多少个事务修改这个表。
新的事务T:
1.当前worker中都没有操作事务T涉及到的表,那选择最空闲的一个
2.有>1的worker在操作事务T涉及到的表,则等到只剩一个
3.只有一个worker在操作事务T涉及到的表,就分给该worker

缺点:如果碰到热点表,就变成类似单线程复制了

按行分发策略

按行复制的核心思路是:如果两个事务没有更新相同的行,它们在备库上可以并行执行。要求 binlog 格式必须是 row。且必须有主键,不能有外键(表如果有外键,级联更新的行就不会记录在binlog中)

每个 worker,分配一个 hash 表。只是要实现按行分发,这时候的 key,就必须是“库名 + 表名 + 唯一键的值”。(这里的唯一键不单单指主键,还需要考虑唯一索引), key 应该是“库名 + 表名 + 索引 a 的名字 +a 的值”

eg.在表 t1 上执行 update t1 set a=1 where id=2 语句(id为主键,a为唯一索引)
这个事务的 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 的行。(a=2为修改前的值)
3.key=hash_func(db1+t1+“a”+1), value=1,表示会影响到这个表 a=1 的行。

按行分发策略的并行度更高。 但对于操作很多行的大事务的话,按行分发的策略有两个问题:
1.耗费内存。比如一个语句要删除 100 万行数据,这时候 hash 表就要记录 100 万个项。
2.耗费 CPU。解析 binlog,然后计算 hash 值,对于大事务,这个成本还是很高的

所以实现按行分发策略的时候会设置一个阈值,单个事务如果超过设置的行数阈值(比如超过 10 万行),就暂时退化为单线程模式
1.coordinator 暂时先 hold 住这个事务;
2.等待所有 worker 都执行完成,变成空队列;
3.coordinator 直接执行这个事务;
4.恢复并行模式

MySQL 5.6 版本的并行复制策略

官方 MySQL5.6 版本,支持了并行复制,只是支持的粒度是按库并行
优点:
1.构造 hash 值快,只需要库名;一个实例上 DB 数不多,不会出现需要构造 100 万个项这种情况。
2.不要求 binlog 的格式
但是对於单库或者DB热点不同,其效率就不是很好了(当然可以做分库)

MariaDB 的并行复制策略

redo log 组提交 (group commit) 优化, 而 MariaDB 的并行复制策略利用的就是这个特性:
1.能够在同一组里提交的事务,一定不会修改同一行;
2.主库上可以并行执行的事务,备库上也一定是可以并行执行的。

流程:

1.在一组里面一起提交的事务,有一个相同的 commit_id(递增就行)
2.commit_id 直接写到 binlog 里面;
3.传到备库应用的时候,相同 commit_id 的事务分发到多个 worker 执行;
4.这一组全部执行完成后,coordinator 再去取下一批。

不足:
1.并没有彻底实现“真正的模拟主库并发度”这个目标。在主库上,一组事务在 commit 的时候,下一组事务是同时处于“执行中”状态的。而这里不行
2.如果有大事务,就会由于当前事务没有commit导致下一个组事务还不能开始执行

开启并行复制后,事务是按照组来提交的,从库也是根据commit_id来回放,如果从库也开启binlog的话,那是不是存在主从的binlog event写入顺序不一致的情况呢?

作者回复: 是有可能binlog event写入顺序不同的,好问题

MySQL 5.7 的并行复制策略

参数 : slave-parallel-type 参考了MariaDB 的并行复制策略思想

1.配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
2.配置为 LOGICAL_CLOCK,表示的就是类似 MariaDB 的策略。不过,MySQL 5.7 这个策略,针对并行度做了优化。

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

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

MySQL 5.7 并行复制策略的思想是:
1.同时处于 prepare 状态的事务,在备库执行时是可以并行的;
2.处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并行的。

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

这两个参数是用于故意拉长 binlog 从 write 到 fsync 的时间,以此减少 binlog 的写盘次数。在 MySQL 5.7 的并行复制策略里,它们可以用来制造更多的“同时处于 prepare 阶段的事务”

这两个参数,既可以“故意”让主库提交得慢些,又可以让备库执行得快些。在 MySQL 5.7 处理备库延迟的时候,可以考虑调整这两个参数值,来达到提升备库复制并发度的目的。

MySQL 5.7.22 的并行复制策略

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 值。

最大的优势(比起按行分发策略):
writeset是在主库生成后并直接写入binlog的,对binlog的格式也没有要求(因为备库分发依赖于主库生成的writeset而不是binlog内容)

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

课后问题:
假设一个MySQL 5.7.22版本的主库,单线程插入了很多数据,过了3个小时后,我们要给这个主库搭建一个相同版本的备库。
这时候,你为了更快地让备库追上主库,要开并行复制。在binlog-transaction-dependency-tracking参数的COMMIT_ORDER、WRITESET和WRITE_SESSION这三个取值中,你会选择哪一个呢?
你选择的原因是什么?如果设置另外两个参数,你认为会出现什么现象呢?

应该将这个参数设置为 WRITESET。
由于主库是单线程压力模式,所以每个事务的 commit_id 都不同,那么设置为 COMMIT_ORDER 模式的话,从库也只能单线程执行。
同样地,由于 WRITESET_SESSION 模式要求在备库应用日志的时候,同一个线程的日志必须与主库上执行的先后顺序相同,也会导致主库单线程压力模式下退化成单线程复制。
所以,应该将 binlog-transaction-dependency-tracking 设置为 WRITESET。

27 | 主库出问题了,从库怎么办?-- 主备切换

一主多从架构下,主库发生故障后
在这里插入图片描述
A’会成为新的主库,从库B、C、D也要改接到A’。

基于位点的主备切换

切换主库时,从库需要执行change master命令

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
MASTER_LOG_FILE=$master_log_name 
MASTER_LOG_POS=$master_log_pos

MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步,也就是同步位点

一种取同步位点的方法是这样的:

  1. 等待新主库 A’把中转日志(relay log)全部同步完成;
  2. 在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
  3. 取原主库 A 故障的时刻 T;
  4. 用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。

因为同步位点并不是精确的,可能会出现bin log重复执行的情形,出现主键冲突等错误。通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。
1.主动跳过一个事务。

set global sql_slave_skip_counter=1;
start slave;

sql_slave_skip_counter 跳过的是一个 event,由于 MySQL 总不能执行一半的事务,所以既然跳过了一个 event,就会跳到这个事务的末尾,因此 set global sql_slave_skip_counter=1;start slave 是可以跳过整个事务的。

2.通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。
在执行主备切换时,有这么两类错误,是经常会遇到的:
1.1062 错误是插入数据时唯一键冲突;
2.1032 错误是删除数据时找不到行。

GTID

GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。
它由两部分组成,GTID=server_uuid:gno

server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务(回滚的不会算入),并加 1

GTID 模式的启动: 启动一个 MySQL 实例的时候,加上参数 gtid_mode=on 和 enforce_gtid_consistency=on

这个 GTID 有两种生成方式,而使用哪种方式取决于 session 变量 gtid_next 的值。
1.如果 gtid_next=automatic,代表使用默认值。
这时,MySQL 就会把 server_uuid:gno 分配给这个事务。
a. 记录 binlog 的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’;
b. 把这个 GTID 加入本实例的 GTID 集合。
2.如果 gtid_next 是一个指定的 GTID 的值,比如通过 set gtid_next='current_gtid’指定为 current_gtid,那么就有两种可能:
a. 如果 current_gtid 已经存在于实例的 GTID 集合中,接下来执行的这个事务会直接被系统忽略;
b. 如果 current_gtid 没有存在于实例的 GTID 集合中,就将这个 current_gtid 分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的 GTID,因此 gno 也不用加 1。

一个 current_gtid 只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要执行 set 命令,把 gtid_next 设置成另外一个 gtid 或者 automatic

使用GTID的话,如果insert sql是重复的,可以通过把对应的GTID添加到从库的GTID的集合中来避免出现重复key的异常

GTID 的主备切换

GTID模式下的切换命令

CHANGE MASTER TO 
MASTER_HOST=$host_name 
MASTER_PORT=$port 
MASTER_USER=$user_name 
MASTER_PASSWORD=$password 
master_auto_position=1 

实例 A’的 GTID 集合记为 set_a,实例 B 的 GTID 集合记为 set_b。
主备切换的时候,B先将set_b发送给A’,计算出set_a和set_b之间的差集 (因为A’和B都是原来A的从库,bin log执行的速度可能不一样,所以两个实例的GTID集合不一定是一致的)
判断 A’本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;

之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。

隐含条件:在基于GTID的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例B需要的日志已经不存在,A’就拒绝把日志发给B。

GTID 和在线 DDL

业务高峰期的慢查询性能问题时,分析到如果是由于索引缺失引起的性能问题,我们可以通过在线加索引来解决。但是,考虑到要避免新增索引对主库性能造成的影响,我们可以先在备库加索引,然后再切换。

当时我说,在双 M 结构下,备库执行的 DDL 语句也会传给主库,为了避免传回后对主库造成影响,要通过 set sql_log_bin=off 关掉 binlog

评论区有位同学提出了一个问题:这样操作的话,数据库里面是加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?

假设,这两个互为主备关系的库还是实例 X 和实例 Y,且当前主库是 X,并且都打开了 GTID 模式。
这时的主备切换流程可以变成下面这样:
1.在实例 X 上执行 stop slave。
2.在实例 Y 上执行 DDL 语句。(不需要关闭 binlog)。执行完成后,查出这个 DDL 语句对应的 GTID,并记为 server_uuid_of_Y:gno。
3.到实例 X 上执行以下语句序列:

set GTID_NEXT=“server_uuid_of_Y:gno”;
begin;
commit;
set gtid_next=automatic;
start slave;

接下来,执行完主备切换,然后照着上述流程再执行一遍即可。
通过GTID,让实例 Y 的更新有 binlog 记录,同时也可以确保不会在实例 X (主库)上执行这条更新。

课后问题: 在 GTID 模式下,如果一个新的从库接上主库,但是需要的 binlog 已经没了,要怎么做?

1.如果业务允许主从不一致的情况,那么可以在主库上先执行 show global variables like ‘gtid_purged’,得到主库已经删除的 GTID 集合,假设是 gtid_purged1;然后先在从库上执行 reset master,再执行 set global gtid_purged =‘gtid_purged1’;最后执行 start slave,就会从主库现存的 binlog 开始同步。binlog 缺失的那一部分,数据在从库上就可能会有丢失,造成主从不一致。

2.如果需要主从数据一致的话,最好还是通过重新搭建从库来做。

3.如果有其他的从库保留有全量的 binlog 的话,可以把新的从库先接到这个保留了全量 binlog 的从库,追上日志以后,如果有需要,再接回主库。
4.如果 binlog 有备份的情况,可以先在从库上应用缺失的 binlog,然后再执行 start slave。

28 | 读写分离有哪些坑?

客户端直连方案,
因为少了一层 proxy 转发,所以查询性能稍微好一点儿,并且整体架构简单,排查问题更方便。但在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。一般采用这样的架构,一定会伴随一个负责管理后端的组件,比如 Zookeeper,尽量让业务端只专注于业务逻辑开发。
带 proxy 的架构,对客户端比较友好
客户端不需要关注后端细节,连接维护、后端信息维护等工作,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。而且,proxy 也需要有高可用架构。因此,带 proxy 架构的整体就相对比较复杂

由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。
这种“在从库上会读到系统的一个过期状态”的现象,在这篇文章里,我们暂且称之为“过期读”

强制走主库方案

将查询请求做分类,需要实时读取到最新的数据的走主库,可以接受延迟的走从库
但是对于要求所有查询都不能是过期读的,就没法进行读写分离了

Sleep 方案

主库更新后,读从库之前先 sleep 一下。具体的方案就是,类似于执行一条 select sleep(1) 命令。
这个方案存在的问题就是不精确。
这个不精确包含了两层意思:
1.如果这个查询请求本来 0.5 秒就可以在从库上拿到正确结果,也会等 1 秒;
2.如果延迟超过 1 秒,还是会出现过期读

判断主备无延迟方案

show slave status 结果里的 seconds_behind_master 参数的值,可以用来衡量主备延迟时间的长短。

第一种确保主备无延迟的方法是,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求

show slave status结果
在这里插入图片描述
第二种方法,对比位点确保主备无延迟:

Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点;
Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点
这两组值完全相同,就表示接收到的日志已经同步完成

第三种方法,对比 GTID 集合确保主备无延迟:

  • Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。
  • Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;
  • Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。

如果这两个集合相同,也表示备库接收到的日志都已经同步完成

但还是有一个问题,上面判断主备无延迟的逻辑,是“备库收到的日志都执行完成了”。但是,从 binlog 在主备之间状态的分析中,不难看出还有一部分日志,处于客户端已经收到提交确认,而备库还没收到日志的状态。也就是在从主库向从库传递binlog的过程中还会存在一部分延迟的数据

配合 semi-sync

半同步复制
semi-sync 做了这样的设计:

  1. 事务提交的时候,主库把 binlog 发给从库;
  2. 从库收到 binlog 以后,发回给主库一个 ack,表示收到了;
  3. 主库主要收到这个 ack(任何一个从库) 以后,才能给客户端返回“事务完成”的确认

semi-sync 配合前面关于位点的判断,就能够确定在从库上执行的查询请求,可以避免过期读。
但是,semi-sync+ 位点判断的方案,只对一主一备的场景是成立的。在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认

判断同步位点的方案还有另外一个潜在的问题,即:如果在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,那么上面的两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况

当发起一个查询请求以后,我们要得到准确的结果,其实并不需要等到“主备完全同步”。 只需要当前查询的结果在从库已经同步就行了

semi-sync 配合判断主备无延迟的方案,存在两个问题:
1.一主多从的时候,在某些从库执行查询请求会存在过期读的现象;
2.在持续延迟的情况下,可能出现过度等待的问题。

等主库位点方案

select master_pos_wait(file, pos[, timeout]);

它是在从库执行的;
参数 file 和 pos 指的是主库上的文件名和位置;
timeout 可选,设置为正整数 N 表示这个函数最多等待 N 秒。

这个命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务。

如果执行期间,备库同步线程发生异常,则返回 NULL;
如果等待超过 N 秒,就返回 -1;
如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0

采用下面的流程就可以避免出现上述主备无延迟配合半同步复制情形下的问题:

  1. trx1 事务更新完成后,马上执行 show master status 得到当前主库执行到的 File 和 Position;
  2. 选定一个从库执行查询语句,在从库上执行 select master_pos_wait(File, Position, 1);
  3. 如果返回值是 >=0 的正整数,则在这个从库执行查询语句;否则,到主库执行查询语句
等待GTID 方案

前提是开启了GTID模式

select wait_for_executed_gtid_set(gtid_set, 1);

这条命令的逻辑是:等待,直到这个库执行的事务中包含传入的 gtid_set,返回 0;超时返回 1。

MySQL 5.7.6 版本开始,允许在执行完更新类事务后,把这个事务的 GTID 返回给客户端(将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值),这样等 GTID 的方案就可以减少一次查询。
https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html

具体流程和等主库位点的类似

课后问题: 如果使用 GTID 等位点的方案做读写分离,在对大表做 DDL 的时候会怎么样。

假设,这条语句在主库上要执行 10 分钟,提交后传到备库就要 10 分钟(这里的10min就是指DDL在主库的执行时间,典型的大事务)。那么,在主库 DDL 之后再提交的事务的 GTID,去备库查的时候,就会等 10 分钟才出现。

这样,这个读写分离机制在这 10 分钟之内都会超时,然后走主库。这种预期内的操作,应该在业务低峰期的时候,确保主库能够支持所有业务查询,然后把读请求都切到主库,再在主库上做 DDL。等备库延迟追上以后,再把读请求切回备库。

使用gh-ost方案来解决这个问题也是不错的选择

我的疑问:
那10min是等DDL执行完,并写完binlog可以发送到从库,期间其他更新SQL在主库不是也可以正常执行吗,那相关的操作不是也能写入binlog,为什么不能发送给从库执行啊??

今天的问题,大表做DDL的时候可能会出现主从延迟,导致等 GTID 的方案可能会导致这部分流量全打到主库,或者全部超时。如果这部分流量太大的话,我会选择上一篇文章介绍的两种方法:1.在各个从库先SET sql_log_bin = OFF,然后做DDL,所有从库及备主全做完之后,做主从切换,最后在原来的主库用同样的方式做DDL。2.从库上执行DDL;将从库上执行DDL产生的GTID在主库上利用生成一个空事务GTID的方式将这个GTID在主库上生成出来。各个从库做完之后再主从切换,然后再在原来的主库上同样做一次。需要注意的是如果有MM架构的情况下,承担写职责的主库上的slave需要先停掉。

29 | 如何判断一个数据库是不是出问题了?

select 1 判断

select 1 成功返回,只能说明这个库的进程还在,并不能说明主库没问题

set global innodb_thread_concurrency=3;
在这里插入图片描述
innodb_thread_concurrency 参数的目的是,控制 InnoDB 的并发线程上限。也就是说,一旦并发线程数达到这个值,InnoDB 在接收到新请求的时候,就会进入等待状态,直到有线程退出

通常情况下,我们建议把 innodb_thread_concurrency 设置为 64~128 之间的值–这主要是针对并发查询(在线程进入锁等待以后,并发线程的计数会减一,也就是说等行锁(也包括间隙锁)的线程是不算在并发线程数里面的。)

并发连接和并发查询,并不是同一个概念。
你在 show processlist 的结果里,看到的几千个连接,指的就是并发连接。而“当前正在执行”的语句,才是我们所说的并发查询

查表判断

一般的做法是,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行

mysql> select * from mysql.health_check;

但是针对binlog空间满了情形又不能检测出来

更新事务要写 binlog,而一旦 binlog 所在磁盘的空间占用率达到 100%,那么所有的更新语句和事务提交的 commit 语句就都会被堵住。但是,系统这时候还是可以正常读数据的

更新判断

mysql> update mysql.health_check set t_modified=now();

这样会有新的问题:主库要判断库是否可用,备库也需要,那么就都需要执行上述更新语句,而双M架构下主库的binlog又会发送给备库,更新同一行数据就可能出现行冲突,也就是可能会导致主备同步停止–需要多行(以server_id为主键)

但有可能,机器的I/O已经100%,但刚好健康检查的sql拿到了资源,成功返回了 – 出现了误判,判定延迟

内部统计

MySQL 5.6 版本以后提供的 performance_schema 库,就在 file_summary_by_event_name 表里统计了每次 IO 请求的时间。
在这里插入图片描述
图中这一行表示统计的是redo log的写入时间,第一列EVENT_NAME 表示统计的类型。
剩下的三组分表是所有IO操作的统计,读操作的统计以及写操作的统计
SUM_NUMBER_OF_BYTES_READ统计的是,总共从redo log里读了多少个字节。
最后的第四组数据,是对其他类型数据的统计。在redo log里,你可以认为它们就是对fsync的统计。

建议只打开自己需要的项进行统计。你可以通过下面的方法打开或者关闭某个具体项的统计。
打开redolog的时间监控

mysql> update setup_instruments set ENABLED=‘YES’, Timed=‘YES’ where name like ‘%wait/io/file/innodb/innodb_log_file%’;

可以通过 MAX_TIMER 的值来判断数据库是否出问题了。比如,设定阈值,单次 IO 请求时间超过 200 毫秒属于异常

mysql> select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in (‘wait/io/file/innodb/innodb_log_file’,‘wait/io/file/sql/binlog’) and MAX_TIMER_WAIT>200* 1000000000;

发现异常后,取到你需要的信息,再通过下面这条语句把之前的统计信息清空

mysql> truncate table performance_schema.file_summary_by_event_name;

我个人比较倾向的方案,是优先考虑 update 系统表,然后再配合增加检测 performance_schema 的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章