分享一个思路,使用插桩技术解决慢查询测试问题
前段时间,我负责测试的系统在生产环境运行出现问题。该系统对于响应时间要求较高,问题发生的时候并发很高,出现大量请求超时,超时请求比例随时间推迟越来越高,最后几乎全部请求都失败。滚动重启了所有进程后,很快又出现超时情况。
后经过排查,发现是新版本实现某个功能时修改了一个数据库查询语句,修改后该查询语句的查询条件未使用到索引字段,而所查询的表生产环境中体量巨大,因此这个查询操作耗时从毫秒级变成了秒级,也就是形成了所谓的慢查询,再加上大量并发,悲剧就发生了。
事件发生后,我们测试团队进行了反思,这么严重的问题为何测试环境没有发现?总结了两点原因:一是,测试环境进行功能测试时并发量不高,即使单个请求变慢也不会发生超时现象;二是,测试环境数据库表的数据量较生产环境小很多,所以单个查询操作比生产快很多,这样压力测试中请求也极少超时。
求索
综上所述,想要在测试过程中人为识别一个慢查询很难,为了杜绝这类问题再次发生,在后续版本测试中我们做了一些尝试。
因为我们内部本来就有使用代码扫描的工具,每个版本都会通过扫描来识别一些问题,所以我们首先想到了通过静态扫描原代码,捞出所有的数据库查询语句然后进行分析。实际操作后发现,我们系统在数据库操作上大量使用框架,不同模块使用的框架还不同,捞出的数据库语句千奇百怪,且包含代码元素,并不是能直接执行的语句,对于大型系统而言,人工去分析这些语句工作量太大,这种方法并不可行。
然后我们想到,可以从数据库侧来解决这个问题,通过开启 Mysql 的慢查询日志开关,将功能测试过程中大于 long_query_time 配置时间的数据库查询操作都记录下来,再逐个分析是否存在慢查询问题。
过程中我们确实抓到了很多执行较慢的查询语句,但经过分析后发现,这些语句绝大部分都是测试人员人工查询数据库的操作,更遗憾的是,由于测试数据数量级较少,之前发生生产问题的查询语句在测试环境的执行时间并没有超过 long_query_time,由此并不能被识别出来。
由此可见,这种方法误报和漏报概率很大,也不可行。
革新
现有工具无法满足我们识别慢查询语句的需求,于是我们决定自己做了一套工具。通过大量的分析和实验,我们得到了一个高效、准确性、且通用性极好的解决方案:
经过分析,识别慢查询语句需要解决两个问题:一是,如何获得系统执行是查询语句;二是,如何分析某个查询是否是慢查询。
解决第一个问题,我们想到了使用插桩技术。
对于一个查询操作,不管上层应用代码如何编写、或使用何种数据库框架,这个操作最终会与目标数据库交互,而交互的时候它一定必须是一个标准的 SQL 语句。基于这一点,我们对这个应用进行了全面的分析,我们的系统部署在 Jboss 上,通过层层剖析,我们找到了这个实际执行查询操作数据库交互的方法,位于 Jboss 的 JCA 包中,共用到以下两处:
① org.jboss.jca.adapters.jdbc.WrappedPreparedStatement.executeQuery()② org.jboss.jca.adapters.jdbc.WrappedStatement.executeQuery()
通过大量的实验,我们确定我们这个系统所有数据库查询操作必定会调用①②中的一个来完成(实现逻辑不同其他系统可能调用的是 JCA 的其他方法)。再通过在①②设置断点 bebug 我们发现,在①②方法内部 SQL 语句是完全可见的。
接下来我们利用的 Java Instrument Api 及其衍生的开源组件,搭建了一个 agent 程序。启动 agent,agent 在应用系统程序运行时动态的往这两个地方分别插个桩,桩的内容非常简单:将当前方法体内存中正在执行的 SQL 语句打印到某个固定位置(假设我们把 SQL 语句输出到日志文件 A 中)。相对于在①②方法体内部多写一句 print,仅仅只做一个打印的操作,不会对业务逻辑产生任何干扰。
于是我们就完成了这样一个事情:当应用系统要进行数据库查询操作时,它会调用①②中的一个来执行这个查询 SQL,①②被调用时,会将正在执行的 SQL 语句输出到日志文件 A 中。这样,每一个查询操作,都会将实际的查询语句记录在日志文件 A 中,也就完成了查询语句的收集啦。
通过插桩我们获得了大量的 SQL 语句,接下来解决第二个问题,如何判断一个查询语句是否为慢查询。
由于测试和生产数据数量级的差异,用执行时间来判断显然不科学。同时,我们一共获得了几万条 SQL 语句,直接进行人工分析显然不可行。
我们想到了 Mysql 提供的 explain 命令来扩展 SQL 语句,通过 Mysql 的执行计划来科学判断执行的快慢。每条可执行 SQL 语句都可以直接用 explain 命令获得
执行计划中的每一个列标签都可以作为匹配环节的关注项,我们称其为指标项,我们用到了与查询效率相关的指标项中最重要的两个:
1.key:表示这个 SQL 语句执行时会使用的索引的键;
2.type:访问方式,表示执行 SQL 语句是在数据库表中找到所需行的方式,可能的值如下:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
从 system 到 ALL,性能从好到差,一般来说应保证至少达到 range 级别。
第一步,我们将日志文件 A 中所有的 SQL 语句逐条转换成执行计划;
第二步,根据系统实际需求,建立一套规则,对执行计划进行筛选,找出可能是慢查询的语句;
我们系统匹配慢查询的规则是:
key in [NULL]ORtype in [range,index,ALL]ORRows >= 1000
这个规则表示:如果一个 SQL 语句它未经过索引、或者访问方式为 range、index、ALL 之一、或者预估扫描行数大于等于 1000 条,那么它可能是一个慢查询。
第三步,对可能是慢查询的语句进行人工分析。
通过第二步的筛选,我们将需要分析的 SQL 语句数量从几十万条降到了十几条,后续再人工逐一分析。
如此,我们完成了系统的慢查询测试工作。之前导致生产问题的 SQL 语句完美命中,其他疑似慢查询语句结合查询频率、生产数据表数量级等因素,人工判定为非慢。
破浪
后来,通过实现 agent 插桩位置、慢查询筛选规则的可配置,我们将这套解决方案优化为一个通用框架,并推广到部门的多个系统使用,并发现了若干慢查询隐患。
对于这套基于插桩的慢查询测试方法,总结优势如下:
1.SQL 语句覆盖全面,且准确性较高。只有插桩点分析准备,可以保证捕获程序运行时执行的所有 SQL 语句(由于实际执行过的 SQL 语句才能被捕获,因此依赖于功能测试的完整性),而以执行计划为基础的分析更具有科学性,且不受数据量大小的影响,准确性更高。
2.有极好的通用性。插桩位置可配置,不同系统只需修改配置既能使用。桩点一般为底层实现与数据库交互的数据库驱动包某一些特定的类和方法,与具体应用程序实现方式不相关,也就是说,无论程序功能是什么、无论使用了什么数据库框架,只要配置正确的数据库交互类及其方法,都能适配。
3.非侵入、可插拔,被测应用无感知。agent 启动,则动态插桩,agent 停止,则桩点消失。无需对被测应用源码做任何修改,检测过程对功能无影响,可在功能测试中悄无声息的完成。
一些实践
有TesterHome社区的同学们看到这一思路后的一些想法和实践:
1.感觉真的好奇妙啊,我们对这个问题的解决思路竟然如此的相似,两年前我做的一个开源项目就是用这个思路,一直没推广,换公司后用 go 比较多了,已经好久没更新了。 https://github.com/bugVanisher/no-slow-query
2.mybatis 插件就够了.天然亲和. 一个 jar 包.全公司引用. Java agent 成本相对还是高一点点。
这是我搞的一个防止 where 标签失效的插件.其他同理https://github.com/Forest10/forest10-tool/blob/master/src/main/java/com/forest10/mybatis/interceptor/BatchModifyForbiddenInterceptor.java
3.根据楼主(作者)的思路实现的,已开源
https://github.com/tangyiming/sql-detect
内含插桩插件,平台前后端,demo 应用,数据库 ddl 与数据资源。
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!
相关文章:

