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上执行这条更新。

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

相关推荐
xiaok1 小时前
GROUP BY进阶用法
mysql
李慕婉学姐2 小时前
【开题答辩过程】以《基于Android的健康助手APP的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
android·java·mysql
qq_12498707532 小时前
基于springboot健康养老APP的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·mysql·微信小程序·毕业设计
亚林瓜子2 小时前
mysql命令行手动导入csv数据到指定表
数据库·mysql·gui·csv·cli·db·import
一分半心动3 小时前
lnmp架构 mysql数据库Cannot assign requested address报错解决
linux·mysql·php
ChristXlx3 小时前
Linux安装mysql(虚拟机适用)
linux·mysql
瀚高PG实验室4 小时前
timestampdiff (MYSQL)函数在Highgo DB中的写法
数据库·mysql·瀚高数据库
还是鼠鼠4 小时前
SQL语句执行很慢,如何分析呢?
java·数据库·mysql·面试
云和数据.ChenGuang4 小时前
批量给100台服务器装系统,还要完成后续的配置和软件部署
运维·服务器·开发语言·mysql
程序员卷卷狗5 小时前
为什么MySQL默认使用可重复读RR?深入解析binlog与隔离级别的关系
数据库·mysql