【全网首发】定位频繁创建对象导致内存溢出风险之JDBC MySQL原创
背景介绍
在文章【定位频繁创建对象导致内存溢出风险的思路】中分析了三种【事中】定位的方法,总体思路是能够拦截对象的创建逻辑,现在对【同一条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、结果集的条数、大小及调用的线程栈信息,当结果集的条数、大小或执行时间超过阈值的时候,进行报警以便更快的发现和分析定位。