草庐IT

真实案例:主备切换数据不一致分析

进击的CJR 2023-03-28 原文

故障复盘

基于一套主从的MHA环境,A为现主库,B为现从库。其中

    A的uuid是5a56.....7df

    B的uuid是6a56.....7df



(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上面有写新数据进去。


(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变量,导致启动失败。


(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可以看到


(4)尝试增强半同步的方式

安装半同步的插件
主库
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
从库
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
依然没有得到解决



复盘结论

    主库最后一个事务truncate操作没有同步到从库。 


主库异常宕机挂掉,truncate没有同步到从库,而当主库重启,被作为新从库,重新加入到主从中去的时候,由于该表数据已经被truncate掉,新主中对该表数据的update

操作同步过来的时候就会报错1032,找不到要更新的数据,主从异常。为了避免主从不一致,尝试改为增强半同步,依然没有解决该问题。


主从不一致分析


主从复制

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无法感知。

异步复制本身对于数据一致性不做保证


半同步复制

基于传统异步存在的缺陷,mysql在5.7版本推出增强半同步复制。可以说半同步复制是传统异步复制的改进,在master事务的commit之前,必须确保一个slave收到relay

log并且响应给master后(从库收到并产生 relaylog 后会向主库发送一个 ACK 的信息包,当主库获得这个包后,认为从库已经获得 relaylog)才能进行事务的commit。但是slave对于relay log的应用仍然是异步进行的。


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,然后切换到备库。那么之前读到的事务就不见了,出现了幻读。


测试:

从库上停掉IO_THREAD模拟从库异常
stop replica io_thread;

主库上插入一条数据,此时会HANG住(但是这条数据已经写入了,开启一个会话是可以查到该数据的)
insert into t1 values(3);
开启新SESSION查询T表
select * from t1;
返回1,2,3
开启另一个会话杀掉主库MYSQLD进程pkill -9 mysqld
此时从库中是查不到插入3这条数据的。

select * from t;
返回1,2
如果此时发生主从切换则主从数据发生不一致。这也是after_commit模式复制中幻读现象。 如图:





AFTER_SYNC方式


AFTER_SYNC 是先传 binlog 后做 REDO COMMITmaster 将每个事务写入binlog , 传递到slave 刷新到磁盘(relay log)。master

等待slave 反馈接收到relay log的ack之后,再提交事务并且返回commit OK

结果给客户端。

即使主库crash,所有在主库上已经提交的事务都能保证

已经同步到slave的relay log中。



sync_binlog对主备的影响


参数值含义

sync_binlog= 0/1/n
0:表示每次提交事务都只 write,不 fsync,每过一秒fsync到磁盘,每一秒刷一次磁盘
1:表示每次事务提交都刷一次磁盘,也就是每次提交事务都会执行fsync
n:(100 200 500)表示每次提交事务都 write到OS cache,但累积 N 个事务后才 fsync到磁盘

binlog传输给备库的时机

主备复制开启的流程
1、在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。

2、在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。

3、主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。

4、备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。

5、sql_thread 读取中转日志,解析出日志里的命令,并执行。


重点思考
需要注意的是,第3步,这里说的主库从本地读取binlog,发给B,这里是读取的page cache还是disk里面的呢?

对于A的线程来说,就是“读文件”
1. 如果这个文件现在还在 page cache中,那就最好了,直接读走;
2. 如果不在page cache里,就只好去磁盘读。
这个行为是文件系统控制的,MySQL只是执行“读文件”这个操作
sync_binlog=1的时候,表示每次事务提交都刷一次磁盘,也就是每次提交事务都会执行fsync,fsync其实很快的。可以理解为传给备库的binlog都是落盘的。

sync_binlog!=1的情况下,主库的binlog传输到备库的event是write之后就会传过去,其实也就是主库读取os cache中的binlog event将其传输到备库。

注:这里的binlog write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,binlog落盘只的是 fsync,才是将数据持久化到磁盘的操作。



sync_binlog=1分析

上面的案例中,sync_binlog已经配置为1,在这种设置下,主库传给备库的binlog都是落盘的。如图,主库binlog wtrite后立即fsync落盘,传输给备库,等待备库返回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。


产生脏数据,是一个业务没得到确认的事务。也可以称为幽灵事务。




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等操作。如果必须操作,可以考虑一些特殊的运维方式操作。







有关真实案例:主备切换数据不一致分析的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby-on-rails - Ruby on Rails with Haml - 如何从 erb 切换 - 2

    我正在从erb文件切换到HAML。我将hamlgem添加到我的系统中。我创建了app/views/layouts/application.html.haml文件。我应该只删除application.html.erb文件吗?此外,仍然有/public/index.html文件被呈现为默认页面。我想创建自己的默认index.html.haml页面。我应该把它放在哪里以及如何使系统呈现该文件而不是默认索引文件?谢谢! 最佳答案 是的,您可以删除任何已转换为HAML的View的ERB版本。至于你的另一个问题,删除public/index/h

  3. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  4. ruby - 如何使用 CarrierWave 从 S3 获取真实文件 - 2

    我有一个应用程序可以读取文件的内容并为其编制索引。我将它们存储在磁盘本身中,但现在我使用的是AmazonS3,因此以下方法不再适用。事情是这样的:defperform(docId)@document=Document.find(docId)if@document.file?#Youshould'tcreateanewversion@document.versionlessdo|doc|@document.file_content=Cloudoc::Extractor.new.extract(@document.file.file)@document.saveendendend@docu

  5. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  9. 「Python|Selenium|场景案例」如何定位iframe中的元素? - 2

    本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决

  10. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

随机推荐