记录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);
//获取静态的field
Field 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);
@Override
public 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目录下依赖的。
更多推荐
所有评论(0)