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

基本的一主多从结构:

图中,A和A'互为主备,从库BCD指向主库A。一主多次的设置,一般用于读写分离,主库负责所有的写入和一部分读,从库负责其他的读请求。

当主库发生故障,主备切换:

一主多从结构在切换完成后,A'会成为新主库,从库需要改接到A',而这个过程会增加主备切换的复杂度。接下来,就看看切换系统会怎么完成该切换过程。

基于位点的主备切换

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

sql 复制代码
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_HOST、MASTER_PORT、MASTER_USER、MASTER_PASSWORD代表主库A'的IP、端口、用户名和密码;

  • MASTER_LOG_FILE和MASTER_LOG_POS表示,要从主库的master_log_name文件的master_log_pos位置的日志继续同步。该位置就是所说的同步位点,也就是主库对应的文件名和日志偏移量。

之前节点B是A的从库,本地记录的是A的位点。但相同的日志,A的位点和A'的位点是不同的,因此从库B要切换时就需要先经过找同步位点的逻辑。

位点的一般获取方式是,考虑到切换过程不能丢数据,找的时候总是要找一个"稍微往前"的,然后再通过判断跳过在从库B上已经执行过的事务。

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

  • 等待新主库A'把中转日志relay log全部同步完成;

  • 在A'上执行show master status命令,得到当前A'上最新的File和position;

  • 取原主库A故障的时刻T;

  • 用mysqlbinlog工具解析A'的File,得到T时刻的位点。

sql 复制代码
mysqlbinlog File --stop-datetime=T --start-datetime=T

图中的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'错误,提示出现主键冲突,然后停止同步。

因此通常切换任务时要先主动跳过这些错误,有两种常用的方法:

一种是主动跳过一个事务,命令为:

sql 复制代码
set global sql_slave_skip_counter=1;
start slave;

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

另一种是通过设置slave_skip_errors参数直接设置跳过指定的错误。

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

  • 1062错误是插入数据时唯一键冲突;

  • 1032错误是删除数据时找不到行。

因此可以把slave_skip_errors设置为1032和1062。

需要注意的是,这种直接跳过指定错误的方法,针对的是主备切换时,由于找不到精确的同步位点,所以只能采用这种方法来创建从库和新主库的主备关系。

在主备切换过程中直接跳过这两类错误是无损的,因此可以这样设置。等到主备间的同步关系建立完成,并稳定执行一段时间之后,还需要把这个参数设置为空,以免之后真出现主从数据不一致也跳过。

GTID

前面的两种方法操作都较为复杂,且容易出错,因此MySQL 5.6引入了GTID,彻底解决了这个困难。

GTID全程是Global Transaction Identifier,即全局事务ID,是一个事务在提交时生成的,是这个事务的唯一标识,格式为:

sql 复制代码
GTID=server_uuid:gno

其中:

  • server_uuid是一个实例第一次启动时自动生成的,是一个全局唯一的值;

  • gno是一个整数,初始值为1,每次提交事务的时候分配给这个事务,并加一。

GTID模式的启动是,在启动一个MySQL实例的时候,加上参数gtid_mode=onenforce_gtid_consistency=on。在GTID模式下,每个事务都会跟一个GTID一一对应,GTID有两种生成方式,而使用哪种方式取决于session变量gtid_next的值:

  • gtid_next=automatic代表使用默认值,即分配server_uuid:gno;

  • gtid_next是一个指定的值,比如指定为current_gtid

    • 如果current_gtid已存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;

    • 如果current_gtid没有存在于实例的GTID集合中,就把这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新GTID,因此gno不用加1。

一个current_gtid只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要执行set命令,把gtid_next设置成另外一个gtid或者automatic。这样,每个MySQL实例都维护了一个GTID集合,用来对应"这个实例执行过的所有事务"。

接下来用一个例子帮助理解。在实例X中创建一个表t:

sql 复制代码
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:

事务BEGIN前有@@SESSION.GTID_NEXT命令,此时如果实例X有从库,将binlog同步过去执行的话,执行事务前会先执行这两个set命令,这样图中的两个GTID都会被加入从库的GTID集合。

假设现在实例X是另外一个实例Y的从库,并且此时在实例Y上执行了插入语句:

sql 复制代码
insert into t values(1,1);

且这条语句在实例Y上的GTID是 "aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10"。那么实例X要同步过来执行时会出现主键冲突,导致实例X同步线程停止,此时处理方法是执行下面的语句序列:

sql 复制代码
set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
begin;
commit;
set gtid_next=automatic;
start slave;

前三条语句的作用是通过提交一个空事务,把该GTID加到实例X的GTID集合中,这样再执行start slav 命令让同步线程执行起来的时候,虽然实例X上还是会继续执行实例Y传过来的事务,但是由于"aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10"已经存在于实例X的GTID集合,所以实例X就会直接跳过这个事务,也就不会再出现主键冲突的错误。

start slave前的set gtid_next=automatic作用是恢复GTID的默认分配行为,即如果之后有新的事务再执行,还是按照原有方式,继续分配gno=3

基于GTID的主备切换

在GTID模式下,备库B要设置为新主库A'的从库的语法如下:

sql 复制代码
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协议。

将该时刻实例A'的GTID集合记为set_a,实例B的GTID集合记为set_b,在实例B上执行start slave命令,取binlog的逻辑为:

  • 实例B指定主库A',基于主备协议建立连接;

  • 实例B把set_b发给主库A';

  • 实例A'算出set_a和set_b的差集,判断A'本地是否包含差集需要的所有binlog事务。

    • 如果不包含,表示A'已经把实例B需要的binlog给删除了,直接返回错误;

    • 如果确认全部包含,A'从自己的binlog里找出第一个不在set_b的事务发给B;

  • 之后从该事务开始,往后读文件,按顺序取binlog发给B执行。

在基于GTID的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例B需要的日志已经不存在,A'就拒绝把日志发给B。而基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。

基于上面的介绍,再看引入GTID后,一主多从的切换场景下如何实现主备切换。由于不需要找位点,所以从库BCD只需要分别执行change master命令指向实例A'即可。

GTID和在线DDL

在之前的文章MySQL 22中,提到过业务高峰期的慢查询性能问题,分析如果是由于索引缺失引起的性能问题,可以通过在线加索引解决,但考虑到避免新增索引对主库性能造成的影响,可以先在备库加索引,然后再切换。在双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上执行以下语句序列:

    sql 复制代码
    set GTID_NEXT="server_uuid_of_Y:gno";
    begin;
    commit;
    set gtid_next=automatic;
    start slave;

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

  • 接下来,执行完主备切换,照着上面的流程再执行一遍即可。

相关推荐
源图客14 小时前
Spark读取MySQL数据库表
数据库·mysql·spark
xiucai_cs16 小时前
MySQL深分页慢问题及性能优化
数据库·mysql·性能优化·深分页
当牛作馬16 小时前
ES常用查询命令
数据库·mysql·elasticsearch
hzp66619 小时前
阿里云的centos8 服务器安装MySQL 8.0
mysql·阿里云·centos8
码luffyliu1 天前
MySQL:MVCC机制及其在Java秋招中的高频考点
java·数据库·mysql·事务·并发·mvcc
水涵幽树1 天前
MySQL 时间筛选避坑指南:为什么格式化字符串比较会出错?
数据库·后端·sql·mysql·database
@蓝眼睛1 天前
mac的m3芯片安装mysql
mysql·macos
冰块的旅行1 天前
MySQL 的时区问题
mysql
舒一笑1 天前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员