文章目录
- 一、MySQL 主备的基本原理
- 二、binlog 里面到底是什么内容,为什么备库拿过去可以直接执行?
- 2.1、binlog的三种格式
- 2.1.1、binlog_format=statement
- 2.1.2、 binlog_format=‘row’
- 2.1.3、binlog_format='mixed'
- 2.1.4、场景要求把 MySQL 的 binlog 格式设置成 row(恢复数据)
- 2.2、循环复制问题(生产双主架构)
- 2.2.1、特殊场景
- 2.3、双主架构,主从判断依据
- 三、双主架构如何保证高可用?
- 3.1、主备延迟的来源(备库性能差)
- 3.2、追问 1:但是,做了对称部署以后,还可能会有延迟。这是为什么呢?(备库压力大)
- 3.3、追问 2:采用了一主多从,保证备库的压力不会超过主库,还有什么情况可能导致主备延迟吗?(大事务)
- 3.3.1、大事务
- 3.3.2、大表DDL(gh-ost 方案)
- 3.3.3、备库的并行复制能力
- 3.3.3.1、事务能不能按照轮询的方式分发给各个 worker,也就是第一个事务分给 worker_1,第二个事务发给 worker_2 呢?
- 3.3.3.2、同一个事务的多个更新语句,能不能分给不同的worker 来执行呢?
- 3.3.3.3、MySQL 5.6 版本的并行复制策略(按库并行)
- 3.3.3.4、MariaDB 的并行复制策略
- 3.3.3.5、MySQL 5.7 的并行复制策略
- 3.3.3.6、MySQL 5.7.22 的并行复制策略
- 3.4、双主消息同步方式
- 四、主备切换
- 4.1、可靠性优先策略
- 五、主从高可用
- 5.1、一主多从
- 5.1.1、在一主多从架构下,主库故障后的主备切换问题
- 5.1.2、一主多从架构下的主备切换步骤
- 5.1.3、同步位点的问题
- 5.1.3.1、跳过事务
- 5.1.3.2、跳过指定错误
- 5.1.3.3、GTID
- 5.1.4、基于GTID的主备切换
- 5.1.5、在双 M 结构下,备库执行的 DDL 语句也会传给主库,为了避免传回后对主库造成影响,要通过 set sql_log_bin=off 关掉 binlog。这样操作的话,数据库里面是加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?
一、MySQL 主备的基本原理

在状态 1 中,客户端的读写都直接访问节点 A,而节点 B 是 A 的备库,只是将 A 的更新都
同步过来,到本地执行。这样可以保持节点 B 和 A 的数据是相同的。
当需要切换的时候,就切成状态 2。这时候客户端读写访问的都是节点 B,而节点 A 是 B
的备库。
在状态 1 中,虽然节点 B 没有被直接访问,但是我依然建议你把节点 B(也就是备库)设
置成只读(readonly)模式。这样做,有以下几个考虑:
- 有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;
- 防止切换逻辑有 bug,比如切换过程中出现双写,造成主备不一致;
- 可以用 readonly 状态,来判断节点的角色。
把备库设置成只读了,还怎么跟主库保持同步更新呢?
这个问题,你不用担心。因为 readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新的线程,就拥有超级权限。
接下来,我们再看看节点 A 到 B 这条线的内部流程是什么样的。图 2 中画出的就是一个update 语句在节点 A 执行,然后同步到节点 B 的完整流程图。
节点 A 到 B 这条线的内部流程是什么样的。图 2 中画出的就是一个update 语句在节点 A 执行,然后同步到节点 B 的完整流程图

图 2 中,包含了我在上一篇文章中讲到的 binlog 和 redo log 的写入机制相关的内容,可以看到:主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写 binlog。备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B的这个长连接。一个事务日志同步的完整过程是这样的:
- 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
- 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread和 sql_thread。其中 io_thread 负责与主库建立连接。
- 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
- 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
- sql_thread 读取中转日志,解析出日志里的命令,并执行。
这里需要说明,后来由于多线程复制方案的引入,sql_thread 演化成为了多个线程
二、binlog 里面到底是什么内容,为什么备库拿过去可以直接执行?
2.1、binlog的三种格式
binlog 有两种格式,一种是 statement,一种是row。可能你在其他资料上还会看到有第三种格式,叫作 mixed,其实它就是前两种格式的混合。
为了便于描述 binlog 的这三种格式间的区别,我创建了一个表,并初始化几行数据。
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;
insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');如果要在表中删除一行数据的话,我们来看看这个 delete 语句的 binlog 是怎么记录的。
注意,下面这个语句包含注释,如果你用 MySQL 客户端来做这个实验的话,要记得加 -c参数,否则客户端会自动去掉注释。
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;2.1.1、binlog_format=statement
当 binlog_format=statement 时,binlog 里面记录的就是 SQL 语句的原文。你可以用命令看 binlog 中的内容。

