记录Druid 监控URL数据、Spring方法没有数据排查过程

2024-08-21 00:18

本文主要是介绍记录Druid 监控URL数据、Spring方法没有数据排查过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

问题

URL监控以及Spring 监控数据没有、SQL监控中存在数据
这里写图片描述

排查过程

1、URL监控、Spring监控没有数据,排查数据源

  <servlet><servlet-name>DruidStatView</servlet-name><servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class></servlet><servlet-mapping><servlet-name>DruidStatView</servlet-name><url-pattern>/druid/*</url-pattern></servlet-mapping>

根据Druid监控数据的配置信息可以查看到,访问地址信息从当前的Servlet入手查看,DruidStatService类中刚刚好查看到所有的访问信息.
DruidStatService 中的部分代码。

public String service(String url) {if (url.startsWith("/weburi.json")) {return returnJSONResult(RESULT_CODE_SUCCESS,  getWebURIStatDataList(parameters));}if (url.startsWith("/webapp.json")) {return returnJSONResult(RESULT_CODE_SUCCESS,  getWebAppStatDataList(parameters));}if (url.startsWith("/websession.json")) {return returnJSONResult(RESULT_CODE_SUCCESS,  getWebSessionStatDataList(parameters));}if (url.startsWith("/spring.json")) {return returnJSONResult(RESULT_CODE_SUCCESS,  getSpringStatDataList(parameters));}if (url.startsWith("/spring-detail.json")) {String clazz = parameters.get("class");String method = parameters.get("method");return returnJSONResult(RESULT_CODE_SUCCESS, getSpringMethodStatData(clazz, method));}}

根据weburi.json这个信息,我们可以了解到其实获取数据的逻辑就是下面的这个方法中获取数据。就问题出发URI监控没有统计信息,根据当前类可以发现,所有的数据信息都是通过WebSppStatManger中进行获取处理的!

private List<Map<String, Object>> getWebURIStatDataList(Map<String,   String> parameters) {List<Map<String, Object>> array = WebAppStatManager .getInstance(). getURIStatData();return comparatorOrderBy(array, parameters);}

WebSppStatManger中获取数据的逻辑,全部信息都是来至于一个Set共享的集合中,然后进行处理,返回前端需要的数据信息!
可以看出来WebAppStatManager有个静态的单例的实例,然后从Set集合中获取数据后进行展示的处理,返回给前端程序查看。

public class WebAppStatManager {private final static WebAppStatManager instance = new WebAppStatManager();private Set<Object>   webAppStatSet      = null;public static WebAppStatManager getInstance() {return instance;}public Set<Object> getWebAppStatSet() {if (webAppStatSet == null) {      webAppStatSet = new CopyOnWriteArraySet<Object>();               }return webAppStatSet;}//这里是获取数据的地方,其实就是从Set集合中获取数据,然后整理处理数据信息public List<Map<String, Object>> getURIStatData() {Set<Object> stats = getWebAppStatSet();List<Map<String, Object>> allAppUriStatDataList = new ArrayList<Map<String, Object>>();for (Object stat : stats) {List<Map<String, Object>> uriStatDataList = WebAppStatUtils.getURIStatDataList(stat);allAppUriStatDataList.addAll(uriStatDataList);}return allAppUriStatDataList;}}

2、Set中的数据从哪里来?

使用IDEA查找相关的使用,发现只有两个地方在使用,其中一个就是当前类!而且这个类就是我们进行web数据统一的Filter入口类中,统计数据的收集就是从这里开始的!
WebAppStatManager中的添加Set集合的地方
这里写图片描述

数据收集过滤的Filter

<filter><filter-name>DruidWebStatFilter</filter-name><filter-class>om.alibaba.druid.support.http.WebStateFilter</filter-class><init-param><param-name>exclusions</param-name><param-value>*.js,*.gif,*.jpg,*.png, *.css,*.ico,/druid/*</param-value></init-param><init-param><param-name>profileEnable</param-name><param-value>true</param-value></init-param></filter>

在Filter的初始化代码中发现了处理逻辑,这个URI统计信息共享的变量是在这个时候被添加到了WebAppStatManger中去的!

this.contextPath = DruidWebUtils.getContextPath(config.getServletContext());
if (webAppStat == null) {webAppStat = new WebAppStat(contextPath, this.sessionStatMaxCount);
}
WebAppStatManager.getInstance().addWebAppStatSet(webAppStat);

仔细看这个类的主要的成员变量在结合WebStatFilter中的过滤的逻辑,很明显的知道当前的webAppStat这个变量就是前端展示数据中统计的信息的实例。
这里写图片描述

3、跟踪WebStatFilter中WebAppStat 实例数据

断点跟中WebStatFilter中的拦截的逻辑,看一下共享的WebAppStat中是否存在有数据信息,是否正确,来验证为啥没有数据信息。经过几次的断点跟踪,发现使用来统计信息的数据啊!都是存在的就是显示不出来啊!很是奇怪。
这里写图片描述

断点跟踪webAppStat实例,发现统计的信息在当前实例中都是存在的,而且有数据!证明至少从配置层面来说,当前的配置是正确的,统计数据存在。
前端请求:http://localhost:8080/druid/weburi.html,没有数据也是事实存在的!很懵,断点跟踪发现,Set中的数据不存在!这个Set集合也就是WebStatFilter 在初始化的时候向WebAppStatManager添加了数据的!为啥在StatViewServlet 这个展示数据的前端中无法看到数据?
这里写图片描述

从Set的数据源从哪里来我们可以知道,在过滤器初始化的时候确实向WebAppStatManager单例中添加了数据,而且根据我们的跟踪数据信息确实存在!为什么在统计数据的时候Set集合中就不存在数据信息啦?首先第一点:WebAppStatManager中实例为单例模式的,拥有add的方法,是不是其他的地方进行了删除?带着这个疑问查看了WebAppStatManger中的处理Set集合的所有的方法!

这里写图片描述

只有在监控的Filter销毁的时候才进行删除,答案很明显,Set集合的消失是不存在的?那为何在前端访问无法获取数据信息?
这里写图片描述

4、就在我觉得不可思议的时候,师兄指点发现是类加载机制造成单例非绝对的单例。一个JVM中两个类加载器加载同一个Class,造成在不同的地方,不同的ClassLoader访问到的添加的数据不存在,因为两个ClassLoader加载的WebAppManager中的数据,因为ClassLoader+类名称才唯一确定一个Class实例哦!非绝对的单例

过滤器监控:过滤器监控中WebAppStatManager中有数据,且类加载机制为Tomcat(这个Tomact的类加载机制被改写过)
这里写图片描述

前端访问StatViewServlet 中,查看WebAppManager中的数据为另外的一个ClassLoader加载的,因为之前添加到WebAppManager中的不是在这个ClassLoader中所有无法访问得到哦。因此Set集合中没有数据信息。
这里写图片描述

由此得出了没有数据的结论:访问数据和采集的数据根本就不是同一个单例的实例,所以造成数据统计无法显示的问题!

解决问题

原以为是单例模式,原来在JVM的世界里是由ClassLoader+Class 才唯一标识一个类。既然我们已经知道了问题,就是由于TDDL这个ClassLoader中Set集合中没有数据造成前端访问没有数据的问题,只要将Tomcat中有统计信息的webAppStat信息添加到Set集合中就搞定了!带着这样的思路,基本上问题就可以解决了

思路有了,如何解决呢?

1、怎么获取到Tddl这个ClassLoader下的WebAppManager的实例,然后添加数据哦?

第一步:首先获取到tddl这个ClassLoader的实例信息;

//这个Clas的字节码是Tddl这个ClassLoader加载的!
ClassLoader duridMouldeClassLoader = DruidDataSource.class.getClassLoader();

第二步:通过这个ClassLoader加载到WebAppManager的Class字节码信息,由于这个是一个静态的单例类,我们可以通过反射获取到当前ClassLoader下的静态实例。

//通过Tddl这个ClassLoader加载这个WebAppManager这个字节码,然后初始化静态变量
//这一步要好好的理解哦!使用特定的ClassLoader加载Class字节码Class<?> webAppStatManager = Class.forName("com.alibaba.druid. support.http.stat.WebAppStatManager", true, duridMouldeClassLoader);//获取静态的fieldField field = ReflectionUtils.findField(webAppStatManager, "instance");field.setAccessible(true);//反射获取到静态的实例,由于静态实例为类所有,所以可以支持获取到其实例的值。Object webAppStatManagerObject = ReflectionUtils.getField(field, null);

第三步:获取当Tomact下的共享的WebAppStat这个统计的数据信息,然后添加到Tddl这个ClassLoader下面的WebAppManager 的静态的实例中Set集合中去。
需要继承之前的WebStatFilter才能获取到WebAppStat这个实例的变量,其他的就简单咯。

/*** durid 统计信息无法访问,处理!由于两个不同的ClassLoader造成数据访问问题的处理;* 一个是Tomcat的ClassLoader;一个是Pandora 模块化加载的ClassLoader* {@link com.alibaba.druid.support.http.StatViewServlet#process(String)}** @author wangji*/
public class WebStateFilterEx extends WebStatFilter {private static Logger logger = LoggerFactory.getLogger(JmonitorCustomListener.class);@Overridepublic void init(FilterConfig config) throws ServletException {ClassLoader duridMouldeClassLoader = DruidDataSource.class.getClassLoader();super.init(config);try {Class<?> webAppStatManager = Class.forName("com.alibaba.druid.support.http.stat.WebAppStatManager", true, duridMouldeClassLoader);Field field = ReflectionUtils.findField(webAppStatManager, "instance");field.setAccessible(true);Object webAppStatManagerObject = ReflectionUtils.getField(field, null);Method method = ReflectionUtils.findMethod(webAppStatManager, "addWebAppStatSet", Object.class);ReflectionUtils.makeAccessible(method);ReflectionUtils.invokeMethod(method, webAppStatManagerObject, this.webAppStat);} catch (Exception e) {logger.error("load class error", e);}}
}

