MySQL实战45讲学习笔记:第二十七讲

1、一主多从的切换正确性

在前面的第2四、25和26篇文章中,我和你介绍了 MySQL 主备复制的基础结构,但这些都是一主一备的结构。mysql

大多数的互联网应用场景都是读多写少,所以你负责的业务,在发展过程当中极可能先会遇到读性能的问题。而在数据库层解决读性能问题,就要涉及到接下来两篇文章要讨论的架
构:一主多从。sql

今天这篇文章,咱们就先聊聊一主多从的切换正确性。而后,咱们在下一篇文章中再聊聊解决一主多从的查询逻辑正确性的方法。数据库

一、一主多从基本结构

如图 1 所示,就是一个基本的一主多从结构。bash

图 1 一主多从基本结构session

图中,虚线箭头表示的是主备关系,也就是 A 和 A’互为主备, 从库 B、C、D 指向的是主库 A。一主多从的设置,通常用于读写分离,主库负责全部的写入和一部分读,其余的
读请求则由从库分担。架构

二、主备切换

今天咱们要讨论的就是,在一主多从架构下,主库故障后的主备切换问题。如图 2 所示,就是主库发生故障,主备切换后的结果。工具

图 2 一主多从基本结构 -- 主备切换性能

相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D 也要改接到 A’。正是因为多了从库 B、C、D 从新指向的这个过程,因此主备
切换的复杂性也相应增长了。ui

接下来,咱们再一块儿看看一个切换系统会怎么完成一主多从的主备切换过程。spa

2、基于位点的主备切换

这里,咱们须要先来回顾一个知识点。

当咱们把节点 B 设置成节点 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  

一、change master 命令参数详解

这条命令有这么 6 个参数:

MASTER_HOST、MASTER_PORT、MASTER_USER 和 MASTER_PASSWORD 四个参数,分别表明了主库 A’的 IP、端口、用户名和密码。

最后两个参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要从主库的master_log_name 文件的 master_log_pos 这个位置的日志继续同步。而这个位置就是咱们所说的同步位点,也就是主库对应的文件名和日志偏移量。

那么,这里就有一个问题了,节点 B 要设置成 A’的从库,就要执行 change master 命令,就不可避免地要设置位点的这两个参数,可是这两个参数到底应该怎么设置呢?

原来节点 B 是 A 的从库,本地记录的也是 A 的位点。可是相同的日志,A 的位点和A’的位点是不一样的。所以,从库 B 要切换的时候,就须要先通过“找同步位点”这个逻辑。

二、这个位点很难精确取到,只能取一个大概位置。为何这么说呢?

我来和你分析一下看看这个位点通常是怎么获取到的,你就清楚其中不精确的缘由了。考虑到切换过程当中不能丢数据,因此咱们找位点的时候,老是要找一个“稍微往前”的,

而后再经过判断跳过那些在从库 B 上已经执行过的事务。

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

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

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

mysqlbinlog File --stop-datetime=T --start-datetime=T

图 3 mysqlbinlog 部分输出结果

图中,end_log_pos 后面的值“123”,表示的就是 A’这个实例,在 T 时刻写入新的binlog 的位置。而后,咱们就能够把 123 这个值做为 $master_log_pos ,用在节点 B
的 change master 命令里。

二、这样方法取出值并不精确。为何呢?

你能够设想有这么一种状况,假设在 T 这个时刻,主库 A 已经执行完成了一个 insert 语句插入了一行数据 R,而且已经将 binlog 传给了 A’和 B,而后在传完的瞬间主库 A 的
主机就掉电了。

那么,这时候系统的状态是这样的:

1. 在从库 B 上,因为同步了 binlog, R 这一行已经存在;
2. 在新主库 A’上, R 这一行也已经存在,日志是写在 123 这个位置以后的;
3. 咱们在从库 B 上执行 change master 命令,指向 A’的 File 文件的 123 位置,就会把插入 R 这一行数据的 binlog 又同步到从库 B 去执行。

