解Bug之路-中间件"SQL重复执行"

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
性能测试 PTS,5000VUM额度
简介:

前言

我们的分库分表中间件在线上运行了两年多,到目前为止还算稳定。在笔者将精力放在处理各种灾难性事件(例如中间件物理机宕机/数据库宕机/网络隔离等突发事件)时。竟然发现还有一些奇怪的corner case。现在就将排查思路写成文章分享出来。

Bug现场

应用拓扑

应用通过中间件连后端多个数据库,sql会根据路由规则路由到指定的节点,如下图所示:

错误现象

应用在做某些数据库操作时,会发现有比较大的概率失败。他们的代码逻辑是这样:

    int count = updateSql(sql1);
    ...
    // 伪代码
    int count = updateSql("update test set value =1 where id in ("100","200") and status = 1;
    if( 0 == count ){
        throw new RuntimeException("更新失败");
    }
    ......
    int count = updateSql(sql3);
    ...

即每做一次update之后都检查下是否更新成功,如果不成功则回滚并抛异常。
在实际测试的过程中,发现经常报错,更新为0。而实际那条sql确实是可以更新到的(即报错回滚后,我们手动执行sql可以执行并update count>0)。

中间件日志

笔者根据sql去中间件日志里面搜索。发现了非常奇怪的结果,日志如下:

2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>0;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24266;
2020-03-13 11:21:01:440 [NIOREACTOR-20-RW] frontIP=>ip1;sqlID=>12345678;rows=>2;sql=>update test set value =1 where id in ("1","2") and status = 1;start=>11:21:01:403;time=>24591;

由于中间件对每条sql都标识了唯一的一个sqlID,在日志表现看来就好像sql执行了两遍!由于sql中有一个in,很容易想到是否被拆成了两条执行了。如下图所示:

这条思路很快被笔者否决了,因为笔者explain并手动执行了一下,这条sql确实只路由到了一个节点。真正完全否决掉这条思路的是笔者在日志里面还发现,同样的SQL会打印三遍!即看上去像执行了三次,这就和仅仅只in了两个id的sql在思路上相矛盾了。

数据库日志

那到底数据真正执行了多少条呢?找DBA去捞一下其中的sql日志,由于线下环境没有日志切割,日志量巨大,搜索时间太慢。没办法,就按照现有的数据进行分析吧。

日志如何被触发

由于当前没有任何思路,于是笔者翻看中间件的代码,发现在update语句执行后,中间件会在收到mysql okay包后打印上述日志。如下图所示:

注意到所有出问题的update出问题的时候都是同一个NIOREACTOR线程先后打印了两条日志,所以笔者推断这两个okay大概率是同一个后端连接返回的。

什么情况会返回多个okay?

这个问题笔者思索了很久,因为在笔者的实际重新执行出问题的sql并debug时,永远只有一个okay返回。于是笔者联想到,我们中间件有个状态同步的部分,而这些状态同步是将set auto_commit=0等sql拼接到应用发送的sql前面。即变成如下所示:

sql可能为
set auto_commit=0;set charset=gbk;>update test set value =1 where id in ("1","2") and status = 1;

于是笔者细细读了这部分的代码,发现处理的很好。其通过计算出前面拼接出的sql数量,再在接收okay包的时候进行递减,最后将真正执行的那条sql处理返回。其处理如下图所示:

但这里确给了笔者一个灵感,即一条sql文本确实是有可能返回多个okay包的。

真相大白

在笔者发现(sql1;sql2;)这样的拼接sql会返回多个okay包后,就立刻联想到,该不会业务自己写了这样的sql发给中间件,造成中间件的sql处理逻辑错乱吧。因为我们的中间件只有在对自己拼接(同步状态)的sql做处理,明显是无法处理应用传过来即为拼接sql的情况。
由于看上去有问题的那条sql并没有拼接,于是笔者凭借这条sql打印所在的reactor线程往上搜索,发现其上面真的有拼接sql!

2020-03-1311:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;
sql=>update test_2 set value =1 where id=1 and status = 1;update test_2 set value =1 where id=2 and status = 1;


如上图所示,(update1;update2)中update1的okay返回被驱动认为是所有的返回。然后应用立即发送了update3。前脚刚发送,update2的okay返回就回来了而其刚好是0,应用就报错了(要不是0,这个错乱逻辑还不会提前暴露)。那三条"重复执行"也很好解释了,就是之前的拼接sql会有三条。

为何是概率出现

但奇怪的是,并不是每次拼接sql都会造成update3"重复执行"的现象,按照笔者的推断应该前面只要是多条拼接sql就会必现才对。于是笔者翻了下jdbc驱动源码,发现其在发送命令之前会清理下接收buffer,如下所示:

MysqlIO.java
final Buffer sendCommand(......){
    ......
    // 清理接收buffer,会将残存的okay包清除掉
    clearInputStream();
    ......
    send(this.sendPacket, this.sendPacket.getPosition());
    ......
}

正是由于clearInputStream()使得错误非必现(暴露),如果okay(update2)在应用发送第三条sql前先到jdbc驱动会被驱动忽略!
让我们再看一下不会让update3"重复执行"的时序图:

即根据okay(update2)返回的快慢来决定是否暴露这个问题,如下图所示:

同时笔者观察日志,确实这种情况下"update1;update2"这条语句在中间件里面日志有两条。

临时解决方案

让业务开发不用这些拼接sql的写法后,再也没出过问题。

为什么不连中间件是okay的

业务开发这些sql是就在线上运行了好久,用了中间件后才出现问题。
既然不连中间件是okay的,那么jdbc必然有这方面的完善处理,笔者去翻了下mysql-connect-java(5.1.46)。由于jdbc里面存在大量的兼容细节处理,笔者这边只列出一些关键代码路径:

MySQL JDBC 源码
MySQLIO
stack;
executeUpdate
    |->executeUpdateInternel
        |->executeInternal
            |->execSQL
                |->sqlQueryDirect
                    |->readAllResults (MysqlIO.java)
readAllResults: //核心在这个函数的处理里面
ResultSetImpl readAllResults(......){
        ......
       while (moreRowSetsExist) {
              ......
              // 在返回okay包的保中其serverStatus字段中如果SERVER_MORE_RESULTS_EXISTS置位
              // 表明还有更多的okay packet
            moreRowSetsExist = (this.serverStatus & SERVER_MORE_RESULTS_EXISTS) != 0;
        }
        ......
}

正确的处理流程如下图所示:

而我们中间件的源码确实这么处理的:

@Override
public void okResponse(byte[] data, BackendConnection conn) {
    ......
    // 这边仅仅处理了autocommit的状态,没有处理SERVER_MORE_RESULTS_EXISTS
    // 所以导致了不兼容拼接sql的现象
    ok.serverStatus = source.isAutocommit() ? 2 : 1;
    ok.write(source);
    ......
}

select也"重复执行"了

解决完上面的问题后,笔者在日志里竟然发现select尽然也有重复的,这边并不会牵涉到okay包的处理,难道还有问题?日志如下所示:

2020-03-13 12:21:01:040[NIOREACTOR-20RW]frontIP=>ip1;sqlID=>12345678;rows=>1;select abc;
2020-03-13 12:21:01:045[NIOREACTOR-21RW]frontIP=>ip2;sqlID=>12345678;rows=>1;select abc;

从不同的REACTOR线程号(20RW/21RW)和不同的frontIP(ip1,ip2)来看是两个连接执行了同样的sql,但为何sqlID是一样的?任何一个诡异的现象都必须一查到底。于是笔者登录到应用上看了下应用日志,确实应用有两个不同的线程运行了同一条sql。
那肯定是中间件日志打印的问题了,笔者很快就想通了其中的关窍,我们中间件有个对同样sql缓存其路由节点结构体的功能(这样下一次同样sql就不必解析,降低了CPU),而sqlID信息正好也在那个路由节点结构体里面。如下图所示:

这个缓存功能感觉没啥用(因为线上基本是没有相同sql的),于是笔者在笔者优化的闪电模式下(大幅度提高中间件性能)将这个功能禁用掉了,没想到为了排查问题而开启的详细日志碰巧将这个功能开启了。

总结

任何系统都不能说百分之百稳定可靠,尤其是不能立flag。在线上运行了好几年的系统也是如此。只有对所有预料外的现象进行细致的追查与深入的分析并解决,才能让我们的系统越来越可靠。

公众号

关注笔者公众号,获取更多干货文章:

原文链接

https://my.oschina.net/alchemystar/blog/3208851

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
7月前
|
SQL IDE 关系型数据库
记录一次SQL中的bug的修复过程
记录一次SQL中的bug的修复过程
91 0
|
SQL Java 数据库连接
springboot-maven项目+jpa 运行过程中执行resources下sql脚本文件-ClassPathResource和ScriptUtils.executeSqlScript的使用
springboot-maven项目+jpa 运行过程中执行resources下sql脚本文件-ClassPathResource和ScriptUtils.executeSqlScript的使用
641 0
springboot-maven项目+jpa 运行过程中执行resources下sql脚本文件-ClassPathResource和ScriptUtils.executeSqlScript的使用
|
6月前
|
存储 SQL 关系型数据库
【BUG记录】Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x90\xA6' for column 'name' at row 1
在MySQL中遇到`Incorrect string value`错误通常是因为尝试插入的字符串包含不被数据库字符集支持的字符,如表情符号。错误根源是MySQL默认的utf8不支持4字节的UTF-8字符(如Emoji)。
735 1
|
SQL 存储 缓存
一文搞懂MySQL中一条SQL语句是如何执行的
一文搞懂MySQL中一条SQL语句是如何执行的
|
SQL 前端开发 Java
JSP缺陷问题(bug)管理系统myeclipse开发sql数据库BS模式java编程MVC结构
JSP 缺陷问题(bug)管理系统是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库采用 serlvet dao bean MVC模式进行开发,系统主要采用B/S模式开发。
59 0
|
消息中间件 存储 NoSQL
【2021年遇到最头疼的Bug】【Alibaba中间件技术系列】「RocketMQ技术专题」Broker配置介绍及发送流程、异常(XX Busy)问题分析总结
【2021年遇到最头疼的Bug】【Alibaba中间件技术系列】「RocketMQ技术专题」Broker配置介绍及发送流程、异常(XX Busy)问题分析总结
692 11
【2021年遇到最头疼的Bug】【Alibaba中间件技术系列】「RocketMQ技术专题」Broker配置介绍及发送流程、异常(XX Busy)问题分析总结
|
SQL 前端开发 安全
一个SQL错误的问题让我找到了公司框架中三个bug
本文是对之前开发中遇到的问题的一个总结,文章其实早就写好,但是觉得自己写得不够深入,就让文章一直躺在草稿箱里。昨天突然想起来了,就将文章重新修改了一下,还是发出来吧!
|
SQL 自然语言处理 前端开发
一个 go-sql-driver 的离奇 bug
对于 Go CURD Boy 来说,相信 `github.com/go-sql-driver/mysql` 这个库都不会陌生。基本上 Go 的 CURD 都离不开这个特别重要的库。我们在开发 Seata-go 时也使用了这个库。不过最近在使用 go-sql-driver/mysql 查询 MySQL 的时候,就出现一个很有意思的 bug, 觉得有必要分享出来,以防止后来者再次踩坑。
一个 go-sql-driver 的离奇 bug
|
SQL 关系型数据库 MySQL
一个 go-sql-driver 的离奇 bug
by 京东技术专家 郝洪范对于 Go CURD Boy 来说,相信 github.com/go-sql-driver/mysql 这个库都不会陌生。或许有些人可能没太留意,直接就复制粘贴了 import。比如我们使用 gorm 的时候,如果不加 _ "github.com/go-sql-driver/mysql" 的话,就会报:panic: sql: unknown drive
296 0
一个 go-sql-driver 的离奇 bug
|
SQL IDE 开发工具
Python脚本执行hive SQL命令
Python脚本执行hive SQL命令