出现问题的原因

一个类里面的类加载由加载这个类的类加载器来加载 需要理解
由于内部使用了一个轻量级别的隔离隔离,能够让中间件之间隔离,中间件和应用之间隔离。这样对于不同的中间件可以使用不同的JAR包 ,一个使用FastJson1.0 一个使用2.0之间互不影响的!因为他们使用不同的ClassLoader加载不同的中间件。这个就能解决应用中的JAR包冲突。在JVM中,一个类型实例是通过它的全类名和加载它的类加载器(ClassLoader)来唯一确定的 。因此,如果要做到“隔离”,就让不同的类加载器去加载需要隔离的类就可以了。但是有时候应用中加载某些Class的时候需要使用中间件中的CLassLoader使用同一个Class,这个就有了共享Class的概念。
这里写图片描述

上图很形象的体现了Tomcat下结合隔离容器 类加载的先后顺序,隔离容器中有很多的模块,每一模块中都有一个类加载器,可以共享出来给应用中使用,比如DruidDataSource.class这个类被隔离容器共享出来,应用程序中使用时候加载顺序为首先查找隔离容器导出的类,不存在再去寻找webapp下的类,然后按照Tomcat类加载器逻辑去加载。有了这些理解,就可以追踪刚刚那个现象的产生到底是为什么?

