【性能监控: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资源池优化

猜你喜欢

转载自blog.csdn.net/haohaoxuexiyai/article/details/123297412
今日推荐