定位频繁创建对象导致内存溢出风险之JDBC MySQL

本文涉及的产品
应用实时监控服务-用户体验监控,每月100OCU免费额度
函数计算FC,每月15万CU 3个月
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 定位频繁创建对象导致内存溢出风险之JDBC MySQL

背景介绍

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

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
220 77
|
4月前
|
Java 关系型数据库 MySQL
mysql5.7 jdbc驱动
遵循上述步骤,即可在Java项目中高效地集成MySQL 5.7 JDBC驱动,实现数据库的访问与管理。
822 1
|
4月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
84 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
5月前
|
Java 测试技术 Android开发
Android性能测试——发现和定位内存泄露和卡顿
本文详细介绍了Android应用性能测试中的内存泄漏与卡顿问题及其解决方案。首先,文章描述了使用MAT工具定位内存泄漏的具体步骤,并通过实例展示了如何分析Histogram图表和Dominator Tree。接着,针对卡顿问题,文章探讨了其产生原因,并提供了多种测试方法,包括GPU呈现模式分析、FPS Meter软件测试、绘制圆点计数法及Android Studio自带的GPU监控功能。最后,文章给出了排查卡顿问题的四个方向,帮助开发者优化应用性能。
318 4
Android性能测试——发现和定位内存泄露和卡顿
|
4月前
|
SQL 分布式计算 关系型数据库
Hadoop-24 Sqoop迁移 MySQL到Hive 与 Hive到MySQL SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-24 Sqoop迁移 MySQL到Hive 与 Hive到MySQL SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
164 0
|
4月前
|
SQL 分布式计算 关系型数据库
Hadoop-23 Sqoop 数据MySQL到HDFS(部分) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-23 Sqoop 数据MySQL到HDFS(部分) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
71 0
|
4月前
|
SQL 分布式计算 关系型数据库
Hadoop-22 Sqoop 数据MySQL到HDFS(全量) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
Hadoop-22 Sqoop 数据MySQL到HDFS(全量) SQL生成数据 HDFS集群 Sqoop import jdbc ETL MapReduce
93 0
|
6月前
|
关系型数据库 MySQL Java
【Azure 应用服务】App Service 无法连接到Azure MySQL服务,报错:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
【Azure 应用服务】App Service 无法连接到Azure MySQL服务,报错:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
205 0
|
3月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
635 1
|
2月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。