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

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
简介: 定位频繁创建对象导致内存溢出风险之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、结果集的条数、大小及调用的线程栈信息,当结果集的条数、大小或执行时间超过阈值的时候,进行报警以便更快的发现和分析定位。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
1月前
|
SQL 监控 关系型数据库
【紧急救援】MySQL CPU 100%!一套组合拳教你快速定位并解决!
凌晨三点MySQL CPU飙至100%,业务瘫痪!本文亲历30分钟应急排障全过程:从紧急止血、定位慢查询、分析锁争用,到优化SQL与索引,最终恢复服务。总结一套可复用的排查路径与预防方案,助你告别深夜救火。
|
2月前
|
SQL 存储 关系型数据库
MySQL内存引擎:Memory存储引擎的适用场景
MySQL Memory存储引擎将数据存储在内存中,提供极速读写性能,适用于会话存储、临时数据处理、高速缓存和实时统计等场景。但其数据在服务器重启后会丢失,不适合持久化存储、大容量数据及高并发写入场景。本文深入解析其特性、原理、适用场景与限制,并提供性能优化技巧及替代方案比较,助你合理利用这一“内存闪电”。
|
10月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
1027 166
|
12月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
423 77
|
8月前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
232 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
Java 测试技术 Android开发
Android性能测试——发现和定位内存泄露和卡顿
本文详细介绍了Android应用性能测试中的内存泄漏与卡顿问题及其解决方案。首先,文章描述了使用MAT工具定位内存泄漏的具体步骤,并通过实例展示了如何分析Histogram图表和Dominator Tree。接着,针对卡顿问题,文章探讨了其产生原因,并提供了多种测试方法,包括GPU呈现模式分析、FPS Meter软件测试、绘制圆点计数法及Android Studio自带的GPU监控功能。最后,文章给出了排查卡顿问题的四个方向,帮助开发者优化应用性能。
988 4
Android性能测试——发现和定位内存泄露和卡顿
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
219 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
缓存 监控 关系型数据库
如何查看MySQL使用的内存
如何查看MySQL使用的内存
374 1
|
存储 缓存 监控
深入了解MySQL内存管理:如何查看MySQL使用的内存
深入了解MySQL内存管理:如何查看MySQL使用的内存
1246 1
|
SQL 监控 关系型数据库
如何查看MySQL使用的内存
综合运用上述方法,您可以全方位地监控和管理MySQL的内存使用。从简单查看配置到深入分析实时内存占用,每种方法都有其适用场景和优势。定期检查和调整MySQL的内存配置,对于维持数据库性能和稳定性至关重要。
1556 0

推荐镜像

更多