分享一个思路,使用插桩技术解决慢查询测试问题
前段时间,我负责测试的系统在生产环境运行出现问题。该系统对于响应时间要求较高,问题发生的时候并发很高,出现大量请求超时,超时请求比例随时间推迟越来越高,最后几乎全部请求都失败。滚动重启了所有进程后࿰…...

【STM32项目】在FreeRtos背景下的实战项目的实现过程(二)
个人主页~ 实战项目的实现过程(一)~ 实战项目的实现过程 二、初步了解各个外设硬件1、OLED模块2、GPS模块3、MPU6050模块4、超声测距模块5、温度测控模块6、语音模块7、SIM模块8、按键模块 三、查阅资料1、查看手册2、查找例程 四、研究硬件功能1、OLED…...

javaer快速入门 goweb框架 gin
gin 入门 前置条件 安装环境 配置代理 # 配置 GOPROXY 环境变量,以下三选一# 1. 七牛 CDN go env -w GOPROXYhttps://goproxy.cn,direct# 2. 阿里云 go env -w GOPROXYhttps://mirrors.aliyun.com/goproxy/,direct# 3. 官方 go env -w GOPROXYhttps://goproxy.…...

SQL - 数据类型
字符串类型 char(10),存储固定长度字符串 varchar(255),存储可变长度字符串 mediumtext,中文本,对于存储JSON对象、SCV字符串很好使 longtext,长文本,可以很好地存储教本或许多年地日志文件 tinytext&#…...

