文章目录

  • MySQL45讲
  • 实践篇
  • 27 | 主库出问题了,从库怎么办?
  • 基于位点的主备切换
  • GTID
  • 基于 GTID 的主备切换
  • GTID 和在线 DDL



MySQL45讲

实践篇

27 | 主库出问题了,从库怎么办?

一主多从基本结构

mysql8 主从忽略 mysql主从故障切换_GTID

主库发生故障,主备切换后的结果

mysql8 主从忽略 mysql主从故障切换_mysql_02

基于位点的主备切换

当把节点 B 设置成节点 A’的从库的时候,需要执行一条 change master 命令:

CHANGE MASTER TO 
MASTER_HOST=$host_name  # 主库 A’ 的 IP
MASTER_PORT=$port  # 主库 A’ 的端口
MASTER_USER=$user_name  # 主库 A’ 的用户名
MASTER_PASSWORD=$password  # 主库 A’的密码
MASTER_LOG_FILE=$master_log_name  # 主库对应的文件名
MASTER_LOG_POS=$master_log_pos  # 主库对应的日志偏移量

参数 MASTER_LOG_FILEMASTER_LOG_POS 表示要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。这个位置就是同步位点

疑问:如何设置参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 这两个参数?

取同步位点的方法:

  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

mysql8 主从忽略 mysql主从故障切换_同步位点_03

end_log_pos 后面的值“123”,表示的就是 A’ 这个实例,在 T 时刻写入新的 binlog 的位置。

end_log_pos 这个值并不精确。

假设在 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’ 错误,提示出现了主键冲突,然后停止同步。

解决方案:

在切换任务的时候,要先主动跳过这些错误。有两种常用的方法。

  1. 主动跳过一个事务。跳过命令的写法是:
set global sql_slave_skip_counter=1;
start slave;

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

  1. 通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。

在执行主备切换时,有两类错误会经常遇到:

  • 1062 错误是插入数据时唯一键冲突;
  • 1032 错误是删除数据时找不到行。
    因此,可以把 slave_skip_errors 设置为 “1032,1062”,这样中间碰到这两个错误时就直接跳过。
    注意:这种直接跳过指定错误的方法,针对的是主备切换时,由于找不到精确的同步位点,所以只能采用这种方法来创建从库和新主库的主备关系。
    背景:清楚在主备切换过程中,直接跳过 1032 和 1062 这两类错误无损,所以才可以这么设置 slave_skip_errors 参数。 等到主备间的同步关系建立完成,并稳定执行一段时间之后,还需要把这个参数设置为空,以免之后真的出现了主从数据不一致,也跳过了。
GTID

通过 sql_slave_skip_counter 跳过事务和通过 slave_skip_errors 忽略错误的方法,虽然都最终可以建立从库 B 和新主库 A’的主备关系,但这两种操作都很复杂,而且容易出错。所以,MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。

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

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

在 MySQL 的官方文档里,GTID 格式是这么定义的:

GTID=source_id:transaction_id

这里的 transaction_id 容易造成误解。在MySQL 中 transaction_id 就是指事务 id,事务 id 是在事务执行过程中分配的,如果这个事务回滚了,事务 id 也会递增,而 gno 是在事务提交的时候才会分配

启动GTID:启动 MySQL 实例时,加上参数 gtid_mode=on 和 enforce_gtid_consistency=on。

GTID 有两种生成方式,而使用哪种方式取决于 session 变量 gtid_next 的值。

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

每个 MySQL 实例都维护了一个 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);

初始化数据的 binlog

mysql8 主从忽略 mysql主从故障切换_MySQL_04

此时,gtid_next=automatic。
记录 binlog 的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’。

如果实例 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 集合中。
start slave 命令是让同步线程执行起来。
set gtid_next=automatic 的作用是“恢复 GTID 的默认分配行为”,也就是说如果之后有新的事务执行,还是按照原来的分配方式,继续分配 gno=3。

mysql8 主从忽略 mysql主从故障切换_mysql_05

此时,对 X来说,gtid_next 是一个指定的 GTID 的值。因为 “aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”已经存在于实例 X 的 GTID 集合中,所以实例 X 会直接跳过这个事务,也就不会再出现主键冲突的错误。

基于 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  # 表示这个主备关系使用的是 GTID 协议

假设实例 A’ 的 GTID 集合记为 set_a,实例 B 的 GTID 集合记为 set_b。主备切换逻辑如下:

  1. 实例 B 指定主库 A’,基于主备协议建立连接。
  2. 实例 B 把 set_b 发给主库 A’。
  3. 实例 A’ 算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b 的 GTID 的集合,判断 A’ 本地是否包含了这个差集需要的所有 binlog 事务。
    a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
    b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;
  4. 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。

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

由于不需要找位点了,所以从库 B、C、D 只需要分别执行 change master 命令指向实例 A’ 即可。但严谨地说,主备切换不是不需要找位点,而是找位点这个工作,在实例 A’ 内部就已经自动完成。

GTID 和在线 DDL

疑问:数据库里面加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?

假设两个互为主备关系的实例 X 和实例 Y,且当前主库是 X,并且都打开了 GTID 模式。主备切换流程如下:

  1. 在实例 X 上执行 stop slave(停止 X 实例从 Y 同步)。
  2. 在实例 Y 上执行 DDL 语句。注意,这里并不需要关闭 binlog。
  3. 执行完成后,查出这个 DDL 语句对应的 GTID,并记为 server_uuid_of_Y:gno。
  4. 到实例 X 上执行以下语句序列:
set GTID_NEXT="server_uuid_of_Y:gno";
begin;
commit;
set gtid_next=automatic;
start slave;

这样做的目的在于,既可以让实例 Y 的更新有 binlog 记录,同时也可以确保不会在实例 X 上执行这条更新。
5. 接下来,执行完主备切换,然后照着上述流程再执行一遍即可。

上述流程分析

当 Y 执行完 DDL,并且 X 忽略 Y 这个 DDL 的 GTID 。之后,将 Y 提升为主库,X 设置成从库。然后 Y 执行 X 执行 DDL,且 Y 忽略 X 的这个 DDL 的 GTID,这样就完成了在两个库上 DDL和主备切换。