近日接到一个故障,主从异步方式,主 crash后,从不可用,检查发现从机Read_Master_Log_Pos与Exec_Master_Log_Pos不一致,似乎还有binlog在回放中,HA在等回放结束,一直保持这个状态。难道从机也出故障了?根本原因是什么?且看下文。

MySQL binlog简介

    首先简单了解要下binlog日志,Binary Log是在MySQL3.23.14中引入的,记录MySQL数据修改记录的文件集合。

Binary Log有两个目的:

  1. 用于复制。master(主)上的binlog日志发送到slave(从)上,slave执行回放master上的修改动作,保持主从数据一致。
  2. 用于数据恢复。对binlog日志文件指定始末位置执行即恢复指定数据。
    本文是针对复制流程中的一个改进,不涉及数据恢复。

基于binlog的复制简介

    Master上对修改动作生成binlog,Slave机IO拉取对应的binlog到本地生成relay log,然后Slave机SQL线程回放执行。binlog的最小单位为event,master与slave之间以**event为单位**传输日志,一个或多个event组成一个事务,slave机上以**事务为单位**回放日志。事务是数据库的常见基本特性,不再介绍,这里主要介绍下binlog中的event。
    事务event包含header 和data,Header包含有event的类型,时间,哪个server产生等信息。data有对应类型event的细节,如特定的数据变化。
    每个binlog第一个event是描述性event,描述当前文件的格式版本,最后一个event是一个log-rotation event用来指定下一个binary log的文件名。中间的则为常规event,描述各种操作,从event类型上就能比较直观看出event内容,如:XID\_EVENT、WRITE\_ROWS\_EVENT、UPDATE\_ROWS\_EVENT、DELETE\_ROWS\_EVENT等。
     一个示例如下:

    这是GTID模式下,建表和插入数据等events示例。第一个是固定的当前文件头信息,第二个Previous\_gtids说明是GTID日志模式,接下来的Gtid event是GTID模式下每个事务的头信息,通过这个不重复且递增的GTID号来保证数据的连续性,一致性,然后是建表并插入数据相关event。

异步复制模式的不足

    上面可以看到,binlog中非常清析的记录了所有动作,正常情况下,slave机只要按这顺序执行完成,就能保证主从数据一致。但事务当提交成功后才发日志给slave机,当master出现故障时,slave机收到的日志不一定是完整的,这时没办法完全保证主从数据完全一致,这是异步模式天生的不足,是否有好的解决办法本文不深入,本文要讨论的是当此类故障出现时如何保证外部业务可用的问题。
    假如master机掉电了或crash了或操作系统崩溃了,无法恢复使用,此时怎么恢复?先看如下简图:

    当master出现问题时,业务将不可用,slave机接收不到binlog,IO线程会处于连接中,HA控制中心确认状态后,会自动把应用流量切到slave机,恢复业务。但是有些情况下,HA控制中心是没办法自我恢复的。如:master在commit之后,开始给slave机传输binlog时,但又没传完,此时master 出现故障,HA需要切换流量到slave,但在切换流量之前,会先等slave机上已经读到的binlog回放完毕。问题就在这里,请看我上面第二节基于binlog的复制简介中加粗字体**event为单位**和**事务为单位**,在slave机有Exec\_Master\_Log\_Pos记录当前已经执行完成的以事务为单位的日志位置,有Read\_Master\_Log\_Pos记录前已经拉取过来的以event为单位日志位置,当Read\_Master\_Log\_Pos读位置与Exec\_Master\_Log\_Pos执行位置相等就说明slave机上日志已经回放完毕,如果IO线程拉取一个事务的部分event,此时master出现故障不能恢复,slave机会一直等待,此时Read\_Master\_Log\_Pos读位置与Exec\_Master\_Log\_Pos执行位置不一致,HA中心会认为还有日志没回放完,一直等待,**正是这个原因造成了本文开头的HA切换不成功故障**。
    HA为什么要等Read\_Master\_Log\_Pos与Exec\_Master\_Log\_Pos一致?虽然异步模式不能完全保证数据不丢失,但要尽量减小丢失。在master并发很大的场景下,主从数据延迟可能会是几十分钟甚至更久,必须要把已经拉取日志回放完毕,减少数据丢失。当出现拉取到不完整事务时,对slave来说是正常状态,可能是网络或其它原因,尝试恢复拉取即可,如果和master通讯恢复正常,slave机是能正常拉取到完整事务的,因此不完整事务状态对slave机说是正常状态。如果master已经不可用,slave机又没拉取完全部事务,Read\_Master\_Log\_Pos与Exec\_Master\_Log\_Pos不一致,HA中心认为回放不完整,不能切换,一直等待。此时甚至DBA上去查看也不好确认slave机状态,如slave机正在回放一个超大事务,需要很长时间,期间master出现故障不可用,此时Read\_Master\_Log\_Pos与Exec\_Master\_Log\_Pos不一致,所有状态在回放过程中不发生改变,无法确定slave机真实状态。
    那么当master不可用了,slave机又没拉取完整事务时怎么办?人工检查slave机状态,对比Read\_Master\_Log\_Pos与Exec\_Master\_Log\_Pos,等待一段时间再比较,如果多次比较Exec\_Master\_Log\_Pos没有变化,并且与Read\_Master\_Log\_Pos相差并不大,可以认为已经读取到的日志已经回放完毕,可以把流量切换过来。实际上这不是非常稳妥的方案,操作也需要人工持续观察来做出判断再处理,步骤繁琐,维护成本高,对于云服务厂商,十万甚至百万级别的实例,单点偶尔发小概率事件可能会变成易现大概率事件,需要有更好的处理手段。