进程相关知识
进程和程序的区别 程序 程序是静态的,是存储在硬盘、SSD等存储介质中的一个文件,通常由源代码(如 .c 文件)编译生成的二进制可执行文件(如 a.out)。程序包含了指令和数据,但在未被执行时&#…...

萝卜快跑和端到端的自动驾驶(1)
先看一篇论文 2311.18636 (arxiv.org) 这篇论文里有一个非常好的图 比较了一下模块化任务(级联任务)和端到端自动驾驶的区别 首先什么叫模块化任务(级联) 如上图所示,左边的方块中的子方块,是展示了自动驾驶获取数据的途径,这里包括&…...

通信原理学习笔记
一个手机通话需要经过下面三个网络 类别接入网(Access Network)承载网(Transport Network)核心网(Core Network)定义连接终端用户与电信网络的部分。在接入网和核心网之间传输数据的网络。处理、交换和管理…...

系统编程---day4
1. 链接文件 命令行: ln -s 文件名 softlink 1.1 symlink int symlink(const char *oldpath, const char *newpath); 功能:创建一个链接向oldpath文件的新符号链接文件 参数:oldpath:被链接向的文件的路径 newpath:新符号链接文件 返回值:成功返回0,失败返回…...

01:电容的什么,各类电容的优缺点
1.电容是什么? 电容是由两块不连通的导体,已经中间的不导电材料组成 电容结构: 1.2电容的容量计算公式 C ε s d \displaystyle\frac{εs}{d} dεs 1.3常见电容的种类 1.4各类电容的特点...

Android+Jacoco+code-diff全量、增量覆盖率生成实战
背景 主要是记录下Android项目使用jacoco生成代码覆盖率的实战流程,目前已完成全量覆盖方案,仅使用jacoco就能实现; 由于我们的Android端是使用Java和kotlin语言,目前增量的方案code-diff仅针对Java代码,卡在kotlin文件的分析&am…...

乌龟对对碰在线版
爆肝两天使用vue开发了一个在线版的乌龟对对碰小游戏之幸运对对碰。没有找到合适的乌龟素材,现在使用小兔子代替。 体验地址:幸运对对碰 | 乌龟对对碰小游戏 之前的python版本的乌龟对对碰:写文章-CSDN博客 乌龟对对碰-幸运对对碰...

