当前位置: 首页 > 科技观察

99%的人不知道MySQL主从复制带来的问题

时间:2023-03-12 09:57:50 科技观察

1.后台电商业务场景。随着平台订单规模的不断扩大,现有的订单存储已经无法支撑后续业务的开展。在做德屋彩石项目的时候,订单是分库分表的。为了解决分库分表后卖家维度的查询问题,单独创建了卖家维度的订单库。目前,订单分为买家和卖家两个数据库。卖家数据库中的数据是通过监控买家数据库的binlog得到的异构数据库。现在订单主要有两个表,分别是订单的主表和子表。在异构逻辑中,我们将对这两个表的binlog消息进行处理,异构形成我们的卖家订单表。监听插入消息时,只会处理分表的插入消息,其余需要补充的主订单表数据直接查询主表。查询主订单表时,如果为空,会抛出异常,依赖MQ的重试功能进行下一次重试。这是现在的逻辑。一般情况下,是不可能找到主订单表的。19日凌晨,主订单表出现大量无法查询的告警。于是通过关键字搜索日志平台,如下图所示:2、分析2.1业务影响通过查看报错信息,第二次重投报文时数据成功存入数据库,具有对业务无影响,但会有异常告警。2.2主从延迟目前的现象是第一次处理时,查询主表为空,异常重试后才能查询到数据。我们的直觉是数据库的主从延迟导致的问题。按照这个思路,我们去查看对应的代码,发现主要的库查询是从代码层面拿的。顺带介绍一下主从路由是如何实现的。项目依赖数据库代理中间件RainbowBridge的jar包,支持通过配置指定默认的读写分离模式(bifrost.read-write-separate-model=SQL_READ_DEFAULT_MASTER)。我们的模式默认路由到主库。如果有需要从从库中查询的场景,会在相应的dao方法中添加注解进行标记。然后通过Mybatis的拦截器处理SQL,通过hint方法将路由方法带到RainbowBridge,RainbowBridge的内部路由按照指定的方法进行。2.3消除了创建单个数据一致性的主从延迟之后,疑惑还存在于另一个方面。会不会在创建订单的时候在同一个事务中保存数据,比如先保存子订单,再保存主订单。这时候子订单的binlog肯定会早于主订单,会有查询主订单空的情况。这也很快被否决了。如果真是这样,那一定是百分百不可避免的场景,而不是偶然的情况。其次,订单创建代码中的数据存储在一笔交易中,所以没有批量保存。2.4数据库内部问题排除外部相关问题,则只剩下数据库内部问题。接下来看看binlog写入过程有没有问题。首先我们看一下整体的流程,如下图所示:这里比较可疑的一点是,在第二阶段提交redolog的时候,此时redolog会被flush到数据盘,也就是MySQL的存储数据实际上是存储在数据库中的。一般情况下,这个速度是很快的。当数据库的IO很高时,磁盘刷写的性能也会受到影响。所以,如果刷盘的时候稍微有X毫秒的延迟,说明binlog已经被应用消费了,就无法查询了。根据报警的时间点,去看了从节点数据库的监控。那段时间IO确实很高,如下图:以上只是猜测。我们如何验证这个猜测是正确的呢?目前使用的数据库是主从模式,所以必然涉及到数据复制。简单介绍几种复制模式:强同步应用发起数据insert/update/delete操作,master实例执行完毕后同步传输日志,直到至少有一个standby实例接收到并提交事务才会提交存储日志。半同步应用发起数据insert/update/delete操作,主实例执行完成后,将日志同步传输到备实例。备实例收到日志后,即使提交了事务,也不需要等待备实例执行完日志内容。异步应用发起数据插入/更新/删除请求,主实例完成操作后立即响应应用,同时主实例异步复制数据到备实例。目前我们的数据库采用的是半同步的方式。半同步支持两种模式,分别是AFTER_COMMIT(5.6版本默认)和AFTER_SYNC(5.7版本才有,默认)。1)AFTER_COMMITmaster将每个事务写入binlog,传给slave刷新到磁盘,master同时提交事务。master等待slave反馈收到relaylog,master收到ACK后才向client反馈commitOK结果。AFTER_COMMIT表示在master上,刚刚提交的事务对数据库的修改对其他事务可见。因此,如果在等待SlaveACK的过程中发生crash,会导致其他事务发生幻读和数据丢失。2)AFTER_SYNCmaster将每个事务写入binlog,传给slave刷新到磁盘。master等待slave反馈收到relaylog的ACK,然后提交事务,返回commitOK结果给client。即使master挂了,也可以保证master上提交的所有事务都已经同步到slave的relaylog中。AFTER_SYNC写入binlog后开始传输,但是事务还没有提交,也就是说当前事务对数据库的修改对其他事务是不可见的。因此,不会出现幻读,也没有数据丢失的风险。我们目前使用的是AFTER_SYNC模式,也就是说,binlog写入后,master会等待slave的反馈,然后再commit。这也可以解释我们未查询问题的原因。原因是master写完binlog后在等待。slave收到binlog后,由于IO高,写入relaylog比较慢。这时候我们的订阅平台也相当于一个从节点。也收到了binlog,下发给Application,application这时候去数据库查询,因为master还没有commit,自然查不到。3.解决方案对于这个问题,有很多解决方案。整理后发现这个场景不影响业务。调整告警信息不需要修改业务。如果你想改变,有一些解决方案可以解决。3.1重试机制对于这样的场景,可以使用重试机制来解决问题。目前的binlog监控也是使用MQ向业务端传递消息,可以直接依赖MQ的重试。这个业务场景本身就是一个异步过程,对实时性要求没有那么高,二来不会影响业务。消息第一次来的时候,没找到,就终止了流程,然后MQ重试,就能找到数据,整个流程就结束了。3.2延迟消息可以改造订阅平台。配置订阅任务时,可以指定监听binlog后延迟多长时间发送给业务应用。这个延迟处理直接使用了MQ的延迟消息,让消息延迟几秒。发给业务应用也能解决问题。延迟消息不能保证得到解决。关键是如何合理设置延迟时间。因为我们无法保证数据库在收到节点的binlog后,需要多长时间才能ack。另外,如果配置很长,需要评估现有场景的业务,看是否可以接受数据延迟。3.3不依赖binlog不依赖binlog是指将消息改为触发业务动作后发送的消息。比如创建订单后,在代码中通过MQ发送一条订单创建消息,消息中包含订单的数据。这样,业务应用不直接依赖binlog消息。当业务消息被监控时,交易必须已经提交,并且当业务应用程序检查回来时数据已经可用。4.总结数据库可以说是作为研发生必须掌握的技能,但是在日常工作中,我们只需要掌握一些基本的语法就可以满足开发的需要,所以很多底层的原理都会被忽略。通过本文的案例,你会发现整个数据库系统还有很多需要学习的地方。试想一下,如果熟悉主从复制的原理,排查问题就会容易很多。其次,作为研发,对于每一个问题都要有认真的态度,不能操之过急。从一个小问题开始,如果忽略它,就会失去求真的动力,从而失去很多学习点。这个问题也是如此。通过逐步调查,积累了新的经验。