性能监控: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的零侵入式服务监控的主要内容,如果未能解决你的问题,请参考以下文章

JVM系统性能监控总结

值得收藏 一文说尽运维监控

JVM性能调优监控工具

JVM性能监控

Tomcat性能监控与调优

性能测试之JVM监控