日常Bug排查-改表时读数据不一致

bug · 浏览次数 : 0

小编点评

# 前言 日常Bug排查系列文章将介绍一些简单Bug的排查技巧,并积累素材。本文将介绍一个实际案例中的Bug排查过程。 # 现场问题 某天,线上服务出现了一个NP异常,问题出现在凌晨低峰期。经过分析,发现问题出现在一个业务请求上,而且这个问题在每天的凌晨都会出现。通过查看时间线,发现每天凌晨都会进行改表操作,而这个表正是出现NP异常的表。 ## 业务场景 A表是主表,B表是子表,两者都需要在一个事务内一块插入和更新。在这个表上,我们发现在一个事务内查询时,能查到A表的数据,但是查不到B表的数据。 ## 数据库特性 数据库的一个核心特性是原子性。在这个场景中,这个特性被破坏了。但是其他时间没有出现类似的错误。因此,我们可以推测这个表的操作会短暂地破坏原子性。 ## 问题定位 通过研究Ghost改表的原理,我们发现最后一步Rename操作才会对当前的SQL产生影响。我们可以推断,最后一步Rename操作可能会导致读数据不一致。 查看DBA的改表日志,我们发现Rename操作的时刻与NP异常出现的时刻完全吻合。这证实了我们的推测。 ## 原因分析 最终导致问题的原因是:BNew新表通过Ghost的binlog重放将原B表中相关的binLog重放到BNew表中。但是在事务T2开始的时候,BNew这张表中新纪录B还没有被重放。在事务T2开始的时候首先查询了A表建立了MVCC视图,这时候的数据库实际快照就是A表有A,BNew表没有B。尽管在Rename表的时候MySQL会对B和BNew都进行锁表,这时候所有对于这两张表的访问都会等锁表的结束。但是由于RR的原因,这个事务内后续读BNew表的时候始终就是A表有A,BNew表没有B这样的现象。在后续的查询中select B查询的实际上是BNew表,进而产生了数据不一致,进而导致了NullPointerException。 ## 测试验证 为了验证我们的推理,我们在线下设计相关实验复现问题。我们模拟了事务顺序,新建一张BNew表然后Rename下,发现现象并不一致。在Rename的时候,模拟的请求2在做select新B表的时候始终会出现Table definition has changed,please retry transaction这个报错。 查阅MySQL源代码,我们发现要让Rename不报错,必须在模拟的请求2事务开始之前就创建这个BNew表。否则请求2在查询BNew表的时候就会由于找不到UndoHistory导致报错。 # 总结 线上环境是错综复杂的,改表等运维操作也会导致出现意料之外的结果。很多组件的特性在一些特殊的情况下会被打破,所以防御式编程就显得尤其重要。在实际工作中,我们需要积累经验,提高自己的技术能力,以便更好地应对这类问题。

正文

前言

日常Bug排查系列都是一些简单Bug的排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材。

Bug现场

线上连续两天出现NP异常,而且都是凌晨低峰期才出现,在凌晨的流量远没有白天高峰期大。而出问题的接口又是通常的业务请求。于是,很自然的,我们就想凌晨有什么特殊的运维动作,翻了下时间线。发现,每天凌晨都会进行改表,而修改的这张表恰好就是出现NP异常的表。如下图所示:

在此解释下业务的相关场景。A表是主表,B表是子表,两者都是严格保证在一个事务内一块插入和更新的,在该表时刻确出现了在一个事务内查询,能查到A确查不到B的现象。

思路

数据库的一个核心特性就是原子性,看上去这个场景破坏了原子性。但是由于是和改表强相关,其它时间没有类似错误。那么很明显的,思路就会指向该表这个动作会短暂的破坏原子性。由于线上使用的ghost进行改表,于是笔者就看了下ghost改表的原理:

ghost会创建一个影子表,在影子表上完成alter改表,然后分批将全量数据应用到新表。
同时在处理增量数据的时候,通过解析binLog事件,将任务期间的新增数据应用到新表。
最后一步,通过Rename语句使新表替换老表

从这个原理中可以推断,最后一步Rename的时候才会对当前的SQL产生影响,是不是刚好这个这个Rename操作短暂的使读数据不一致了呢?看了下DBA那边的改表日志,发现Rename那个时刻和NP异常出现时刻完全吻合。看来它就是罪魁祸首了。

为什么Rename会导致读数据不一致?

笔者稍加思索就明白了原因。首先,线上库的隔离级别是RR的,也就是可重复读。而Alter表的时候势必会有一张旧表B和新表BNew。业务的事务保证是操作在A和B上的,而读数据不一致应该是A和BNew上,所以无法保证A和BNew的一致。只能通过binLog的重放保证最终一致。 那么最终导致问题的原因就很明显了,如下所示:

BNew新表通过ghost的binlog重放将原B表中相关的binLog重放到BNew表中。但是在事务T2开始的时候BNew这张表中新纪录B还没有被重放。在事务T2开始的时候首先查询了A表建立了MVCC视图,这时候的数据库实际快照就是A表有A,BNew表没有B。尽管在Rename表的时候MySQL会对B和BNew都进行锁表,这时候所有对于这两张表的访问都会等锁表的结束。但是由于RR的原因,这个事务内后续读BNew表的时候始终就是A表有A,BNew表没有B这样的现象。在后续的查询中select B查询的实际上是BNew表,进而产生了数据不一致,进而导致了NullPointerException。

测试复现实验的一个小问题