这时候,从库 B 的同步线程就会报告 Duplicate entry ‘id_of_R’ for key‘PRIMARY’ 错误,提示出现了主键冲突,而后中止同步。

三、一般状况下,咱们在切换任务的时候,要先主动跳过这些错误,有两种经常使用的方法。

一、一种作法是,主动跳过一个事务。跳过命令的写法是:

set global sql_slave_skip_counter=1;
start slave;

由于切换过程当中,可能会不止重复执行一个事务,因此咱们须要在从库 B 刚开始接到新主库 A’时,持续观察,每次碰到这些错误就停下来,执行一次跳过命令,直到再也不出现停
下来的状况,以此来跳过可能涉及的全部事务。

二、另一种方式是,经过设置 slave_skip_errors 参数,直接设置跳过指定的错误。

另一种方式是,经过设置 slave_skip_errors 参数,直接设置跳过指定的错误。在执行主备切换时,有这么两类错误,是常常会遇到的:

1062 错误是插入数据时惟一键冲突;
1032 错误是删除数据时找不到行。

所以,咱们能够把 slave_skip_errors 设置为 “1032,1062”,这样中间碰到这两个错误时就直接跳过。

这里须要注意的是,这种直接跳过指定错误的方法,针对的是主备切换时,因为找不到精确的同步位点,因此只能采用这种方法来建立从库和新主库的主备关系。

这个背景是,咱们很清楚在主备切换过程当中,直接跳过 1032 和 1062 这两类错误是无损的,因此才能够这么设置 slave_skip_errors 参数。等到主备间的同步关系创建完成,并

稳定执行一段时间以后,咱们还须要把这个参数设置为空,以避免以后真的出现了主从数据不一致,也跳过了。

3、GTID

经过 sql_slave_skip_counter 跳过事务和经过 slave_skip_errors 忽略错误的方法,虽然都最终能够创建从库 B 和新主库 A’的主备关系,但这两种操做都很复杂,并且容易出
错。因此,MySQL 5.6 版本引入了 GTID,完全解决了这个困难。

那么,GTID 究竟是什么意思,又是如何解决找同步位点这个问题呢?如今,我就和你简单介绍一下。

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

GTID=server_uuid:gno

一、GTID 究竟是什么意思?

其中:

server_uuid 是一个实例第一次启动时自动生成的,是一个全局惟一的值;gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。

这里我须要和你说明一下,在 MySQL 的官方文档里,GTID 格式是这么定义的:

GTID=source_id:transaction_id

这里的 source_id 就是 server_uuid;然后面的这个 transaction_id,我以为容易形成误导,因此我改为了 gno。为何说使用 transaction_id 容易形成误解呢?

由于,在 MySQL 里面咱们说 transaction_id 就是指事务 id,事务 id 是在事务执行过程当中分配的,若是这个事务回滚了,事务 id 也会递增,而 gno 是在事务提交的时候才会分配。

从效果上看,GTID 每每是连续的,所以咱们用 gno 来表示更容易理解。

GTID 模式的启动也很简单,咱们只须要在启动一个 MySQL 实例的时候,加上参数gtid_mode=on 和 enforce_gtid_consistency=on 就能够了。

二、如何解决找同步位点这个问题呢?

在 GTID 模式下,每一个事务都会跟一个 GTID 一一对应。这个 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。

这样,每一个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的全部事务”。

这样看上去不太容易理解,接下来我就用一个简单的例子,来和你说明 GTID 的基本用法。

咱们在实例 X 中建立一个表 t。

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

insert into t values(1,1);

 图 4 初始化数据的 binlog

能够看到,事务的 BEGIN 以前有一条 SET @@SESSION.GTID_NEXT 命令。这时,若是实例 X 有从库,那么将 CREATE TABLE 和 insert 语句的 binlog 同步过去执行的话,执