第一行 SET @@SESSION.GTID_NEXT='ANONYMOUS’你可以先忽略,后面文章我们会在介绍主备切换的时候再提到;
第二行是一个 BEGIN,跟第四行的 commit 对应,表示中间是一个事务;
第三行就是真实执行的语句了。可以看到,在真实执行的 delete 命令之前,还有一个“use ‘test’”命令。这条命令不是我们主动执行的,而是 MySQL 根据当前要操作的表所在的数据库,自行添加的。这样做可以保证日志传到备库去执行的时候,不论当前的工作线程在哪个库里,都能够正确地更新到 test 库的表 t。
use 'test’命令之后的 delete 语句,就是我们输入的 SQL 原文了。可以看到,binlog“忠实”地记录了 SQL 命令,甚至连注释也一并记录了。
为了说明 statement 和 row 格式的区别,我们来看一下这条 delete 命令的执行效果图:

可以看到,运行这条 delete 命令产生了一个 warning,原因是当前 binlog 设置的是statement 格式,并且语句中有 limit,所以这个命令可能是 unsafe 的。
为什么这么说呢?这是因为 delete 带 limit,很可能会出现主备数据不一致的情况。比如上面这个例子:
- 如果 delete 语句使用的是索引 a,那么会根据索引 a 找到第一个满足条件的行,也就是说删除的是 a=4 这一行;
- 但如果使用的是索引 t_modified,那么删除的就是 t_modified='2018-11-09’也就是a=5 这一行。
由于 statement 格式下,记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a;而在备库执行这条 SQL 语句的时候,却使用了索引 t_modified。因此,MySQL 认为这样写是有风险的
2.1.2、 binlog_format=‘row’

可以看到,与 statement 格式的 binlog 相比,前后的 BEGIN 和 COMMIT 是一样的。但
是,row 格式的 binlog 里没有了 SQL 语句的原文,而是替换成了两个 event:
Table_map 和 Delete_rows。
- Table_map event,用于说明接下来要操作的表是 test 库的表 t;
- Delete_rows event,用于定义删除的行为。
其实,我们通过图 5 是看不到详细信息的,还需要借助 mysqlbinlog 工具,用下面这个命令解析和查看 binlog 中的内容。因为图 5 中的信息显示,这个事务的 binlog 是从 8900这个位置开始的,所以可以用 start-position 参数来指定从这个位置的日志开始解析。
mysqlbinlog -vv data/master.000001 --start-position=8900;
server id 1,表示这个事务是在 server_id=1 的这个库上执行的。
每个 event 都有 CRC32 的值,这是因为我把参数 binlog_checksum 设置成了CRC32。
Table_map event 跟在图 5 中看到的相同,显示了接下来要打开的表,map 到数字226。现在我们这条 SQL 语句只操作了一张表,如果要操作多张表呢?每个表都有一个对应的 Table_map event、都会 map 到一个单独的数字,用于区分对不同表的操作。
我们在 mysqlbinlog 的命令中,使用了 -vv 参数是为了把内容都解析出来,所以从结果里面可以看到各个字段的值(比如,@1=4、 @2=4 这些值)。
binlog_row_image 的默认配置是 FULL,因此 Delete_event 里面,包含了删掉的行的所有字段的值。如果把 binlog_row_image 设置为 MINIMAL,则只会记录必要的信息,在这个例子里,就是只会记录 id=4 这个信息。
最后的 Xid event,用于表示事务被正确地提交了。
你可以看到,当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除 id=4 的行,不会有主备删除不同行的问题。
2.1.3、binlog_format=‘mixed’
因为有些 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式。
但 row 格式的缺点是,很占空间。比如你用一个 delete 语句删掉 10 万行数据,用statement 的话就是一个 SQL 语句被记录到 binlog 中,占用几十个字节的空间。但如果用 row 格式的 binlog,就要把这 10 万条记录都写到 binlog 中。这样做,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度。
所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用row 格式,否则就用 statement 格式。
也就是说,mixed 格式可以利用 statment 格式的优点,同时又避免了数据不一致的风险。
因此,如果你的线上MySQL 设置的 binlog 格式是 statement 的话,那基本上就可以认为这是一个不合理的设置。你至少应该把 binlog 的格式设置为 mixed。
2.1.4、场景要求把 MySQL 的 binlog 格式设置成 row(恢复数据)
因为通过row全量的上下文信息更好的可以恢复数据,比如这款工具
2.2、循环复制问题(生产双主架构)