还有一个小问题,就是笔者在线下设计相关实验复现问题的时候。这个复现的实验看上去是比较容易的,模拟一下事务顺序,新建一张BNew表然后Rename下,看看现象是否一致就可以了,如下图所示:

但笔者发现,在Rename的时候,模拟的请求2在做select 新B表的时候始终会出现

Table definition has changed,please retry transaction

这个报错。于是笔者看了下MySQL的源代码,要想让Rename不报错,必须在模拟的请求2事务开始之前就创建这个BNew表,否则请求2在查询BNew表的时候就会由于找不到UndoHistory导致报错。MySQL源代码如下所示:

row_vers_old_has_index_entry(......){

  ......
  /* If purge can't see the record then we can't rely on
  the UNDO log record. */

  bool purge_sees = trx_undo_prev_version_build(
   rec, mtr, version, index, *offsets, heap,
   &prev_version, NULL, vrow, 0);
  // 在这边,如果找不到这张表在t1前的undo history的话,则会报"Table definition has changed, please retry transaction"
  err  = (purge_sees) ? DB_SUCCESS : DB_MISSING_HISTORY;

  if (prev_heap != NULL) {
   mem_heap_free(prev_heap);
  }
    ......
}

总结

线上环境是错综复杂的,改表等运维操作也会导致出现意料之外的结果,很多组件的特性在一些特殊的情况下会被打破,所以防御式编程就显得尤其重要了。

与日常Bug排查-改表时读数据不一致相似的内容:

日常Bug排查-改表时读数据不一致

前言 日常Bug排查系列都是一些简单Bug的排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材。 Bug现场 线上连续两天出现NP异常,而且都是凌晨低峰期才出现,在凌晨的流量远没有白天高峰期大。而出问题的接口又是通常的业务请求。于是,很自然的,我们就想凌晨有什么特殊的运维动作,翻了下时

日常Bug排查-MVCC和for update混用导致读数据不一致

日常Bug排查-MVCC和for update混用导致读数据不一致 前言 日常Bug排查系列都是一些简单Bug的排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材。 Bug现场 又是喜闻乐见的读数据不一致的问题。这次的问题是这样,业务在一个事务中更新A和B两个表的两个数据。但是在另一个

日常Bug排查-偶发性读数据不一致

日常Bug排查-偶发性读数据不一致 前言 日常Bug排查系列都是一些简单Bug的排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材。 Bug现场 业务场景 先描述这个问题出现的业务场景。这是一个支付的场景,如果支付成功了,我们就把支付状态置为success(主单据更新)同时写入支付成功

日常Bug排查-连接突然全部关闭

日常Bug排查-连接突然全部关闭 前言 日常Bug排查系列都是一些简单Bug的排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材。 Bug现场 最近碰到一个问题,一台机器上的连接数在达到一定连接数(大概4.5W)连接数之后会突然急速下降到几百。在应用上的表现就是大量的连接报错,系统失去

日常Bug排查-读从库没有原子性?

日常Bug排查系列都是一些简单Bug排查。问题虽小,但经常遇到,了解这些问题,会让我们少走点弯路,提升效率。说不定有些问题你遇到过哦:) Bug现场 业务开发同学突然问了笔者一个问题,从库读会不会没有原子性?我下意识的反应怎么可能,只要是遵守MySQL主从Replication协议的原子性至少是能够

如何实现简单的分布式链路功能?

为什么需要链路跟踪 为什么需要链路跟踪?微服务环境下,服务之间相互调用,可能存在 A->B->C->D->C 这种复杂的服务交互,那么需要一种方法可以将一次请求链路完整记录下来,否则排查问题不好下手、请求日志也无法完整串起来。 如何实现链路跟踪 假设我们从用户请求接口开始,每次请求需要有唯一的请求

debug技巧之远程调试

一、前言 大家好啊,我是summo,今天给大家分享一下我平时是怎么调试代码的,不是权威也不是教学,就是简单分享一下,如果大家还有更好的调试方式也可以多多交流哦。 当我们的应用发布到线上之后,就不能随意启停了,但如果线上出现了BUG怎么办呢?大多数时候我们会借助线上打印的日志进行排查问题,如果幸运的话

《最新出炉》系列入门篇-Python+Playwright自动化测试-42-强大的可视化追踪利器Trace Viewer

1.简介 在我们日常执行自动化测试工作的过程中,经常会遇到一些偶发性的bug,但是因为bug是偶发性的,我们不一定每次执行都能复现,所以我们在测试执行的时候,追踪用例执行就变得非常重要了。playwright提供了一个Playwright Trace Viewer工具来追踪测试执行,这是一个GUI工

FastJson不成想还有个版本2啊:序列化大字符串报错

# 背景 发现陷入了一个怪圈,写文章的话,感觉只有大bug或比较值得写的内容才会写,每次一写就是几千字,争取写得透彻一些,但这样,我也挺费时间,读者也未必有这么多时间看。 我想着,日常遇到的小bug、平时工作中的一些小的心得体会,都还是可以写写,这样也才是最贴近咱们作为一线开发生活的,也不必非得是个

测试的底层逻辑

写这篇文章,是希望把我的一些我认为是非常有价值的经验总结出来,能够帮助刚做测试不久的新同事,或者是测试经验丰富的老同事以共享。希望我们可爱的新同事,准备要在测试领域耕耘的伙伴,能够通过我的文章了解到测试的底层逻辑,也就是我们测试工作中可能看不到隐藏较深的点,而不只是日常所见的写用例、提bug、开发自动化、做平台;俗话说外行看热闹,内行看门道。