行事务以前就会先执行这两个 SET 命令, 这样被加入从库的 GTID 集合的,就是图中的这两个 GTID。

假设,如今这个实例 X 是另一个实例 Y 的从库,而且此时在实例 Y 上执行了下面这条插入语句:

insert into t values(1,1);

而且,这条语句在实例 Y 上的 GTID 是 “aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”。

那么,实例 X 做为 Y 的从库,就要同步这个事务过来执行,显然会出现主键冲突,致使实例 X 的同步线程中止。这时,咱们应该怎么处理呢?

处理方法就是,你能够执行下面的这个语句序列:

set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
begin;
commit;
set gtid_next=automatic;
start slave;

其中,前三条语句的做用,是经过提交一个空事务,把这个 GTID 加到实例 X 的 GTID 集合中。如图 5 所示,就是执行完这个空事务以后的 show master status 的结果。

图 5 show master status 结果

能够看到实例 X 的 Executed_Gtid_set 里面,已经加入了这个 GTID。

这样,我再执行 start slave 命令让同步线程执行起来的时候,虽然实例 X 上仍是会继续执行实例 Y 传过来的事务,可是因为“aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”已经存

在于实例 X 的 GTID 集合中了,因此实例 X 就会直接跳过这个事务,也就不会再出现主键冲突的错误。

在上面的这个语句序列中,start slave 命令以前还有一句 set gtid_next=automatic。这句话的做用是“恢复 GTID 的默认分配行为”,也就是说若是以后有新的事务再执行,就
仍是按照原来的分配方式,继续分配 gno=3

4、基于 GTID 的主备切换

如今,咱们已经理解 GTID 的概念,再一块儿来看看基于 GTID 的主备复制的用法。在 GTID 模式下,备库 B 要设置为新主库 A’的从库的语法以下:

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

其中,master_auto_position=1 就表示这个主备关系使用的是 GTID 协议。能够看到,

前面让咱们头疼不已的 MASTER_LOG_FILE 和 MASTER_LOG_POS 参数,已经不须要指定了。

咱们把如今这个时刻,实例 A’的 GTID 集合记为 set_a,实例 B 的 GTID 集合记为set_b。接下来,咱们就看看如今的主备切换逻辑。

一、咱们在实例 B 上执行 start slave 命令,取 binlog 的逻辑是这样的:

1. 实例 B 指定主库 A’,基于主备协议创建链接。
2. 实例 B 把 set_b 发给主库 A’。
3. 实例 A’算出 set_a 与 set_b 的差集,也就是全部存在于 set_a,可是不存在于 set_b的 GITD 的集合,判断 A’本地是否包含了这个差集须要的全部 binlog 事务。

a. 若是不包含,表示 A’已经把实例 B 须要的 binlog 给删掉了,直接返回错误;
b. 若是确认所有包含,A’从本身的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;

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

其实,这个逻辑里面包含了一个设计思想:在基于 GTID 的主备关系里,系统认为只要创建主备关系,就必须保证主库发给备库的日志是完整的。所以,若是实例 B 须要的日志已
经不存在,A’就拒绝把日志发给 B。

这跟基于位点的主备协议不一样。基于位点的协议,是由备库决定的,备库指定哪一个位点,主库就发哪一个位点,不作日志的完整性判断。

二、基于上面的介绍,咱们再来看看引入 GTID 后,一主多从的切换场景下,主备切换是如何实现的。

因为不须要找位点了,因此从库 B、C、D 只须要分别执行 change master 命令指向实例A’便可。

其实,严谨地说,主备切换不是不须要找位点了,而是找位点这个工做,在实例 A’内部就已经自动完成了。但因为这个工做是自动的,因此对 HA 系统的开发人员来讲,很是友好。

以后这个系统就由新主库 A’写入,主库 A’的本身生成的 binlog 中的 GTID 集合格式是:server_uuid_of_A’:1-M。

