性能监控:jvm+cpu+目标field自定义类加载器+Java agent+反射实现对tomcat的零侵入式服务监控
Posted Cry丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了性能监控:jvm+cpu+目标field自定义类加载器+Java agent+反射实现对tomcat的零侵入式服务监控相关的知识,希望对你有一定的参考价值。
前言
最近项目中有一个需求,需要临时监控一下一个部署在tomcat中的服务的jvm性能和cpu性能,这个服务中有一个内存队列queue,存储的是消费kafka后的数据,也需要对其进行大小的监控,来判断是否存在消息积压,从而判断是否需要进行性能的调优或者扩服务。
市面上已经有很多成熟的大型项目的监控方案了:例如可以用prometheus或者arthas来实现各种可定制的监控方案,我会在后面抽空补充下这些常用的开源监控组件的使用方案,但是这些方案都有个很明显的问题,就是部署起来太重,而我现在只需要快速且轻量的临时解决,所以最后决定用jdk自带的java agent来实现,其实上面那些提到的大型开源监控组件底层也是用到了java agent。
在实际开发的过程中,比想象中要困难些,我会逐步分析下遇到的难点,具体单个功能的实现,比如如何实现一个自定义的类加载器,如何java agent的api如何使用,网上教程有很多,我这边也会贴一些链接给大家。
简单的java agent实现
通过对 Java Agent 以及相关 API,我想大家应该想到一种 JVM Agent 的设计方案,基本思路就是利用 Java Agent 的先于 main 方法执行而且无需修改应用程序源代码的特性,实现一个 Java Agent 的 premain 方法,并且在 premain 中启动一个独立线程,该线程负责定时通过 java.lang.management 包提供的 API 收集JVM等性能数据并打包上报,如下图所示:
java agent参考代码:
public static void premain(String agentArgs, Instrumentation inst)
new Thread(() ->
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
while (true)
Class[] allLoadedClasses = inst.getAllLoadedClasses();
System.out.println(allLoadedClasses.length + "====");
for (Class allLoadedClass : allLoadedClasses)
if (allLoadedClass.getName().equals(ServiceConstant.COM_AWIFI_ATHENA_DATASERVICE_CORE_NBIOT_SERVICE_QUEUE_COLLECTQUEUE))
Field test = null;
try
test = allLoadedClass.getDeclaredField(ServiceConstant.KAFKA_COLLECT_1);
LOGGER.info("nb-iot服务监听的队列名 = " + test.getName());
Object o = test.get(allLoadedClass);
if (o instanceof BlockingQueue)
BlockingQueue queue = (BlockingQueue) o;
Jedis jedis = getJedis();
LOGGER.info("nb-iot服务队列大小 = " + queue.size());
String ATHENA_NB_IOT_QUEUE = getProperty("actuator.properties", RedisConstant.ATHENA_NB_IOT_QUEUE);
jedis.set(ATHENA_NB_IOT_QUEUE, String.valueOf(queue.size()));
catch (NoSuchFieldException e)
LOGGER.error("没有这个字段 = " + test.getName());
catch (IllegalAccessException e)
LOGGER.error("获取字段对象发生异常 = " + test.getName(), e.getMessage(), e);
try
printJvmInfo();
catch (Exception e)
LOGGER.error("监听jvm性能发生异常 = " + e.getMessage(), e);
try
printlnCpuInfo();
catch (Exception e)
LOGGER.error("监听cpu性能发生异常 = " + e.getMessage(), e);
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
).start();
看上去似乎这种设计方案就可以满足我们的要求了,是真的如此吗?实际上,基于这种设计方案实现的监控 Agent 接入到普通的简单 Java 应用程序是可以胜任工作的,JVM 的性能数据能够被成功的采集并且上报。
但是,考虑到我们将应用到生产环境,需要监控的运行于 JVM 之上的应用程序有:Tomcat,Resin,Spark,Hadoop,ElasticSearch等等。这些不同的应用程序的运行环境各有差别,那么我们设计开发的 JVM 性能监控 Agent 必须考虑他们之间的兼容性。
ClassNotFoundException 问题
使用类似Tomcat的Web容器来运行我们的应用程序,会产生ClassNotFoundException的问题,具体原因简单的说就是因为Tomcat实现了自己的类加载器,打破了双亲委派模型,在Tomcat中的应用的Class加载路径都会去WEB-INF/lib路径下寻找并加载,而java agent始终默认调用的是ApplicationClassLoader,是一个系统类加载器,所以在指定java agent启动的Web容器的时候,会导致找不到java agent中所依赖的包。解决方案就是实现一个自定义的类加载器,去指定目录下加载自己的jar包。
导致ClassNotFoundException的具体原因可以了解这篇:JVM性能监控Agent设计实现(二)
实现一个自定义的类加载器加载jar包
自定义类加载器参考代码:
加载jar包jdk为我们提供了一个自带的工具jarFile
public class JarClassLoader extends ClassLoader
public JarFile jarFile;
public ClassLoader parent;
public JarClassLoader(JarFile jarFile)
super(Thread.currentThread().getContextClassLoader());
this.parent = Thread.currentThread().getContextClassLoader();
this.jarFile = jarFile;
public JarClassLoader(JarFile jarFile, ClassLoader parent)
super(parent);
this.parent = parent;
this.jarFile = jarFile;
/**
* 转换类加载名
* @param name: com.awifi.athena.agent.PreMainAgent
* @return java.lang.String: com/awifi/athena/agent/PreMainAgent.class
*/
public String classNameToJarEntry(String name)
String s = name.replaceAll("\\\\.", "\\\\/");
StringBuilder stringBuilder = new StringBuilder(s);
stringBuilder.append(".class");
return stringBuilder.toString();
/**
* 转换类加载名
* @param name: com.awifi.athena.agent.PreMainAgent
* @return java.lang.String: com/awifi/athena/agent/PreMainAgent.class
*/
public String classNameToProperties(String name)
String s = name.replaceAll("\\\\.", "\\\\/");
StringBuilder stringBuilder = new StringBuilder(s);
stringBuilder.append(".properties");
return stringBuilder.toString();
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException
try
Class c = null;
if (null != jarFile)
String jarEntryName = classNameToJarEntry(name);
JarEntry entry = jarFile.getJarEntry(jarEntryName);
if (null != entry)
InputStream is = jarFile.getInputStream(entry);
int availableLen = is.available();
int len = 0;
byte[] bt1 = new byte[availableLen];
while (len < availableLen)
len += is.read(bt1, len, availableLen - len);
c = defineClass(name, bt1, 0, bt1.length);
else
if (parent != null)
return parent.loadClass(name);
return c;
catch (IOException e)
throw new ClassNotFoundException("Class " + name + " not found.");
@Override
public InputStream getResourceAsStream(String name)
InputStream is = null;
try
if (null != jarFile)
JarEntry entry = jarFile.getJarEntry(name);
if (entry != null)
is = jarFile.getInputStream(entry);
if (is == null)
is = super.getResourceAsStream(name);
catch (IOException e)
// logger.error(e.getMessage());
System.out.println(e.getMessage());
return is;
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException
JarClassLoader jarClassLoader = new JarClassLoader(new JarFile(new File("D:\\\\test\\\\com\\\\awifi\\\\athena\\\\agent\\\\athena-agent-1.0.0-jar-with-dependencies.jar")));
Enumeration<JarEntry> entries = jarClassLoader.jarFile.entries();
while (entries.hasMoreElements())
String name = entries.nextElement().getName();
System.out.println(name + "-=-=-=-=");
Class clazz1 = jarClassLoader.loadClass("com.awifi.athena.agent.PreMainAgent");
Object obj1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("printJvmInfo", null);
method1.invoke(obj1, null);
System.out.println(clazz1.getClassLoader().getClass().getName());
存储逻辑和java agent的入口分别单独成包
要时刻记得,我们的核心思路就是在java agent的入口处,用我们自定义的ClassLoader去加载我们的存储逻辑的jar包,加载进来后获取到类名,然后通过反射生成一个目标对象,就可以实现解耦了。
/**
* 创建目标agent实例
* @param agentClassLoader
* @param agentEntryClass
* @return com.awifi.athena.agent.PreMainAgent
*/
public static AgentInterface createAgentInstance(ClassLoader agentClassLoader,String agentEntryClass) throws Exception
AgentInterface agentInstance = null;
Thread currentThread = Thread.currentThread();
ClassLoader beforeClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(agentClassLoader);
try
Class<?> agentClass = agentClassLoader.loadClass(agentEntryClass);
Constructor<?> constructor = agentClass.getDeclaredConstructor();
Object instance = constructor.newInstance();
if (instance instanceof AgentInterface)
agentInstance = (AgentInterface) instance;
finally
currentThread.setContextClassLoader(beforeClassLoader);
return agentInstance;
注意的细节点
要注意的细节点1:反射创建对象时,可以用目标类的接口来接收,这是因为不能直接引入这个目标类
要注意的细节点2:使用jedis对象池操作redis的时候,使用完要回收对象,否则消耗完会导致线程阻塞,jedis的优化可以参考这篇文章:JedisPool资源池优化
以上是关于性能监控:jvm+cpu+目标field自定义类加载器+Java agent+反射实现对tomcat的零侵入式服务监控的主要内容,如果未能解决你的问题,请参考以下文章