【1】故障复盘
基于一套主从的MHA环境,A为现主库,B为现从库。其中
A的uuid是5a56.....7df
B的uuid是6a56.....7df
问题:主库有个事务,在主库故障后,并没有传输到从库,导致主从不一致
(1.1)基于MHA的一主一从环境,演练主库宕机,主备切换
VIP目前在A上,提供给业务使用,模拟主库宕机
systemctl stop mysqld
主库宕机后,观察到VIP正常飘移到B库上,业务正常使用,此时,重启A库,企图将A库重启后重新加入集群,并启动MHA。
启动A库后,将A库作为从库加入到新主B,出现1032报错。
A库信息如下
B库信息如下
发现此刻的从库的GTID_SET 5a开头的那个少了一个事务,也就是说旧主A没有将事务全部同步到旧从B,导致现在B少了一个事务,就切换为新主了。
此刻想法就是先尝试将A库没有同步到B库的事务先拉过去,采用主从的方式,将A库进行stop slave操作, 然后将B库重新指向为A的从库,这样就将B库缺少的事务拉过去了,
再次将B库进行stop slave 操作,将A库 start slave 发现依然报错。
分析:由于B此前未开启read_only,很有可能,A还是主的时候,B上面有写新数据进去。
(1.2)备库只读状态下,演练主库宕机,主备切换
(1)将B全备用于恢复A
(2)将A的read_only打开为ON,禁止写入 set global read_only=on;
(3)重新开启MHA,此时B为主,A为从库
(4)将两个库的配置文件read_only均打开,修改MHA master_ip_failover脚本,切换为主的时候,才将read_only关闭。避免从库写入数据。
注意:此时是B主,A从,测试挂掉主库B
现VIP在B库上,A为从库,测试将B库宕机,VIP飘移到A库。业务正常使用,这个时候,将B库重启
观察此刻A库上的gtid信息如下图
而B库重启后的gtid信息如下
可以看到B库原来是主库,uuid是6a,B的gtid在本地库执行的gno比A多了一个26010,A库上只有26009。
依然出现了新主库比旧主库少了一个事务的情况,并且在修改从库只读的情况下,将挂掉的旧主重新加入主从依然是报错1032。
分析:可能是全备恢复的时候,从库没有进行purge操作,导致gtid_executed表在导入数据的过程中被覆盖。一旦从库再次重启,读取gtid_executed表就会得到错误的gtid_executed变量,导致启动失败。
(1.3)重做备库,将备库gtid_executed可能导致的问题排除
此刻A是主,B是备
(1)停止主从
(2)主库A进行reset master
(3)主库A进行全备,并传输到备库B
(4)将B库reset master
(5)在B库上执行purge操作
(6)在B库上执行reset slave all
(7)重搭主从
CHANGE MASTER TO MASTER_HOST='*******', MASTER_USER='repl', MASTER_PASSWORD='******', MASTER_PORT=3306, MASTER_AUTO_POSITION=1; start slave ;
(8)修复MHA配置文件,重启MHA
此刻A为主库,VIP在主库,提供给业务使用
测试挂掉主库A,此刻A库挂掉,VIP飘移到B上,重启A库,试图重新加入到主从,报错
分析:A库依然比B库多了一个事务,且报错也是1032,解析原当前主库B库该位点的binlog,是一条update信息,而该表在A库中是空表,所以报错:1032错误要更新的数据不存在
查看从A库dump出来的备份文件,查看该表是有在备份时候有记录,发现备份出来的时候是有的
解析B库的所有binlog信息,并没有发现有对该表进行drop ,detete,或者truncate 操作。
可以大概猜得到应该是主库A备份出来的时候数据还在,而主库A挂之前,该表被清空的操作,并没有同步到从库。所以出现也gtid比新主库多了一个事务,且主从1032的错误,因为新主对该表的update操作无法在新从,也就是旧主A上执行。
验证:
解析A库binlog可以看到
(1.4)尝试增强半同步的方式
安装半同步的插件
-- 主库 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; show plugins; -- 从库 INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; show plugins; 查看是否安装成功 -- 半同步需要在主从同时开启 -- 主库 SET GLOBAL rpl_semi_sync_master_enabled = 1; -- 从库 SET GLOBAL rpl_semi_sync_slave_enabled = 1; -- 以上的启动方式是在命令行操作,也可写在配置文件中。 -- 重启从库的IO线程 STOP SLAVE IO_THREAD; START SLAVE IO_THREAD; -- 查看是否开启半同步复制 mysql> show status like 'Rpl_semi_sync_master_status'; +-----------------------------+-------+ | Variable_name | Value | +-----------------------------+-------+ | Rpl_semi_sync_master_status | ON | +-----------------------------+-------+ mysql> show status like 'Rpl_semi_sync_slave_status'; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | Rpl_semi_sync_slave_status | ON | +----------------------------+-------+ -- 配置文件 #plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so" rpl_semi_sync_slave_enabled = 1 rpl_semi_sync_master_enabled = 1 rpl_semi_sync_master_wait_point = after_sync rpl_semi_sync_master_timeout =10000 rpl_semi_sync_master_wait_no_slave=0
依然没有得到解决
(1.5)复盘结论
主库最后一个事务truncate操作没有同步到从库。
主库异常宕机挂掉,truncate没有同步到从库,而当主库重启,被作为新从库,重新加入到主从中去的时候,由于该表数据已经被truncate掉,新主中对该表数据的update
操作同步过来的时候就会报错1032,找不到要更新的数据,主从异常。为了避免主从不一致,尝试改为增强半同步,依然没有解决该问题。
【2】主从不一致分析
(2.1)主从复制
master事务的提交不需要经过slave的确认,slave是否接收到master的binlog,master并不care。slave接收到master
binlog后先写relay log,最后异步地去执行relay log中的sql应用到自身。由于master的提交不需要确保slave
relay log是否被正确接受,当slave接受master binlog失败或者relay log应用失败,master无法感知。
异步复制本身对于数据一致性不做保证
(2.2)半同步复制
基于传统异步存在的缺陷,mysql在5.7版本推出增强半同步复制。可以说半同步复制是传统异步复制的改进,在master事务的commit之前,必须确保一个slave收到relay
log并且响应给master后(从库收到并产生 relaylog 后会向主库发送一个 ACK 的信息包,当主库获得这个包后,认为从库已经获得 relaylog)才能进行事务的commit。但是slave对于relay log的应用仍然是异步进行的。
(2.3)半同步之-----AFTER_COMMIT 方式
MYSQL5.7 之前半同步复制采用的是 AFTER_COMMIT 方式--比 AFTER_SYNC 会有更大概率造成数据不一致
AFTER_COMMIT 是先做 REDO COMMIT 后传 BINLOG,做事务提交,只是不给客户端返回。
AFTER_COMMIT(5.6默认值)
master将每个事务写入binlog ,传递到slave 刷新到磁盘(relay log),同时主库提交事务。
master等待slave 反馈收到relay log,只有收到ACK后master才将commit OK结果反馈给
客户端。
实际上,主库在等待ACK的InnoDB存储引擎内部已经提交事务,
只是阻塞了返回给发起事务提交的客户端消息而已。 该缺陷可能导致非发起数据提交的客户端在碰到主库故障转移时发生幻读。
commitTrx的调用在engine层commit之后(在ordered_commit函数中process_after_commit_stage_queue调用),如上图所示。即在等待Slave
ACK时候,虽然没有返回当前客户端,但事务已经提交,其他客户端会读取到已提交事务。如果Slave端还没有读到该事务的events,同时主库发生了crash,然后切换到备库。那么之前读到的事务就不见了,出现了幻读。
测试:
(1)从库上停掉IO_THREAD模拟从库异常 stop replica io_thread; (2)主库上插入一条数据,此时会HANG住(但是这条数据已经写入了,开启一个会话是可以查到该数据的) insert into t1 values(3); (3)开启新SESSION查询T表 select * from t1; (4)返回1,2,3 (5)开启另一个会话杀掉主库MYSQLD进程pkill -9 mysqld -- 此时从库中是查不到插入3这条数据的。 select * from t; 返回1,2
如果此时发生主从切换则主从数据发生不一致。这也是after_commit模式复制中幻读现象。
如图:
(2.4)半同步之----AFTER_SYNC方式
AFTER_SYNC 是先传 binlog 后做 REDO COMMIT;
master 将每个事务写入binlog , 传递到slave 刷新到磁盘(relay log)。
master 等待slave 反馈接收到relay log的ack之后,再提交事务并且返回commit OK
结果给客户端。
即使主库crash,所有在主库上已经提交的事务都能保证
已经同步到slave的relay log中。
(2.5)主从复制不一致的两种极端情况情况(幽灵事务)
极端有两种情况:
1. 当主库还没来得及把日志传输到从库上;主库上在完成write binlog后crash
2.日志已经传输到从库上,完成了wait slave ack,此时发生crash;应用端此时并没有接收到主库返回OK。
情况1. 主库Crash恢复后,这个事务操作数据可以被commit,这种事务可以称为local commit或是幽灵事务,并没有真正的完成半同步。
情况2. 产生脏数据,是一个业务没得到确认的事务。也可以称为幽灵事务。
在after_sync中,确实不会丢数据了,但有可能会多数据。
这种架构下复制故障:1062,1032类的错误,会发生在第一种情况,原始的master故障恢复后,做为新master的从。
1062错误很容易出现,但1032错误可能被MySQL本身掩盖了。
所以对于after_sync复制,最好的做法是原始主库故障后,可以对比一下最后一个GTID事务的内容或是直接自动化重建(云上的方案)
【3】sync_binlog 对主从的影响
参数值含义
sync_binlog= 0/1/n 0:表示每次提交事务都只 write,不 fsync,每过一秒fsync到磁盘,每一秒刷一次磁盘 1:表示每次事务提交都刷一次磁盘,也就是每次提交事务都会执行fsync n:(100 200 500)表示每次提交事务都 write到OS cache,但累积 N 个事务后才 fsync到磁盘
(3.1)binlog传输给备库的时机
sync_binlog=1的时候,binlog是需要主库binlog sync到磁盘后才会传输给备库。
sync_binlog!=1的情况下,主库的binlog传输到备库的event是write之后就会传过去。
注:这里的binlog write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,binlog落盘只的是 fsync,才是将数据持久化到磁盘的操作。
(3.2)sync_binlog=1分析
上面的案例中,sync_binlog已经配置为1,在这种设置下,主库是需要binlog sync到磁盘后才传输给备库。如图,主库binlog落盘后,传输给备库,等待备库返回ACK。而在这个时候发生了主库的crash。
会出现两种情况:
(1)当主库还没来得及把日志传输到从库上;主库上在完成write binlog后crash
主库Crash恢复后,这个事务操作数据可以被commit,这种事务可以称为local commit或是幽灵事务,并没有真正的完成半同步。就会出现上面所述案例中主库比从库多事务的情况。
这种情况下,原始的master故障恢复后,作为新master的从,1062错误很容易出现,因为主库有事务没有同步到从库,而新主写入很有可能与这个事务冲突。1032错误,就是我们遇到的这个,truncate后空表数据无法殴update操作。
所以对于after_sync复制,最好的做法是原始主库故障后,可以对比一下最后一个GTID事务的内容
(2)日志已经传输到从库上,完成了wait slave ack,此时发生crash;应用端此时并没有接收到主库返回OK。
产生脏数据,是一个业务没得到确认的事务。也可以称为幽灵事务。
(3.3)sync_binlog!=1分析
主机crash
主库所在主机crash后,可能导致主库比备库少一些gtid。在sync_binlog不等于1的情况下,在binlog还没有sync到磁盘的时候,binlog event被同步到了从库上。
binlog在写文件时先write,再sync。假设主库在write binlog之后,sync
之前,同时备库也拉取了这些未sync的binlog。此时主库宕机,主库一部分 binlog
未落盘,但这部分binlog已经传到了备库,那么备库会比主库多一些事务。因此主库重启后,重新构造 gtid_executed_set
时会比备库少一些gtid。
那些未sync的事务实际处于两阶段提交的prepare状态,重启后这些处于prepare的事务由于没有写binlog会回滚掉。
主机宕机HA切换后,新主库会比新备库多一些事务。
而实际上新主库会比新备库多一些事务应该没有影响,这些事务是用户发出了commit命令,但主机crash了,没有收到commit的回复,处于未知状态。这些未决事务可以提交也可以回滚!
对于以上情况,在binlog没有purge的情况下,结合应用我们可以根据gtid来修复主备不一致的情况,或回滚备库的修改,或者重做主库丢失的事务。
【总结】:如何避免主从不一致?
那么使用复制如何保证数据的绝对一致性呢?
1.复制一定是binlog row格式+gtid,同时在数据库故障时,注意local commit问题,引入数据校验机制。
2. 复制环境绝对一致性属于伪命题,如果想要绝对的一致目前可以考虑MySQL Group Replication。
3. 如果一定要用复制架构,同时又要绝对的一致性,考虑使用增强半同步after_sync结合session_track_gtids功能使用。
4. 复制推荐使用after_sync,同样要求半同步不允许退化成为异步。
5. 深入理解复制的原理,避免不适当的操作造成复制一致性: 大事务,较长DDL等操作。如果必须操作,可以考虑一些特殊的运维方式操作。
-----------------------------------
【参考文档】
©著作权归作者所有:来自51CTO博客作者进击的CJR的原创作品,请联系作者获取转载授权,否则将追究法律责任
真实案例:主备切换数据不一致分析