性能文章>【全网首发】定位频繁创建对象导致内存溢出风险之JDBC MySQL>

【全网首发】定位频繁创建对象导致内存溢出风险之JDBC MySQL原创

330478

背景介绍

在文章【定位频繁创建对象导致内存溢出风险的思路】中分析了三种【事中】定位的方法,总体思路是能够拦截对象的创建逻辑,现在对【同一条SQL语句,平时返回预期内的数据条数,出问题的时候返回了几十万条数据,短时间内创建了大量对象进而导致非预期的GC】这个场景进行分析。

问题分析

同一条SQL语句,平时返回预期内的数据条数,出问题的时候返回了几十万条数据,短时间内创建了大量对象进而导致非预期的GC,严重情况下会导致应用无法提供服务。当这样情况发生的时候,需要能够及时发现并进行处理,为了便于定位问题,需要以下信息:

  1. 引起问题的sql及sql的参数
  2. 查询结果集的条数
  3. 查询结果集的字节大小
  4. 执行该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、结果集的条数、大小及调用的线程栈信息,当结果集的条数、大小或执行时间超过阈值的时候,进行报警以便更快的发现和分析定位。

点赞收藏
大禹的足迹

在阿里搬了几年砖的大龄码农,头条号:大禹的足迹

请先登录,查看7条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
8
7