当前位置: 首页 > 后端技术 > Java

使用存根技术解决慢查询测试问题

时间:2023-04-02 00:52:15 Java

原文由zlulu发表于TesterHome社区。系统对响应时间要求高。出现问题时,并发度高,大量请求超时。超时请求的比例随着时间的增加而增加,最后几乎所有的请求都失败了。在所有进程滚动重启后,不久又发生超时。经排查,发现新版本实现某功能时修改了一条数据库查询语句。修改后,查询语句的查询条件没有使用索引字段,而且生产环境中查询的表体量巨大,所以本次查询操作的耗时从毫秒级变成了秒级,这就是形成所谓的慢查询。再加上大量的并发,悲剧就发生了。事件发生后,我们的测试团队反思,为什么在测试环境中没有发现这么严重的问题?归纳起来有两个原因。首先,测试环境下功能测试的并发度不高,即使单个请求变慢也不会超时。其次,测试环境的数据库表数据量比生产环境小很多,所以单次Query操作比生产快很多,所以压测时请求很少超时。Searching综上所述,在测试过程中手动识别慢查询是非常困难的。为了防止此类问题再次发生,我们在后续的版本测试中做了一些尝试。因为我们内部已经有了代码扫描工具,每个版本都会通过扫描发现一些问题,所以我们首先想到的是静态扫描原始代码,将所有的数据库查询语句提取出来,然后进行分析。经过实际运行,我们发现我们的系统在数据库操作上使用了大量的框架,而且不同的模块使用的框架是不一样的。检索到的数据库语句千奇百怪,包含代码元素,不是可以直接执行的语句。对于大型系统,手动分析这些语句的工作量太大,这种方法不可行。然后我们想到可以从数据库端解决这个问题。通过打开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语句是完全可见的。接下来,我们使用JavaInstrumentApi及其派生的开源组件构建了一个代理程序。启动agent,agent会在应用系统程序运行时,动态的在这两个地方插入一个stub。存根的内容很简单:将当前方法体内存中正在执行的SQL语句打印到固定位置(假设我们把语句输出到日志文件A)。相比在①②方法体内多写一句打印,只写一次打印操作不会对业务逻辑造成任何干扰。于是我们就完成了这样一件事情:当应用系统要进行数据库查询操作时,会调用①②其中之一来执行查询SQL,当调用①②时,会将正在??执行的SQL语句输出到日志文件中A。这样,每次查询操作都会在日志文件A中记录下实际的查询语句,查询语句的收集就完成了。我们通过插桩获取了大量的SQL语句,然后解决第二个问题,如何判断一条查询语句是否为慢查询。由于测试和生产数据的量级差异,以执行时间来判断显然是不科学的。同时,我们总共获取了数万条SQL语句,直接人工分析显然是行不通的。我们想到了Mysql提供的explain命令来展开SQL语句,通过Mysql的执行计划来科学判断执行速度。使用explain命令可以直接获取每条可执行的SQL语句。执行计划中的每一列标签都可以作为匹配过程中的关注项。我们称之为索引项。我们使用与查询效率相关的最重要的索引项。其中两个:1.key:表示执行SQL语句时将使用的索引的key;2.type:访问方式,表示在数据库表中查找所需行的SQL语句的执行方式。可能取值如下:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL从system到ALL,性能从好到坏,一般来说应该保证至少达到范围水平。第一步,我们将日志文件A中的所有SQL语句逐一转化为执行计划;第二步,根据系统的实际需要,我们建立一套规则来过滤执行计划,找出可能是慢查询的语句;我们系统匹配慢查询的规则是:keyin[NULL]ORtypein[range,index,ALL]ORRows>=1000这个规则的意思是:如果一个SQL语句没有被索引,或者访问方式是range,index之一,ALL,或者预估扫描行数大于等于1000,则可能是慢查询。第三步,手动分析可能是慢查询的语句。通过第二步筛选,我们把需要分析的SQL语句从几十万条减少到十几条,然后再一条一条人工分析。这样,我们就完成了系统的慢查询测试。之前导致生产问题的SQL语句被完美命中,其他疑似慢查询语句结合查询频率、生产数据表量级等因素人工判断为不慢。破浪之后,我们通过实现agent存根位置和慢查询过滤规则的可配置性,将这个方案优化成一个通用的框架,并推广到部门内的多个系统,发现了几个慢查询的隐患。对于这套基于instrumentation的慢查询测试方法,优势总结如下:1.SQL语句全覆盖,精准。只有插桩点分析准备才能保证捕获到程序运行时执行的所有SQL语句(由于只能捕获实际执行的SQL语句,所以依赖于功能测试的完整性),分析基于执行计划更科学,不受数据大小影响,准确率更高。2.优异的通用性。Stub的位置是可配置的,不同的系统只需要修改配置就可以使用。存根一般是数据库驱动包的具体类和方法,用于与数据库交互的底层实现,与具体应用程序的实现无关。也就是说,无论程序的功能是什么,无论使用什么数据库框架,只要配置正确,数据库交互类及其方法都可以适配。3.非侵入式可插拔,被测应用无感知。当代理启动时,存根将被动态插入,当代理停止时,存根将消失。无需对被测应用源代码做任何修改,检测过程对功能无影响,可在功能测试中安静完成。收获前沿的测试开发技术学习先进的质量管理方法结识测试专家和行业精英↓↓↓↓