如果节点 A 同时是节点 B 的备库,相当于又把节点 B 新生成的 binlog 拿过来执行了一次,然后节点 A 和 B 间,会不断地循环执行这个更新语句,也就是循环复制了。这个要怎么解决呢?
从上面的图 6 中可以看到,MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的server id。因此,我们可以用下面的逻辑,来解决两个节点间的循环复制的问题:
- 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
- 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的binlog;
- 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。
按照这个逻辑,如果我们设置了双 M 结构,日志的执行流就会变成这样: - 从节点 A 更新的事务,binlog 里面记的都是 A 的 server id;
- 传到节点 B 执行一次以后,节点 B 生成的 binlog 的 server id 也是 A 的 server id;
- 再传回给节点 A,A 判断到这个 server id 与自己的相同,就不会再处理这个日志。所以,死循环在这里就断掉了
2.2.1、特殊场景
MySQL 通过判断 server id 的方式,断掉死循环。但是,这个机制其实并不完备,在某些场景下,还是有可能出现死循环
2.3、双主架构,主从判断依据
一开始创建主备关系的时候, 是由备库指定的。
比如基于位点的主备关系,备库说“我要从binlog文件A的位置P”开始同步, 主库就从这个指定的位置开始往后发。
而主备复制关系搭建完成以后,是主库来决定“要发数据给备库”的。
所以主库有生成新的日志,就会发给备库
三、双主架构如何保证高可用?

“同步延迟”。与数据同步有关的时间点主要包括以下三个:
- 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
- 之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
- 备库 B 执行完成这个事务,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。
你可以在备库上执行 show slave status 命令,它的返回结果里面会显示
seconds_behind_master,用于表示当前备库延迟了多少秒。
seconds_behind_master 的计算方法是这样的:
- 每个事务的 binlog 里面都有一个时间字段,用于记录主库上写入的时间;
- 备库取出当前正在执行的事务的时间字段的值,计算它与当前系统时间的差值,得到seconds_behind_master。
可以看到,其实 seconds_behind_master 这个参数计算的就是 T3-T1。所以,我们可以用 seconds_behind_master 来作为主备延迟的值,这个值的时间精度是秒。
你可能会问,如果主备库机器的系统时间设置不一致,会不会导致主备延迟的值不准?
其实不会的。因为,备库连接到主库的时候,会通过执行 SELECT UNIX_TIMESTAMP() 函数来获得当前主库的系统时间。如果这时候发现主库的系统时间与自己不一致,备库在执行seconds_behind_master 计算的时候会自动扣掉这个差值。
需要说明的是,在网络正常的时候,日志从主库传给备库所需的时间是很短的,即 T2-T1的值是非常小的。也就是说,网络正常情况下,主备延迟的主要来源是备库接收完 binlog和执行完这个事务之间的时间差。
所以说,主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产binlog 的速度要慢。
3.1、主备延迟的来源(备库性能差)
首先,有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。
如果是双主架构,则主备会随时切换,所以规格应该相同。
3.2、追问 1:但是,做了对称部署以后,还可能会有延迟。这是为什么呢?(备库压力大)
这就是第二种常见的可能了,即备库的压力大。一般的想法是,主库既然提供了写能力,那么备库可以提供一些读能力。或者一些运营后台需要的分析语句,不能影响正常业务,所以只能在备库上跑。
我真就见过不少这样的情况。由于主库直接影响业务,大家使用起来会比较克制,反而忽视了备库的压力控制。结果就是,备库上的查询耗费了大量的 CPU 资源,影响了同步速度,造成主备延迟。
- 一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。
- 通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的
能力。
其中,一主多从的方式大都会被采用。因为作为数据库系统,还必须保证有定期全量备份的
能力。而从库,就很适合用来做备份。
备注:这里需要说明一下,从库和备库在概念上其实差不多。在我们这个专栏里,为了方便描述,我把会在 HA 过程中被选成新主库的,称为备库,其他的称为从库。
3.3、追问 2:采用了一主多从,保证备库的压力不会超过主库,还有什么情况可能导致主备延迟吗?(大事务)
3.3.1、大事务
大事务这种情况很好理解。因为主库上必须等事务执行完成才会写入 binlog,再传给备库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10分钟。
不知道你所在公司的 DBA 有没有跟你这么说过:不要一次性地用 delete 语句删除太多数据。其实,这就是一个典型的大事务场景。
比如,一些归档类的数据,平时没有注意删除历史数据,等到空间快满了,业务开发人员要一次性地删掉大量历史数据。同时,又因为要避免在高峰期操作会影响业务(至少有这个意识还是很不错的),所以会在晚上执行这些大量数据的删除操作。
结果,负责的 DBA 同学半夜就会收到延迟报警。然后,DBA 团队就要求你后续再删除数据的时候,要控制每个事务删除的数据量,分成多次删除
3.3.2、大表DDL(gh-ost 方案)
大表 DDL。这个场景,我在前面的文章中介绍过。处理方
案就是,计划内的 DDL,建议使用 gh-ost 方案
3.3.3、备库的并行复制能力
在官方的 5.6 版本之前,MySQL 只支持单线程复制,由此在主库并发高、TPS 高时就会出
现严重的主备延迟问题。

