通过这篇文章,我们可以了解到,利用 JMX 技术可以方便获取 Tomcat 监控情况。但是我们采用自研的框架而非大家常见的 SpringBoot,于是就不能方便地通过设置配置开启 Tomcat 的 JMX,——尽管我们也是基于 Tomcat 的 Web 容器,而是还是 SpringMVC。
在笔者一番尝试下,终于实现了“Enable Embedded Tomcat JMX Programmatically”,所谓 Programmatically 就是编程式的用 Java 代码去配置。实际情况也很简单,就是在 Tomcat 启动的LifecycleEvent
事件中加入:
context.addLifecycleListener((LifecycleEvent event) -> {
if (isStatedSpring || (event.getLifecycle().getState() != LifecycleState.STARTING_PREP))
return;
BaseWebInitializer.coreStartup(context.getServletContext(), clz);
// anotherWayToStartStrping();
if (isEnableJMX) {
try {
LocateRegistry.createRegistry(9011);
JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(
new JMXServiceURL("service:jmx:rmi://localhost/jndi/rmi://localhost:9011/jmxrmi"),
null,
ManagementFactory.getPlatformMBeanServer()
);
cs.start();
LOGGER.info("成功启动 JMXConnectorServer");
} catch (IOException e) {
e.printStackTrace();
}
}
isStatedSpring = true;
springTime = System.currentTimeMillis() - startedTime;
});
要注意的是端口的配置,当前是 9011。
另外如果要鉴权,把newJMXConnectorServer()
的第二个参数environment
由null
改为一个 map。
哦,对了,还有 Maven 的依赖,——貌似 8 最高的也就这个版本。
<!-- 监控用 -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina-jmx-remote</artifactId>
<version>8.5.75</version>
<type>jar</type>
</dependency>
后来测试好像不用这个依赖也行。
连接 JMX
刚才的配置就像一个服务端,而接着我们这一步就相当于是客户端的连接。先准备好连接地址:
String jmxURL = "service:jmx:rmi:///jndi/rmi://127.0.0.1:9011/jmxrmi";
// 如果要鉴权,还要配置下面的,传入到 environment
Map<String, String[]> map = new HashMap<>();
String[] credentials = new String[]{"monitorRole", "tomcat"};
map.put("jmx.remote.credentials", credentials);
进行连接:
MBeanServerConnection msc = JMXConnectorFactory.connect(new JMXServiceURL(jmxURL)).getMBeanServerConnection();
分门别类地查询
成功连接后,会返回大量的信息。JMX 提供了一种 Domain 命名空间的概念,是为第一的大分类。我们可以打印 Domains 出来,再用getObjectNamesByDomain()
列出子类 :
for (String domain : msc.getDomains())
System.out.println(domain);
List<Node> tomcat = MonitorUtils.getObjectNamesByDomain(msc, "Tomcat");
Node 是我们对结果的封装,实际最重要是里面的fullName
即对应 JMX API 的CanonicalName
,它就是对象名称 ObjectName,以此来获取具体的属性。
ObjectName threadObjName = new ObjectName("Tomcat:name=\"http-nio-8301\",type=ThreadPool");
System.out.println("currentThreadCount:" + msc.getAttribute(threadObjName, "currentThreadCount"));// tomcat的线程数对应的属性值
因为是自定义的 Tomcat,所以 ObjectName 会不一样。常见的 Tomcat 是
ObjectName threadObjName = new ObjectName("Catalina:type=ThreadPool,name=http-8301");// 端口最好是动态取得
于是就必须通过前面说的getObjectNamesByDomain()
“人肉”查找。另外也要注意 Tomcat 的端口配置。
获取 Tomcat 监控
只要能成功连接并获取 JMX 信息,下一步就是将其转换为监控信息渲染到前端。
测试代码:
public class TestTomcat {
String jmxURL = "service:jmx:rmi:///jndi/rmi://127.0.0.1:9011/jmxrmi";
@Test
public void testConnectJMX() {
TomcatInfo info = new TomcatJmx().getInfo(jmxURL);
TestHelper.printJson(info);
}
}
返回结果
完整源码
package com.ajaxjs.developertools.monitor;
import com.ajaxjs.developertools.monitor.model.tomcat.*;
import com.ajaxjs.util.DateUtil;
import com.ajaxjs.util.logger.LogHelper;
import com.ajaxjs.util.reflect.BeanUtils;
import org.springframework.util.CollectionUtils;
import javax.management.*;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.remote.JMXConnector;
import java.io.IOException;
import java.lang.management.MemoryUsage;
import java.util.*;
/**
* 获取 Tomcat 的 JMX 信息
*/
public class TomcatJmx extends JmxHelper {
private static final LogHelper LOGGER = LogHelper.getLog(TomcatJmx.class);
public TomcatInfo getInfo(String jmxURL) {
return getInfo(jmxURL, null);
}
public TomcatInfo getInfo(String jmxURL, Integer port) {
TomcatInfo info = new TomcatInfo();
try (JMXConnector connect = connect(jmxURL)) {
assert connect != null;
MBeanServerConnection msc = connect.getMBeanServerConnection();
setMsc(msc);
SystemInfo systemInfo = new SystemInfo();
ThreadPool threadPool = new ThreadPool();
info.jvmInfo = jvm();
info.systemInfo = systemInfo;
info.session = getSession();
info.threadPool = threadPool;
if (port == null) port = getTomcatPort(msc);
everyAttribute(objectNameFactory("Tomcat:name=\"http-nio-" + port + "\",type=ThreadPool"), (key, value) -> BeanUtils.setBeanValue(threadPool, key, value));
everyAttribute(objectNameFactory("java.lang:type=Runtime"), (key, value) -> {
if ("StartTime".equals(key)) {
Date startTime = new Date((Long) value);
BeanUtils.setBeanValue(systemInfo, key, DateUtil.formatDate(startTime));
} else if ("Uptime".equals(key)) {
Date startTime = new Date((Long) value);
BeanUtils.setBeanValue(systemInfo, key, formatTimeSpan((Long) value));
} else BeanUtils.setBeanValue(systemInfo, key, value);
});
} catch (IOException e) {
LOGGER.warning(e);
}
return info;
}
/**
* 获取 tomcat 运行端口
*/
private static int getTomcatPort(MBeanServerConnection msc) {
try {
Set<ObjectName> objectNames = queryNames(msc, "Tomcat:type=Connector,*");
if (CollectionUtils.isEmpty(objectNames))
throw new IllegalStateException("没有发现JVM中关联的MBeanServer : " + msc.getDefaultDomain() + " 中的对象名称.");
for (ObjectName objectName : objectNames) {
String protocol = (String) msc.getAttribute(objectName, "protocol");
if (protocol.equals("HTTP/1.1")) return (Integer) msc.getAttribute(objectName, "port");
}
} catch (MBeanException | AttributeNotFoundException | ReflectionException | InstanceNotFoundException |
IOException e) {
LOGGER.warning(e);
}
return 0;
}
private List<Session> getSession() {
Set<ObjectName> objectNames = queryNames("Tomcat:type=Manager,*");
List<Session> list = new ArrayList<>(objectNames.size());
for (ObjectName obj : objectNames) {
// List<Node> tomcat = JmxUtils.getObjectNamesByDomain(msc, "Tomcat");
// System.out.println("应用名:" + obj.getKeyProperty("path"));
// System.out.println("currentThreadCount:" + msc.getAttribute(threadObjName, "currentThreadCount"));// tomcat的线程数对应的属性值
Session session = new Session();
everyAttribute(objectNameFactory(obj.getCanonicalName()), (key, value) -> BeanUtils.setBeanValue(session, key, value));
list.add(session);
}
return list;
}
private JvmInfo jvm() {
try {
// 堆使用率
ObjectName heapObjName = objectNameFactory("java.lang:type=Memory");
MemoryUsage heapMemoryUsage = MemoryUsage.from((CompositeDataSupport) getMsc().getAttribute(heapObjName, "HeapMemoryUsage"));
// 堆当前分配
long commitMemory = heapMemoryUsage.getCommitted(), usedMemory = heapMemoryUsage.getUsed();
JvmInfo jvmInfo = new JvmInfo();
jvmInfo.setMaxMemory(heapMemoryUsage.getMax());
jvmInfo.setHeap(((Long) (usedMemory * 100 / commitMemory)).intValue());
MemoryUsage nonheapMemoryUsage = MemoryUsage.from((CompositeDataSupport) getMsc().getAttribute(heapObjName, "NonHeapMemoryUsage"));
long nonCommitMemory = nonheapMemoryUsage.getCommitted(), nonUsedMemory = heapMemoryUsage.getUsed();
jvmInfo.setNonCommitMemory(nonCommitMemory);
jvmInfo.setNonUsedMemory(nonUsedMemory);
jvmInfo.setNonHeap(((Long) (nonUsedMemory * 100 / nonCommitMemory)).intValue());
// ObjectName permObjName = new ObjectName("java.lang:type=MemoryPool,name=Perm Gen");
// MemoryUsage permGenUsage = MemoryUsage.from((CompositeDataSupport) getMsc().getAttribute(permObjName, "Usage"));
// long committed = permGenUsage.getCommitted();
// long used = heapMemoryUsage.getUsed();
//
// jvmInfo.setCommitted(committed);
// jvmInfo.setUsed(used);
// jvmInfo.setPermUse(((Long) (used * 100 / committed)).intValue());
return jvmInfo;
} catch (ReflectionException | AttributeNotFoundException | InstanceNotFoundException | MBeanException |
IOException e) {
LOGGER.warning(e);
}
return null;
}
private static String formatTimeSpan(long span) {
long minSeconds = span % 1000;
span = span / 1000;
long seconds = span % 60;
span = span / 60;
long min = span % 60;
span = span / 60;
long hours = span % 24;
span = span / 24;
long days = span;
try (Formatter formatter = new Formatter()) {
return formatter.format("%1$d天 %2$02d:%3$02d:%4$02d.%5$03d", days, hours, min, seconds, minSeconds).toString();
}
}
}
JmxHelpe:
package com.ajaxjs.developertools.monitor;
import com.ajaxjs.developertools.monitor.model.jvm.Node;
import com.ajaxjs.developertools.monitor.model.jvm.NodeType;
import com.ajaxjs.util.logger.LogHelper;
import org.springframework.util.StringUtils;
import javax.management.*;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.BiConsumer;
public class JmxHelper {
private static final LogHelper LOGGER = LogHelper.getLog(JmxHelper.class);
public JmxHelper() {
}
public JmxHelper(MBeanServerConnection msc) {
this.msc = msc;
}
/**
* 连接实例
*/
private MBeanServerConnection msc;
/**
* 连接对象
*/
private JMXConnector connector;
public MBeanAttributeInfo[] getAttributes(ObjectName objName) {
try {
return msc.getMBeanInfo(objName).getAttributes();
} catch (InstanceNotFoundException | IntrospectionException | ReflectionException | IOException e) {
LOGGER.warning(e);
return null;
}
}
public void everyAttribute(ObjectName objName, BiConsumer<String, Object> fn) {
MBeanAttributeInfo[] attributes = getAttributes(objName);
try {
for (MBeanAttributeInfo attribute : attributes) {
String key = attribute.getName();
Object value = msc.getAttribute(objName, key);
// System.out.println("private " + attribute.getType() + " " + key + ";");
fn.accept(key, value);
}
} catch (MBeanException | AttributeNotFoundException | InstanceNotFoundException | ReflectionException |
IOException e) {
LOGGER.warning(e);
}
}
/**
* 列出所有的对象。
* 下面方法同等效果
* <code>
* for (String domain : msc.getDomains())
* System.out.println(domain);
* List<Node> tomcat = MonitorUtils.getObjectNamesByDomain(msc, "Tomcat");
* </code>
*/
void listObjectInstance() {
try {
Set<ObjectInstance> MBeanset = msc.queryMBeans(null, null);
for (ObjectInstance objectInstance : MBeanset) {
ObjectName objectName = objectInstance.getObjectName();
String canonicalName = objectName.getCanonicalName();
System.out.println("canonicalName : " + canonicalName);
// 可以查询集群
// if (canonicalName.equals("Catalina:host=localhost,type=Cluster")) {
// // Get details of cluster MBeans
// // getMBeansDetails(canonicalName);
// String canonicalKeyPropList = objectName.getCanonicalKeyPropertyListString();
// }
}
} catch (IOException e) {
LOGGER.warning(e);
}
}
public List<Node> getObjectNamesByDomain(String domain) {
return getObjectNamesByDomain(msc, domain);
}
public static JMXConnector connect(String jmxUrl) {
try {
return JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl));
} catch (IOException e) {
LOGGER.warning(e);
return null;
}
}
public static JMXConnector connect(String host, String port) {
String jmxUrl = String.format("service:jmx:rmi:///jndi/rmi://%s:%s/jmxrmi", host, port);
return connect(jmxUrl);
}
/**
* 根据命名空间获取对象名称列表
* 可以先打印出所有的命名空间:
* <code>
* for (String domain : msc.getDomains())
* System.out.println(domain);
* </code>
*
* @param msc MBeanServerConnection
* @param domain 命名空间
* @return 对象名称列表
*/
public static List<Node> getObjectNamesByDomain(MBeanServerConnection msc, String domain) {
List<Node> nodes = new ArrayList<>();
Map<String, ObjectName> types = new HashMap<>();
for (ObjectName objectName : Objects.requireNonNull(queryNames(msc, domain + ":*"))) {
String type = objectName.getKeyProperty("type");
types.put(type, objectName);
}
for (Map.Entry<String, ObjectName> entry : types.entrySet()) {
Node node = new Node();
node.setTitle(StringUtils.hasText(entry.getKey()) ? entry.getKey() : entry.getValue().getCanonicalName());
node.setKey(entry.getValue().getCanonicalName());
node.setNodeType(NodeType.OBJECTNAME.getName());
node.setFullName(entry.getValue().getCanonicalName());
List<Node> subNodes = getObjectNamesByType(msc, domain, entry.getKey());
if (subNodes.size() > 1) node.setChildren(subNodes);
nodes.add(node);
}
return nodes;
}
private static List<Node> getObjectNamesByType(MBeanServerConnection msc, String domain, String type) {
List<Node> nodes = new ArrayList<>();
for (ObjectName objectName : Objects.requireNonNull(queryNames(msc, domain + ":type=" + type + ",*"))) {
Node node = new Node();
String fullName = objectName.getCanonicalName(), name = objectName.getKeyProperty("name");
node.setTitle(StringUtils.hasText(name) ? name : objectName.getCanonicalName());
node.setFullName(fullName);
node.setKey(fullName);
node.setNodeType(NodeType.OBJECTNAME.getName());
nodes.add(node);
}
return nodes;
}
public static SortedMap<String, Object> analyzeCompositeData(Object compositeData) {
try {
if (compositeData instanceof CompositeDataSupport) {
CompositeDataSupport support = (CompositeDataSupport) compositeData;
Field field = support.getClass().getDeclaredField("contents");
field.setAccessible(true);
return (SortedMap<String, Object>) field.get(support);
}
} catch (IllegalAccessException | NoSuchFieldException e) {
LOGGER.warning(e);
}
return null;
}
/**
* 实例化都抛出异常,麻烦
*/
public static ObjectName objectNameFactory(String name) {
try {
return new ObjectName(name);
} catch (MalformedObjectNameException e) {
LOGGER.warning(e);
return null;
}
}
public Set<ObjectName> queryNames(String name) {
return queryNames(msc, name);
}
public static Set<ObjectName> queryNames(MBeanServerConnection msc, String name) {
try {
return msc.queryNames(objectNameFactory(name), null);
} catch (IOException e) {
LOGGER.warning(e);
return null;
}
}
public MBeanInfo getMBeanInfo(ObjectName o) {
return getMBeanInfo(msc, o);
}
public static MBeanInfo getMBeanInfo(MBeanServerConnection msc, ObjectName o) {
try {
return msc.getMBeanInfo(o);
} catch (InstanceNotFoundException | IntrospectionException | ReflectionException | IOException e) {
LOGGER.warning(e);
return null;
}
}
public void setMsc(MBeanServerConnection msc) {
this.msc = msc;
}
public MBeanServerConnection getMsc() {
return msc;
}
public void setConnector(JMXConnector connector) {
this.connector = connector;
}
public JMXConnector getConnector() {
return connector;
}
}
使用限制
Tomcat 返回的监控信息比较完整,其实可以说覆盖了全部。但是有个致命的问题是,注册到 MBeanServer 需要一个端口!在旧时代的一个 Tomcat 挂多个服务,这个本不是问题,但刻下多流行 Spring Boot 的程序整合了 Tomcat,一个服务就是一个端口。分配端口也是个麻烦事情。
后来我在这篇文章中找一个获取 Tomcat 信息的方法,它是通过 Tomcat 自身 API 获取的,不是很全,只有寥寥几笔而已。
只能说聊胜于无。
/**
* 通过 Tomcat 自身 API 获取的实时信息
*
* @param tomcat Tomcat 实例
* @return
*/
public static Map<String, String> getTomcatSimplyInfo(Tomcat tomcat) {
ThreadPoolExecutor executor = (ThreadPoolExecutor) tomcat.getConnector().getProtocolHandler().getExecutor();
Map<String, String> returnMap = new LinkedHashMap<>();
returnMap.put("核心线程数(corePoolSize)", String.valueOf(executor.getCorePoolSize()));
returnMap.put("最大线程数(maximumPoolSize)", String.valueOf(executor.getMaximumPoolSize()));
returnMap.put("活跃线程数(activeCount)", String.valueOf(executor.getActiveCount()));
returnMap.put("池中当前线程数(poolSize)", String.valueOf(executor.getPoolSize()));
returnMap.put("历史最大线程数(largestPoolSize)", String.valueOf(executor.getLargestPoolSize()));
returnMap.put("工作队列任务数量(workQueue.size)", String.valueOf(executor.getQueue().size()));
returnMap.put("线程允许空闲时间/s(keepAliveTime)", String.valueOf(executor.getKeepAliveTime(TimeUnit.SECONDS)));
returnMap.put("核心线程数是否允许被回收(allowCoreThreadTimeOut)", String.valueOf(executor.allowsCoreThreadTimeOut()));
returnMap.put("提交任务总数(submittedCount)", String.valueOf(executor.getSubmittedCount()));
returnMap.put("历史执行任务的总数(近似值)(taskCount)", String.valueOf(executor.getTaskCount()));
returnMap.put("历史完成任务的总数(近似值)(completedTaskCount)", String.valueOf(executor.getCompletedTaskCount()));
returnMap.put("拒绝策略(ejectedExecutionHandler.class)", executor.getRejectedExecutionHandler().getClass().getCanonicalName());
return returnMap;
}
上述参数 Tomcat 可以通过 Spring Boot 的 ((TomcatWebServer) webServerApplicationContext.getWebServer()).getTomcat()
获取。
Tomcat JDBC Pool 监控
我们使用了 Tomcat JDBC Pool 作为数据连接池。Tomcat JDBC Pool 本身默认支持通过 JMX 获取监控信息,按照此文的方式即可简单获取之。可是我们通过独立创建了 DataSource 调用 JDBC Pool 就不会自动注册 JMX 组件。幸好官网文档介绍了一下,可以手动登记注册 JMX。
/**
* 手动创建连接池。这里使用了 Tomcat JDBC Pool
*
* @param driver 驱动程序,如 com.mysql.cj.jdbc.Driver
* @param url 数据库连接字符串
* @param userName 用户
* @param password 密码
* @return 数据源
*/
public static DataSource setupJdbcPool(String driver, String url, String userName, String password) {
PoolProperties p = new PoolProperties();
p.setDriverClassName(driver);
p.setUrl(url);
p.setUsername(userName);
p.setPassword(password);
p.setMaxActive(100);
p.setInitialSize(10);
p.setMaxWait(10000);
p.setMaxIdle(30);
p.setJmxEnabled(true);
p.setMinIdle(5);
p.setTestOnBorrow(true);
p.setTestWhileIdle(true);
p.setTestOnReturn(true);
p.setValidationInterval(18800);
p.setDefaultAutoCommit(true);
org.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();
ds.setPoolProperties(p);
// 一般来说 Tomcat 会自动注册。但是我们现在手动使用 Pool,于是也得手动地注册到 MBean
try {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName on = new ObjectName("org.apache.tomcat.jdbc.pool.jmx.ConnectionPool:type=Logging2");
server.registerMBean(ds.getPool().getJmxPool(), on);
} catch (Exception e) {
LOGGER.warning(e);
}
return ds;
}
这里的ObjectName
好像随便填都可以,但一定要按照其格式。成功注册后,查询MBeanClassName=org.apache.tomcat.jdbc.pool.jmx.ConnectionPool
就是连接池当前的情况。
原先文章介绍是 JSP 显示的。我们改为返回 Map。
@GetMapping("/jdbc_pool_status")
public Map<String, Object> jdbcPoolStatus() {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> objectNames = server.queryNames(null, null);
Map<String, Object> result = new HashMap<>();
try {
for (ObjectName name : objectNames) {
MBeanInfo info = server.getMBeanInfo(name);
if (info.getClassName().equals("org.apache.tomcat.jdbc.pool.jmx.ConnectionPool")) {
for (MBeanAttributeInfo mf : info.getAttributes()) {
Object attributeValue = server.getAttribute(name, mf.getName());
if (attributeValue != null)
result.put(mf.getName(), attributeValue);
}
break;
}
}
} catch (InstanceNotFoundException | IntrospectionException | ReflectionException | MBeanException |
AttributeNotFoundException e) {
throw new RuntimeException(e);
}
return result;
}
返回如下:
如果 JMX 不凑效,有没有其他手段呢?有人介绍说用 JDBC Pool 的拦截器,——那样也行。还有个大神的方法《自定义带监控的数据库连接池》,比较深入底层的说,没试过。
分享一个 MBeanInfo 浏览器
JSP 写的,简单易用,无它,就是暴力遍历所有的 Mean 及其字段。可以传入如?name=ConnectionPool
的参数指定某个 MBean。
<%@ page contentType="text/plain; charset=UTF-8" pageEncoding="ISO-8859-1" session="false"
import="java.io.*, java.util.*, java.net.*, javax.management.*, java.lang.management.ManagementFactory"
%>
<%!
private void dumpMBean(MBeanServer server, ObjectName objName, MBeanInfo mbi, Writer writer) throws Exception {
writer.write(String.format("MBeanClassName=%s%n", mbi.getClassName()));
Map<String,String> props=new HashMap<String,String>();
int idx=0;
for(MBeanAttributeInfo mf : mbi.getAttributes()) {
idx++;
try {
Object attr = server.getAttribute(objName, mf.getName());
if (attr!=null)
props.put(mf.getName(), attr.toString());
} catch(Exception ex) {
// sun.management.RuntimeImpl: java.lang.UnsupportedOperationException(Boot class path mechanism is not supported)
props.put("error_"+idx, ex.getClass().getName()+" "+ex.getMessage());
}
}
// sort by hashmap keys
for(String sKey : new TreeSet<String>(props.keySet()))
writer.write(String.format("%s=%s%n", sKey, props.get(sKey)));
}
%><%
// Dump MBean management properties, all beans or named beans
// dumpMBean.jsp?name=ConnectionPool,ContainerMBean
// dumpMBean.jsp?name=
if (request.getCharacterEncoding()==null)
request.setCharacterEncoding("UTF-8");
String val = request.getParameter("name");
String[] names = val!=null ? val.trim().split(",") : new String[0];
if (names.length==1 && names[0].isEmpty()) names=new String[0];
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
for(ObjectName objName : server.queryNames(null,null)) {
MBeanInfo mbi = server.getMBeanInfo(objName);
boolean match = names.length<1;
String name = mbi.getClassName();
for(int idx=0; idx<names.length; idx++) {
if (name.endsWith(names[idx])) {
match=true;
break;
}
}
if (match) {
dumpMBean(server, objName, mbi, out);
out.println("");
}
}
out.flush();
%>
返回如下: