文章目录
- MySQL45讲
- 实践篇
- 27 | 主库出问题了,从库怎么办?
- 基于位点的主备切换
- GTID
- 基于 GTID 的主备切换
- GTID 和在线 DDL
MySQL45讲
实践篇
27 | 主库出问题了,从库怎么办?
一主多从基本结构
主库发生故障,主备切换后的结果
基于位点的主备切换
当把节点 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_FILE 和 MASTER_LOG_POS 表示要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。这个位置就是同步位点。
疑问:如何设置参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 这两个参数?
取同步位点的方法:
- 等待新主库 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 的位置。
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’ 错误,提示出现了主键冲突,然后停止同步。
解决方案:
在切换任务的时候,要先主动跳过这些错误。有两种常用的方法。
- 主动跳过一个事务。跳过命令的写法是:
set global sql_slave_skip_counter=1;
start slave;
因为切换过程中,可能会不止重复执行一个事务,所以需要在从库 B 刚开始连接到新主库 A’ 时,持续观察,每次碰到这些错误就停下来,执行一次跳过命令,直到不再出现停下来的情况,以此来跳过可能涉及的所有事务。
- 通过设置 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
此时,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。
此时,对 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。主备切换逻辑如下:
- 实例 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。
由于不需要找位点了,所以从库 B、C、D 只需要分别执行 change master 命令指向实例 A’ 即可。但严谨地说,主备切换不是不需要找位点,而是找位点这个工作,在实例 A’ 内部就已经自动完成。
GTID 和在线 DDL
疑问:数据库里面加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?
假设两个互为主备关系的实例 X 和实例 Y,且当前主库是 X,并且都打开了 GTID 模式。主备切换流程如下:
- 在实例 X 上执行 stop slave(停止 X 实例从 Y 同步)。
- 在实例 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 上执行这条更新。
5. 接下来,执行完主备切换,然后照着上述流程再执行一遍即可。
上述流程分析
当 Y 执行完 DDL,并且 X 忽略 Y 这个 DDL 的 GTID 。之后,将 Y 提升为主库,X 设置成从库。然后 Y 执行 X 执行 DDL,且 Y 忽略 X 的这个 DDL 的 GTID,这样就完成了在两个库上 DDL和主备切换。