图 2 中,coordinator 就是原来的 sql_thread, 不过现在它不再直接更新数据了,只负责读取中转日志和分发事务。真正更新日志的,变成了 worker 线程。而 work 线程的个数,就是由参数 slave_parallel_workers 决定的。根据我的经验,把这个值设置为 8~16 之间最好(32 核物理机的情况),毕竟备库还有可能要提供读查询,不能把 CPU 都吃光了。
3.3.3.1、事务能不能按照轮询的方式分发给各个 worker,也就是第一个事务分给 worker_1,第二个事务发给 worker_2 呢?
事务被分发给 worker 以后,不同的 worker 就独立执行了。但是,
由于 CPU 的调度策略,很可能第二个事务最终比第一个事务先执行。而如果这时候刚好这
两个事务更新的是同一行,也就意味着,同一行上的两个事务,在主库和备库上的执行顺序
相反,会导致主备不一致的问题
3.3.3.2、同一个事务的多个更新语句,能不能分给不同的worker 来执行呢?
也不行。举个例子,一个事务更新了表 t1 和表 t2 中的各一行,如果这两条更新
语句被分到不同 worker 的话,虽然最终的结果是主备一致的,但如果表 t1 执行完成的瞬
间,备库上有一个查询,就会看到这个事务“更新了一半的结果”,破坏了事务逻辑的隔离
性。
coordinator 在分发的时候,需要满足以下这两个基本要求:
- 不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker
中。 - 同一个事务不能被拆开,必须放到同一个 worker 中。
3.3.3.3、MySQL 5.6 版本的并行复制策略(按库并行)
相比实现按表/按行并行策略的好处:
- 构造 hash 值的时候很快,只需要库名;而且一个实例上 DB 数也不会很多,不会出现需
要构造 100 万个项这种情况。 - 不要求 binlog 的格式。因为 statement 格式的 binlog 也可以很容易拿到库名。
3.3.3.4、MariaDB 的并行复制策略
redo log 组提交 (group commit) 优化, 而 MariaDB的并行复制策略利用的就是这个特性:
- 能够在同一组里提交的事务,一定不会修改同一行;
- 主库上可以并行执行的事务,备库上也一定是可以并行执行的。
实现步骤:
- 在一组里面一起提交的事务,有一个相同的 commit_id,下一组就是 commit_id+1;
- commit_id 直接写到 binlog 里面;
- 传到备库应用的时候,相同 commit_id 的事务分发到多个 worker 执行;
- 这一组全部执行完成后,coordinator 再去取下一批。
假设了三组事务在主库的执行情况,你可以看到在 trx1、trx2 和 trx3 提交的
时候,trx4、trx5 和 trx6 是在执行的。这样,在第一组事务提交完成的时候,下一组事务很快就会进入 commit 状态。

而按照 MariaDB 的并行复制策略,备库上的执行效果如图 6 所示。