如何更改select option边框颜色和选中的颜色
<!doctype html> <html> <head> <meta charset"utf-8"> <title>如何更改select option边框颜色和选中的颜色</title> </head><style>ul{border: 1px solid #000000;width: 500px;height: auto;background-color: aq…...

6. 数据结构—串的匹配算法
1.BF算法(暴力算法) //模式匹配(暴力算法) int Index(SString S,SString T){int i1,j1;while(i<S.length&&j<T.length){if(S[i]T[i]){i;j;}else{ii-j2; //最开始匹配的位置的后一个j1; //从头匹配 }}if(j>T.length)return i-T.length;return return 0…...

九大服务架构性能优化方式
来源:九大服务架构性能优化方式 目录 性能优化九大方式: 缓存 使用什么样的缓存 缓存常见问题 缓存淘汰 缓存数据一致性 并行化处理 批量化处理 数据压缩合并 无锁化 顺序写 分片化 避免请求 池化 异步处理 总结 最近做了一些服务性能优…...

【RabbitMQ】 相关概念 + 工作模式
本文将介绍一些MQ中常见的概念,同时也会简单实现一下RabbitMQ的工作流程。 MQ概念 Message Queue消息队列。是用来存储消息的队列,多用于分布式系统之间的通信。 系统间调用通常有:同步通信和异步通信。MQ就是在异步通信的时候使用的。 同…...

嵌入式学习 ——(Linux高级编程——进程)
目录 一、进程的含义 二、进程和程序的区别 三、进程的作用 四、进程的状态 五、进程的调度与上下文切换 六、查询进程相关命令 七、fork()函数 八、getpid()和getppid()函数 九、面试题解析: 十、应用场合及测试 一、进程的含义 进程指正在运行的程序&a…...

C++练习备忘录
1. 保留两位小数输出格式 #include <iostream> #include <iomanip> using namespace std; int main() {double S 0;S (15 25) * 20 / 2;cout << fixed << setprecision(2) << S;return 0; }2. 设置输出宽度 #include <iostream> #inclu…...

改善工作流
快捷键管理器 打开Editor->Shortcuts查看和编辑Unity中的快捷键 示例 ShiftSpace 窗口最大化 P 选择预制体 进入预制体编辑模式 单一检视窗口 选择组件,选择Properties打开一个窗口,显示组件信息;切换对象,窗口信息不会改变…...

迭代器失效
一、什么是迭代器失效 迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指…...

@RequestParam @RequestBody @PathVariable 这三个注解对应的前端使用vue的http请求时不同的调用方式
1. RequestParam 用途:用于提取请求参数,常见于GET请求或表单提交。 Vue HTTP 请求示例: // 使用axios发送GET请求 axios.get(/api/users, { params: { id: 1, name: John } }); 2. RequestBody 用途:用于提取请求体…...

SQL - 索引
索引本质上是数据库引擎用来快速查找数据的数据结构,可以显著提高查询的性能,为了加快运行较慢的查询。创建索引 默认索引 create index 索引名 on 表名 (列名); 通过对列名进行创建索引,在查询的时候,数据库就能通过索引找到匹配…...

Oracle23ai新特性FOR LOOP循环控制结构增强
在Oracle数据库中,FOR LOOP是一种常用的循环控制结构,它允许你重复执行一系列语句固定次数或直到满足特定条件为止。然而,标准的Oracle PL/SQL中的FOR LOOP主要用于遍历集合(如数组或游标的结果集),而不是像…...

DHU OJ 二维数组
思路及代码 #include<iostream> using namespace std; int main(){ //input 多组 //input M,N int 1< <20 //input M 行 N 列 数据 //initialize listint M, N;while (cin >> M >> N){int list[M][N];for (int i 0; i < M-1; i){for (int j 0; j…...

UDP/TCP --- Socket编程
本篇将使用 Linux 中的系统调用来实现模拟 TCP 和 UDP 的通信过程,其中只对 UDP 和 TCP 进行了简单的介绍,本篇主要实现的是代码,至于 UDP 和 TCP 的详细讲解将会在之后的文章中给出。 本篇给出的 tcp 和 udp 的代码中的 echo 都是测试连接是…...

【C语言】最详细的单链表(两遍包会!)
🦄个人主页:小米里的大麦-CSDN博客 🎏所属专栏:C语言数据结构_小米里的大麦的博客-CSDN博客 🎁代码托管:黄灿灿/数据结构 (gitee.com) ⚙️操作环境:Visual Studio 2022 目录 一、前言 二、单链表的概念 1. 单链表的特点 2. 单链表的基本…...

QT:VS2019 CMake编译CEF
CEF介绍 CEF作为一个基于Chromium的开源Web浏览器控件,为第三方应用提供了强大的嵌入浏览器支持。其多平台支持、HTML5特性、自定义能力以及多进程架构等特性,使得CEF在浏览器开发、桌面应用、开发工具以及自动化测试等领域得到了广泛应用。 多平台支持…...

day31(8/19)——静态文件共享、playbook
目录 一、ansible模块 script模块 copy模块 使用command模块下载 nfs-utils rpcbind 在被控制的主机上添加static目录,并创建test文件 command模块 service模块 二、playbook 三、playbook编排vsftpd 1、安装 2、卸载 3、启动服务 4、修改配置文件设置不…...

白骑士的C#教学实战项目篇 4.4 游戏开发
系列目录 上一篇:白骑士的C#教学实战项目篇 4.3 Web开发 在这一部分,我们将探索如何使用 Unity 和 C# 开发游戏。游戏开发结合了编程、图形设计和创意,既充满挑战又充满乐趣。通过这一节的学习,您将了解游戏引擎的基础知识&#…...

在Vue工程中开发页面时,发现页面垂直方向出现两个滚动条的处理
在Vue工程中开发页面时,发现页面垂直方向出现两个滚动条 最近在开发页面时,发现页面多了两个滚动条,如图: 原因: 当一个页面的内容高度大于屏幕的高度时就会出现滚动条。一般情况下当一个页面高度大于屏幕高度时&a…...

【C++初阶】:C++入门篇(一)
文章目录 前言一、C命名空间1.1 命名空间的定义1.2 命名空间的使用 二、C的输入和输出2.1 cin和cout的使用 三、缺省参数3.1 缺省参数的分类 四、函数重载4.1 函数重载概念及其条件4.2 C支持函数重载原理 -- 名字修饰 前言 C是在C语言的基础之上,增加了一些面向对象…...