Read_Master_Log_Pos更新时机探讨

    出现Exec\_Master\_Log\_Pos和Read\_Master\_Log\_Pos不一致的根本原因就在于两个更新时机不一样,一个事务回放结束才更新,一个拉取到event即更新。对于slave机来说,Exec\_Master\_Log\_Pos要等一个事务的全部event拉取过来并且被sql线程回放成功才能更新,但Read\_Master\_Log\_Pos只要有binlog的event被IO线程拉取过来就会改变。而sql回放线程是根据event中标记事务结束或开始的状态对之前事务进行回放,Read\_Master\_Log\_Pos 记录的是当前已经拉取过来的位置,并不影响回放线程。因此可以把Read\_Master\_Log\_Pos更新时机调整到读到完整事务之后再更新,与Exec\_Master\_Log\_Pos更新逻辑保持一致,这样Read\_Master\_Log\_Pos标记的位置就是肯定可以回放的完整事务的位置,不会出现之前那样不完整事务位置造成无法判断真实状态的情况。两种更新方式对比见下图:

    当以事务为单位更新Read\_Master\_Log\_Pos 后,无论master什么时机点crash,slave机上Exec\_Master\_Log\_Pos最终都能追上Read\_Master\_Log\_Pos。对于已经被slave机拉取过来的最后一个不完整事务的event,处理逻辑也原来一致,不会继续执行。但HA控制中心已经可以根据各个状态自动做出正确处理的,把流量切入从机,判断逻辑也与原来一样,Read\_Master\_Log\_Pos 与Exec\_Master\_Log\_Pos保持相等即可切换,因为现在任何情况下都成达成这个条件。

修改Read_Master_Log_Pos更新位置注意点

    原先逻辑,slave机IO线程并不需要过多解析event,基本上只要确定长度就可,具体内容在sql线程回放时再解析。现在则要在IO线程中确定每个event类型,判断是否是事务结束的event,需要对event做一些解析工作。要修改的点并不多,注意GTID和非GTID区别,考虑row模式和statement差异,对不同情况下事务开始和结束标记理清,在queue\_event函数增加处理逻辑即可。

文章来源于腾讯云开发者社区,点击查看原文