1. 背景
druid本身提供了监控功能,具体在我另一篇博文《Druid连接池监控》里有介绍。当时提到有以下缺陷:
- 无法灵活监控多个目标
- 切换环境不方便
- JMX重连不会成功
因此针对这些问题,对其进行改造。改造后的源码已经放在个人的github上:
https://github.com/bungder/druid-aggregated-monitor
对应本文的版本,已经打了tag:
https://github.com/bungder/druid-aggregated-monitor/releases/tag/0.0.1
对于在同一个工程里进行监控和展示的情况不进行考虑,具体原因见刚刚提到的博文。
2. 原理分析
首先,监控数据的展示是通过com.alibaba.druid.support.http.StatViewServlet
实现的,将其源码贴上来分析:
1 |
|
首先,现在需要搞清楚的问题有:
- 配置信息是如何生效的
- 监控数据是怎么流动的
- 权限控制是怎样实现的
- 为什么重连会失败
逐个方法去看,init
方法是初始化的,应该能找到『配置信息是如何生效的』的答案。里面调用了readInitparam
方法来读取,而这个方法又调用了getInitParameter
方法,进入方法后发现此方法是javax.servlet.GenericServlet
里的,已经不是druid的代码,意味着读取参数是通过调用容器的api实现的,这个过程无法进行篡改。
这里只是读取参数值,还没使用,让我们一步步回退回init
方法,在读取了参数值之后就调用initJmxConn
方法,该方法初始化了与监控目标之间的JMX连接,是关键的地方。但是里面也没多少东西,主要就是根据url去获取连接,对于『为什么重连会失败』,应该也是一个切入点。但是一路点进去看都没发现有重试的机制。
接着往下看,剩下getJmxResult
和process
两个方法,其注释里已经讲得很明白了。可以发现,在process
方法里有重连的机制,那么还是没搞清楚为什么无法重连成功。
在看完这个类之后,可以发现请求的调用链并没有体现出来,所以看它的父类ResourceServlet
。
1 | /* |
其中,service
方法里有URL的判断,方法里有request和response,看上去就是流程的起点,但是一般我们写servlet都是从doGet
和doPost
入手的,这里面不知道做了什么封装,于是继续往父类去看,发现其父类是javax.servlet.http.HttpServlet
,已经是J2EE定义的类了,我用的是tomcat容器,所以这个类由tomcat提供。里面有常见的doPost
和doGet
方法
可以看到,doGet
和doPost
方法默认都是不通的:
1 |
|
赶紧去看注释:
1 |
|
原来这些方法是由service
方法调用的,并且需要自己覆盖,很符合我们一贯的经验。再看看service
方法是怎么回事:
1 |
|
这个方法是请求的统一接收入口,然后将请求分发到doGet
、doPost
、doHead
等对应标准HTTP请求方法的方法去。注释里特地说明了没有必要覆盖这个方法,druid的开发者很直接粗暴,不管你请求方法是什么,全部一刀切,反正这玩意儿要求不高。总之,现在我们知道了,service
方法就是请求的入口,这样我们再回去看看com.alibaba.druid.support.http.ResourceServlet
的service
方法,通过这个方法应该就能理顺整个流程。
将其方法代码加上我自己的注释贴出来:
1 |
|
有两个方法需要看:
- returnResourceFile
- process
returnResourceFile
方法在ResourceServlet
里面实现了(注释是我加的):
1 | protected void returnResourceFile(String fileName, String uri, HttpServletResponse response) throws ServletException, IOException { |
接着看process方法(注释是原有的):
1 |
|
可见它是有重连的,而保证了连接成功之后,获取数据的方法是getJmxResult
,这个是在StatViewServlet
里面实现的:
1 | private String getJmxResult(MBeanServerConnection connetion, String url) throws Exception { |
所以实际上就是用MBeanServer的连接去直接取数据然后原样返回,所有的监控数据其实是缓存在被监控的目标处的,web的监控只是一个请求转发与展示的作用。
所以现在总结StatViewServlet整个工作的主要过程:
- 记录用户名和密码
- 根据配置的jmxUrl初始化jmx连接
- 接收请求,分发请求
- 如果请求是json数据请求,则通过jmx连接到被监控对象处取数据,然后返回
3. 改进思路
3.1 思路一:动态创建并注册StatViewServlet
这是首先想到的思路,因为使用这种办法不需要对druid的web监控细节了解多少。要实现这个目标,需要做到以下两点之一:
- 对于Java Web容器的启动过程很了解,并且深入细节
- Google能找到相似的例子
第一点我还做不到,短时间内也做不到,所以只能往第二点去努力。找到了一个最贴切的办法是:
Dynamic Servlet Registration Example
1 | import java.util.Map; |
主要就是实现javax.servlet.ServletContextListener
,通过javax.servlet.ServletContextEvent
实例获取javax.servlet.ServletContext
实例,然后调用它的方法去注册新的servlet。
看上去好像可行的样子,但是在实际运行起来之后,在这一行报错了:
1 | final ServletRegistration.Dynamic dynamic = servletContext.addServlet("Example Servlet", ExampleServlet.class); |
错误信息没有记录下来,但是意思就是说这个操作是不支持的,反正就是没戏。具体为什么,还需要进一步了解。
3.2 思路二:修改StatViewServlet的机制
在第一个思路走不通之后,只能从其工作机制上入手。其原理分析已经在上文给出。
因为只要有JMX的连接就可以获取数据了,所以关键在于以下几点:
- 持有多个jmx连接并且与不同的请求关联起来
- 根据配置去动态创建连接
- 将原本固定的几个页面与配置的多个监控对象动态地对应起来
- 配置能根据部署环境的不同而改变,并且发生变更的时候能轻易修改
- 列出所有被监控对象
对于第一点,创建jmx连接只要有jmxUrl就够了,所以很容易做到,至于与请求关联起来,其实就是从请求的url里提取特征,用于表示不同的监控对象,然后将此特征映射到对应的jmx连接即可。
对于第二点,这其实就是普通的读取、解析配置,然后用配置信息去初始化jmx连接(当然还有登录名、密码和黑白名单等)。
第三点,转下弯,原有的逻辑是将url直接映射为资源文件,只要在这中间加一层解析即可。
第四点,简单的方案是配置多个配置文件,根据不同的环境打不同的包。但是这种做法不灵活,最好还是做成注册中心的形式,被监控对象启动的时候网注册中心写入信息,这边从注册中心读,还有下线机制。但是这种做法工作量大,而且要改被监控的一方,容易引入bug。要不就与配置中心集成,这样就只需改动web监控一端即可。这里的配置方案有多种,很适合采用SPI。
第五点,根据配置信息做个汇总,然后给个页面列出来就可以。
4 实现
上文提及的问题在这里基本上都解决了,思路都讲清楚了,实现就不再重复讲。请移步我github的仓库:https://github.com/bungder/druid-aggregated-monitor
在这里提提失败重连的问题
4.1重连失败的问题
其重连失败的问题,在debug的时候发现其实并没有重连,它重连的条件是conn为null,但是实际上conn初始化之后就不会为null了,但是当连接失效之后,里面的terminated
属性为true
,而MBeanServerConnection
是一个interface
,本身没定义操作这个属性的方法,并且至少有两个类实现了这个接口,运行时的实际类型并不确定是否总是某个实现类型,所以也不好去强转类型进行操作。但是可以利用它本身的逻辑:既然它触发重连的条件是conn为null,我就将它设成null好了。当conn不为null但是获取数据又出错的时候,就可以判断连接有问题,不妨触发重试,即使这种情况下不一定是连接失效了,但是正常情况下不会出现这种现象,就将其当成是连接失效也无妨。