在备库上执行的时候,要等第一组事务完全执行完成后,第二组事务才能开始执行,这样系统的吞吐量就不够。
另外,这个方案很容易被大事务拖后腿。假设 trx2 是一个超大事务,那么在备库应用的时候,trx1 和 trx3 执行完成后,就只能等 trx2 完全执行完成,下一组才能开始执行。这段时间,只有一个 worker 线程在工作,是对资源的浪费。
3.3.3.5、MySQL 5.7 的并行复制策略
slave-parallel-type 来控制并行复制策略:
- 配置为 DATABASE,表示使用 MySQL 5.6 版本的按库并行策略;
- 配置为 LOGICAL_CLOCK,表示的就是类似 MariaDB 的策略。不过,MySQL 5.7 这个
策略,针对并行度做了优化。
同时处于“执行状态”的所有事务,是不是可以并行?
答案是,不能。
因为,这里面可能有由于锁冲突而处于锁等待状态的事务。如果这些事务在备库上被分配到不同的 worker,就会出现备库跟主库不一致的情况。
而上面提到的 MariaDB 这个策略的核心,是“所有处于 commit”状态的事务可以并行。事务处于 commit 状态,表示已经通过了锁冲突的检验了

其实,不用等到 commit 阶段,只要能够到达 redo log prepare 阶段,就表示事务已经通过锁冲突的检验了。
因此,MySQL 5.7 并行复制策略的思想是:
- 同时处于 prepare 状态的事务,在备库执行时是可以并行的;
- 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在备库执行时也是可以并
行的。
讲 binlog 的组提交的时候,介绍过两个参数:
- binlog_group_commit_sync_delay 参数,表示延迟多少微秒后才调用 fsync;
- binlog_group_commit_sync_no_delay_count 参数,表示累积多少次以后才调用
fsync。
这两个参数是用于故意拉长 binlog 从 write 到 fsync 的时间,以此减少 binlog 的写盘次
数。在 MySQL 5.7 的并行复制策略里,它们可以用来制造更多的“同时处于 prepare 阶
段的事务”。这样就增加了备库复制的并行度。
3.3.3.6、MySQL 5.7.22 的并行复制策略
在 2018 年 4 月份发布的 MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。
相应地,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用
这个新策略。这个参数的可选值有以下三种。
- COMMIT_ORDER,表示的就是前面介绍的,根据同时进入 prepare 和 commit 来判断
是否可以并行的策略。 - WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合
writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可
以并行。 - WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
当然为了唯一标识,这个 hash 值是通过“库名 + 表名 + 索引名 + 值”计算出来的。如果一个表上除了有主键索引外,还有其他唯一索引,那么对于每个唯一索引,insert 语句对应的 writeset 就要多增加一个 hash 值。
- writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解
析 binlog 内容(event 里的行数据),节省了很多计算量; - 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个 worker,更省内存;
- 由于备库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也是可以
的。
因此,MySQL 5.7.22 的并行复制策略在通用性上还是有保证的。
当然,对于“表上没主键”和“外键约束”的场景,WRITESET 策略也是没法并行的,也
会暂时退化为单线程模型。
3.4、双主消息同步方式
主备关系里面,备库主动连接,之后的binlog 发送是主库主动推送的。之所以这么设计也是为了效率和实时性考虑,毕竟靠备库轮询,会有时间差。
四、主备切换
4.1、可靠性优先策略
五、主从高可用
工作中大多数的互联网应用场景都是读多写少,因此你负责的业务,在发展过程中很可能先会遇到读性能的问题。而在数据库层解决读性能问题,就要涉及到接下来两篇文章要讨论的架构:
5.1、一主多从

图中,虚线箭头表示的是主备关系,也就是 A 和 A’互为主备, 从库 B、C、D 指向的是
主库 A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读
请求则由从库分担。
5.1.1、在一主多从架构下,主库故障后的主备切换问题

