背景介绍
在文章【定位频繁创建对象导致内存溢出风险的思路】中分析了三种【事中】定位的方法,总体思路是能够拦截对象的创建逻辑,现在对【同一条SQL语句,平时返回预期内的数据条数,出问题的时候返回了几十万条数据,短时间内创建了大量对象进而导致非预期的GC】这个场景进行分析。
问题分析
同一条SQL语句,平时返回预期内的数据条数,出问题的时候返回了几十万条数据,短时间内创建了大量对象进而导致非预期的GC,严重情况下会导致应用无法提供服务。当这样情况发生的时候,需要能够及时发现并进行处理,为了便于定位问题,需要以下信息:
- 引起问题的sql及sql的参数
- 查询结果集的条数
- 查询结果集的字节大小
- 执行该sql的线程栈信息
当然也可以根据具体需求,获取更多的信息,比如:数据库连接信息、各种相关配置参数等。
实现方法
采用字节码增强技术,当Statement执行execute和executeQuery的时候,拦截方法的返回并对返回结果进行分析。具体实现如下:
mysql-connector-java:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>
字节码增强框架使用的是bytekit:
<dependency> <groupId>com.alibaba</groupId> <artifactId>bytekit-core</artifactId> <version>0.0.8</version> </dependency>
Mysql Statement Query Interceptor :
import com.alibaba.bytekit.a**.binding.Binding; import com.alibaba.bytekit.a**.interceptor.annotation.AtEnter; import com.alibaba.bytekit.a**.interceptor.annotation.AtExit; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.SQLException; public class MysqlStatementQueryInterceptor { private static ThreadLocal<Long> HOLDER = new ThreadLocal<>(); private static int THRESHOLD_COUNT = 100; private static int THRESHOLD_SIZE = 1*1024; private final static int THRESHOLD_ELAPSED = 10 * 1000; @AtEnter(inline = false) public static void atEnter() { HOLDER.set(System.currentTimeMillis()); } @AtExit(inline = false) public static void atExit(@Binding.This Object target, @Binding.Args Object[] args, @Binding.MethodName String methodName) { try{ doAtExit(target,args,methodName); }catch (Throwable throwable){ throwable.printStackTrace(); } } public static void doAtExit(Object target,Object[] args, String methodName) throws SQLException, IllegalAccessException, InvocationTargetException { Field resultsField = field(target.getClass(),"results"); resultsField.setAccessible(true); Object obj = resultsField.get(target); Method getUpdateCount = method(obj.getClass(),"getUpdateCount"); getUpdateCount.setAccessible(true); long updateCount = (long)getUpdateCount.invoke(obj); Method getBytesSize = method(obj.getClass(),"getBytesSize"); getBytesSize.setAccessible(true); int byteSize = (int)getBytesSize.invoke(obj); long elapsed = System.currentTimeMillis() - HOLDER.get(); if(updateCount > THRESHOLD_COUNT || byteSize > THRESHOLD_SIZE || elapsed > THRESHOLD_ELAPSED){ String sql = (args.length >= 1) ? (String) args[0] : ""; Method asSql = method(target.getClass(),"asSql"); if(asSql != null){ asSql.setAccessible(true); sql = (String) asSql.invoke(target); } String ** = target.getClass().getName() + "." + methodName + "," + sql + "," + byteSize + " bytes"+ ",amount " + updateCount + ",elapsed " + elapsed + " ms"; TooManyResultException e = new TooManyResultException(**); e.setStackTrace(Thread.currentThread().getStackTrace()); e.printStackTrace(); } } private static Field field(Class<?> clazz,String fieldName){ if(clazz == null){ return null; } try{ return clazz.getDeclaredField(fieldName); }catch (NoSuchFieldException exception){ return field(clazz.getSuperclass(),fieldName); } } private static Method method(Class<?> clazz, String methodName){ if(clazz == null){ return null; } try{ return clazz.getDeclaredMethod(methodName); } catch (NoSuchMethodException e) { return method(clazz.getSuperclass(),methodName); } } }
增强字节码:
Instrumentation instrumentation = AgentUtils.install(); DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser(); List<InterceptorProcessor> processors = interceptorClassParser.parse(MysqlStatementQueryInterceptor.class); String classPattern = "com.mysql.jdbc.StatementImpl"; Set<String> methodNames = new HashSet<>(); methodNames.add("executeQuery"); methodNames.add("execute"); BytekitUtils.reTransformClass(instrumentation,processors,classPattern,methodNames,true);
import com.alibaba.bytekit.a**.MethodProcessor; import com.alibaba.bytekit.a**.interceptor.InterceptorProcessor; import com.alibaba.bytekit.utils.AgentUtils; import com.alibaba.bytekit.utils.A**Utils; import com.alibaba.deps.org.objectweb.a**.tree.ClassNode; import com.alibaba.deps.org.objectweb.a**.tree.MethodNode; import java.lang.instrument.Instrumentation; import java.util.List; import java.util.Set; public class BytekitUtils { public static void reTransformClass(Instrumentation instrumentation, List<InterceptorProcessor> processors, String className, Set<String> methodNames, boolean subClass){ Set<Class<?>> classes = SearchUtils.searchClassOnly(instrumentation,className,false); if(classes.isEmpty()){ return; } Set<Class<?>> subClasses = classes; if(subClass){ subClasses =SearchUtils.searchSubClass(instrumentation,classes); } reTransform(processors,subClasses,methodNames); } public static void reTransform(List<InterceptorProcessor> processors,Set<Class<?>> classes,Set<String> methodNames) { for(Class<?> cls : classes) { ClassNode classNode = null; try { classNode = A**Utils.loadClass(cls); classNode = A**Utils.removeJSRInstructions(classNode); } catch (Exception e) { e.printStackTrace(); continue; } boolean inited = false; for (MethodNode methodNode : classNode.methods) { if (methodNames == null || methodNames.contains(methodNode.name)) { MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode); for (InterceptorProcessor interceptor : processors) { try { interceptor.process(methodProcessor); inited = true; } catch (Exception e) { e.printStackTrace(); } } } } if (!inited) { continue; } byte[] bytes = A**Utils.toBytes(classNode); try { AgentUtils.reTransform(cls, bytes); } catch (Exception e) { e.printStackTrace(); } } } }
应用方式
方式一
在应用启动的时候进行字节码增强,可以实时监控每个数据库查询,当出现问题的时候,可以进行报警,能够更快的发现问题;代价是有些额外的开销。
方式二
按需进行字节码增强,即当系统出现问题的时候进行字节码增强(比如作为一条command集成进arthas),当再次出现问题的时候,可以抓取到异常信息进行分析。
总结
通过字节码增强技术来拦截Statement的执行,从而获取执行的sql、结果集的条数、大小及调用的线程栈信息,当结果集的条数、大小或执行时间超过阈值的时候,进行报警以便更快的发现和分析定位。