当前tdd中间件中druid中的包被导出的包名称为
这里写图片描述

com.alibaba.druid.support.http.WebStatFilter 非导出类
com.alibaba.druid.support.http.StatViewServlet 非导出Class
com.alibaba.druid.support.http.stat.WebAppStatManager 非导出Class

com.alibaba.druid.stat.DruidStatService 导出Class

还原问题

监控Filter:WebStatFilter非导出Class,所以由Tomcat加载,当前类中的WebAppManager也是非导出类因此也是由Tomcat加载,问题还原成功。

统计信息查看Servlet:StatViewServlet非导出Class,所以由Tomcat加载,但是内部DruidStatService为导出Class;按照:一个类里面的类加载由加载这个类的类加载器来加载,那么DruidStatService这个StatViewServlet的成员变量也是应该有Tomcat这个ClassLoader加载的,怎么会变为TDDL这个加载器了? 大概的意思就是Tomcat的类加载器加载Class的时候会进行代理,首先去寻找隔离容器中导出的Class,如果没有在去寻找自己的,因此这个时候DruidStatService由TDDL这个ClassLoader进行加载,内部的所有的成员变量都是由TDDL这个CLassLoader加载,因为这个ClassLoader不再被代理啦,所以当前类中的WebAppManager由TDDL加载,这个就造成了访问前端没有数据出现的原因,JVM中存在两个WebAppManager的Class字节码。

类加载机制