若是以前从库 B 的 GTID 集合格式是 server_uuid_of_A:1-N, 那么切换以后 GTID 集合的格式就变成了 server_uuid_of_A:1-N, server_uuid_of_A’:1-M。

固然,主库 A’以前也是 A 的备库,所以主库 A’和从库 B 的 GTID 集合是同样的。这就达到了咱们预期。

5、GTID 和在线 DDL

接下来,我再举个例子帮你理解 GTID。

以前在第 22 篇文章《MySQL 有哪些“饮鸩止渴”提升性能的方法?》中,我和你提到业务高峰期的慢查询性能问题时,分析到若是是因为索引缺失引发的性能问题,咱们能够通
过在线加索引来解决。可是,考虑到要避免新增索引对主库性能形成的影响,咱们能够先在备库加索引,而后再切换。

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

评论区有位同窗提出了一个问题:这样操做的话,数据库里面是加了索引,可是 binlog并无记录下这一个更新,是否是会致使数据和日志不一致?

这个问题提得很是好。当时,我在留言的回复中就引用了 GTID 来讲明。今天,我再和你展开说明一下。

假设,这两个互为主备关系的库仍是实例 X 和实例 Y,且当前主库是 X,而且都打开了GTID 模式。这时的主备切换流程能够变成下面这样:

  • 在实例 X 上执行 stop slave。
  • 在实例 Y 上执行 DDL 语句。注意,这里并不须要关闭 binlog。
  • 执行完成后,查出这个 DDL 语句对应的 GTID,并记为 server_uuid_of_Y:gno。
  • 到实例 X 上执行如下语句序列:
set GTID_NEXT="server_uuid_of_Y:gno";
begin;
commit;
set gtid_next=automatic;
start slave;

这样作的目的在于,既可让实例 Y 的更新有 binlog 记录,同时也能够确保不会在实例X 上执行这条更新。

接下来,执行完主备切换,而后照着上述流程再执行一遍便可。

6、小结

在今天这篇文章中,我先和你介绍了一主多从的主备切换流程。在这个过程当中,从库找新主库的位点是一个痛点。由此,咱们引出了 MySQL 5.6 版本引入的 GTID 模式,介绍了
GTID 的基本概念和用法。

能够看到,在 GTID 模式下,一主多从切换就很是方便了。

所以,若是你使用的 MySQL 版本支持 GTID 的话,我都建议你尽可能使用 GTID 模式来作一主多从的切换。

在下一篇文章中,咱们还能看到 GTID 模式在读写分离场景的应用。

最后,又到了咱们的思考题时间。

你在 GTID 模式下设置主从关系的时候,从库执行 start slave 命令后,主库发现须要的binlog 已经被删除掉了,致使主备建立不成功。这种状况下,你以为能够怎么处理呢?

你能够把你的方法写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。

7、上期问题时间

上一篇文章最后,我给你留的问题是,若是主库都是单线程压力模式,在从库追主库的过程当中,binlog-transaction-dependency-tracking 应该选用什么参数?

这个问题的答案是,应该将这个参数设置为 WRITESET。

因为主库是单线程压力模式,因此每一个事务的 commit_id 都不一样,那么设置为COMMIT_ORDER 模式的话,从库也只能单线程执行。

一样地,因为 WRITESET_SESSION 模式要求在备库应用日志的时候,同一个线程的日志必须与主库上执行的前后顺序相同,也会致使主库单线程压力模式下退化成单线程复制。

因此,应该将 binlog-transaction-dependency-tracking 设置为 WRITESET。

评论区留言点赞板:

@慧鑫 coming 问了一个好问题,对同一行做更新的几个事务,若是commit_id 相同,是否是在备库并行执行的时候会致使数据不一致?这个问
题的答案是更新同一行的事务是不可能同时进入 commit 状态的。

@老杨同志 对这个问题给出了更详细的回答,你们能够去看一下。

相关文章
相关标签/搜索