相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库 B、C、D 也要改接到 A’。正是由于多了从库 B、C、D 重新指向的这个过程,所以主备切换的复杂性也相应增加
5.1.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这条命令的6个参数:
MASTER_HOST、MASTER_PORT、MASTER_USER 和 MASTER_PASSWORD 四个参数,分别代表了主库 A’的 IP、端口、用户名和密码。
最后两个参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要从主库的
master_log_name 文件的 master_log_pos 这个位置的日志继续同步。而这个位置就是我们所说的同步位点,也就是主库对应的文件名和日志偏移量。
5.1.3、同步位点的问题
原来节点 B 是 A 的从库,本地记录的也是 A 的位点。但是相同的日志,A 的位点和 A’的位点是不同的。因此,从库 B 要切换的时候,就需要先经过“找同步位点”这个逻辑。
这个位点很难精确取到,只能取一个大概位置。为什么这么说呢?
- 考虑到切换过程中不能丢数据,所以我们找位点的时候,总是要找一个“稍微往前”的,然
后再通过判断跳过那些在从库 B 上已经执行过的事务。
- 等待新主库 A’把中转日志(relay log)全部同步完成;
- 在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
- 取原主库 A 故障的时刻 T;
- 用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。
mysqlbinlog File --stop-datetime=T --start-datetime=T
图中,end_log_pos 后面的值“123”,表示的就是 A’这个实例,在 T 时刻写入新的
binlog 的位置。然后,我们就可以把 123 这个值作为 $master_log_pos ,用在节点 B 的change master 命令里。
当然这个值并不精确。为什么呢?
你可以设想有这么一种情况,假设在 T 这个时刻,主库 A 已经执行完成了一个 insert 语句插入了一行数据 R,并且已经将 binlog 传给了 A’和 B,然后在传完的瞬间主库 A 的主机就掉电了。
那么,这时候系统的状态是这样的:
- 在从库 B 上,由于同步了 binlog, R 这一行已经存在;
- 在新主库 A’上, R 这一行也已经存在,日志是写在 123 这个位置之后的;
- 我们在从库 B 上执行 change master 命令,指向 A’的 File 文件的 123 位置,就会把
插入 R 这一行数据的 binlog 又同步到从库 B 去执行。
这时候,从库 B 的同步线程就会报告 Duplicate entry ‘id_of_R’ for key ‘PRIMARY’
错误,提示出现了主键冲突,然后停止同步。
所以,通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。
5.1.3.1、跳过事务
一种做法是,主动跳过一个事务。跳过命令的写法是:
set global sql_slave_skip_counter=1;
start slave;因为切换过程中,可能会不止重复执行一个事务,所以我们需要在从库 B 刚开始接到新主库 A’时,持续观察,每次碰到这些错误就停下来,执行一次跳过命令,直到不再出现停下来的情况,以此来跳过可能涉及的所有事务。
5.1.3.2、跳过指定错误
通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。
在执行主备切换时,有这么两类错误,是经常会遇到的:
因此,我们可以把 slave_skip_errors 设置为 “1032,1062”,这样中间碰到这两个错误时就直接跳过。
这里需要注意的是,这种直接跳过指定错误的方法,针对的是主备切换时,由于找不到精确的同步位点,所以只能采用这种方法来创建从库和新主库的主备关系。
这个背景是,我们很清楚在主备切换过程中,直接跳过 1032 和 1062 这两类错误是无损的,所以才可以这么设置 slave_skip_errors 参数。等到主备间的同步关系建立完成,并稳定执行一段时间之后,我们还需要把这个参数设置为空,以免之后真的出现了主从数据不一致,也跳过了。
5.1.3.3、GTID
GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分组成,格式是:
1 GTID=server_uuid:gnoserver_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;
gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
在 MySQL 的官方文档里,GTID 格式是这么定义的:
1 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 的值。
- 如果 gtid_next=automatic,代表使用默认值。这时,MySQL 就会把
server_uuid:gno 分配给这个事务。
a. 记录 binlog 的时候,先记录一行 SET
@@SESSION.GTID_NEXT=‘server_uuid:gno’;
b. 把这个 GTID 加入本实例的 GTID 集合。
1 GTID=server_uuid:gno
server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;
gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
1 GTID=source_id:transaction_id- 如果 gtid_next 是一个指定的 GTID 的值,比如通过 set gtid_next='current_gtid’指
定为 current_gtid,那么就有两种可能:
a. 如果 current_gtid 已经存在于实例的 GTID 集合中,接下来执行的这个事务会直接被
系统忽略;
b. 如果 current_gtid 没有存在于实例的 GTID 集合中,就将这个 current_gtid 分配给
接下来要执行的事务,也就是说系统不需要给这个事务生成新的 GTID,因此 gno 也不
用加 1。
5.1.4、基于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在实例 B 上执行 start slave 命令,取 binlog 的逻辑是这样的
- 实例 B 指定主库 A’,基于主备协议建立连接。
- 实例 B 把 set_b 发给主库 A’。
- 实例 A’算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b
的 GTID 的集合,判断 A’本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,
发给 B;- 之后就从这个事务开始,往后读文件,按顺序取 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.1.5、在双 M 结构下,备库执行的 DDL 语句也会传给主库,为了避免传回后对主库造成影响,要通过 set sql_log_bin=off 关掉 binlog。这样操作的话,数据库里面是加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?
假设,这两个互为主备关系的库还是实例 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上执行这条更新
