Java默认提供三个类加载器,按照层次关系( 非继承关系,只是类加载器的父子关系,通过classloader.getParent()获取父类加载器 )如下图所示
这里写图片描述

其中,
Bootstrap Class Loader: 启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等;
Extension ClassLoader: 扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar;
App/System Class Loader: 系统类加载器,负责加载应用程序CLASSPATH目录下的所有jar和class文件。

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要实现自定义的 Custom ClassLoader ,这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类。Java提供的两个ClassLoader:ExtClassLoader和AppClassLoader也都继承自java.lang.ClassLoader,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造ExtClassLoader和AppClassLoader。

双亲委派

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个类加载器在执行loadClass()操作时,会首先将加载类的任务委派给自己的父classloader,而父classloader在执行loadClass()操作时,又会继续委派给它的父classloader,依次向上…直到最顶层的Bootstrap ClassLoader。如果某个层次上的父classloader可以成功加载到目标class,则返回;只有当父classloader加载不到目标class时,才会由当前的classloader进行加载。

而在Tomcat等应用容器中,则采用了另一种类加载机制:反双亲委派。即某个类加载器在执行loadClass()操作时,会首先尝试从自己的ClASSPATH下加载类,只有在加载不到时,才会委派给父classloader帮他加载。这样,就能保证应用加载到的一些三方jar中的类,是自己应用lib目录下依赖的版本了

说明:
应用容器在实现反双亲委派时,会对Java核心类库中的类按照全类名进行过滤,保证Java核心类库中的类是从Bootstrap ClassLoader加载到的。

中间件之间的隔离,通过为每一个中间件插件创建一个 ModuleClassLoader 进行类加载,来实现“中间件与中间件”的隔离。而“中间件与应用”的隔离则是天然的通过应用容器的类加载器来实现的。需要共享的时候导出Class,Tomact在加载的时候首先访问导出的Class。
这里写图片描述

Bootstrap ClassLoader: 相当于Java中默认类加载器中的 Bootstrap ClassLoader + Extension ClassLoader ,加载JVM运行环境所需的核心类库和$JAVA_HOME/jre/lib/ext目录下的扩展类库;
System ClassLoader: 相当于Java默认类加载器中的System/App Class Loader,其加载的所有类对Tomcat自身和Web Apps都可见。然而,在Tomcat启动脚本(catalina.bat/catalina.sh)中却无视了CLASSPATH环境变量本身,而是指定了以下三个jar:

$CATALINA_HOME/bin/bootstrap.jar 
— Contains the main() method that is used to initialize the Tomcat server, 
and the class loader implementation classes it depends on.
$CATALINA_HOME/bin/tomcat-juli.jar— Logging implementation classes.  These include enhancement classes to java.util.logging API,  known as Tomcat JULI, and a package-renamed copy of Apache Commons Logging library used internally by Tomcat.
$CATALINA_HOME/bin/commons-daemon.jar  
— The classes from Apache Commons Daemon project.

Common ClassLoader: CommonLoader加载的类对于Tomcat和Web Apps都是可见的,其加载路径由catalina.properties中的common.loader、shared.loader、server.loader指定,默认值为如下:

common.loader=${catalina.base}/lib, 
${catalina.base}/lib/*.jar, 
${catalina.home}/lib, 
${catalina.home}/lib/*.jar
shared.loader=
server.loader=

WebAppClassLoader: WebApp Class Loader是Tomcat为每一个Web App创建的类加载器,用于加载 /WEB-INF/classes 和 /WEB-INF/lib 下的类文件和JAR包,WebApp Class Loader加载的类文件只针对当前web应用可见。
修改Tomcat扩展了 WebAppClassLoader,构造了自己的 WebappClassloader,来对应用类加载过程做一些干预。Tomcat在启动过程中,会反射调用 中间件隔离的的启动类,完成隔离容器的启动。启动过程根据配置将ExportClass 放置在WebappClassLoader的成员变量commonRepository中。
当webapp需要进行类加载时,它所对应的 WebappClassloader实例会首先尝试从commonRepository中进行类加载 ,如果加载到,就返回;若加载不到,才会继续按照Apache Tomcat的原有反双亲委派类加载流程进行类加载。从而保证应用加载到的中间件的类是包中,而非自己lib目录下依赖的。

这篇关于记录Druid 监控URL数据、Spring方法没有数据排查过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1091582

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M