A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区


如何开发一款java应用运行时的监控程序?

前言 每个程序员都或多或少遇到过相当多的疑难杂症问题排查的时刻。我自己也是工作中遇到许多稀奇古怪的问题。最开始我们排查问题使用的是jprofiler。特别是使用jprofiler来排查调用链路的耗时问题。

前言
每个程序员都或多或少遇到过相当多的疑难杂症问题排查的时刻。我自己也是工作中遇到许多稀奇古怪的问题。最开始我们排查问题使用的是jprofiler。特别是使用jprofiler来排查调用链路的耗时问题。如下图所示:
但是jprofiler只能用于排查一些本地的问题。对于一些生产环境的由于网络隔离在加上权限受限, jprofiler就不是那么好使了。这时候萌生了自己做个小工具的想法。同时参考了一些工具和apm的实现, 简单实现了所需的功能。
我们现在思考下, 假设要开发一个java程序的监控工具,比如包含以下功能, 都需要怎么实现?
[AppleScript] 纯文本查看 复制代码
1. 实时或周期性的获取java进程运行数据, 包括但不限于内存,线程,操作系统,GC等。
2. 如何在运行时知道一个class是被哪个classloader加载的?
3. 如何动态的知道一个方法的执行时间?(对于基础的排查性能问题很有用)
4. 如何动态知道一个方法被调用时候的完整调用栈?
5. 如何动态的知道一次调用下一个方法的入参,返回值?
...
基础部分
在这个【基础部分】里, 我们可以很轻松的解决上边的问题1。这要感谢JDK5后提供的两大神器:Instrument和management。前置提供了应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序, 后者可以实时获取应用程序的实时运行数据。
Agent
我们遇到的第一个问题, 是如何将自己的监控程序和目标进程关联起来。
比如我1个monitor.jar,里边包含了我们的监控程序, 如何和生产环境正在运行的tomcat进程进行关联?
答案是JDK提供的agent机制。简单来说只需要做以下事情:
[JavaFX] 纯文本查看 复制代码
1. 监控代码的jar中包含Agent-Class属性。该值的名字是自定义的agent类。
2. 该类必须实现如下的方法:
   public static void agentmain(String agentArgs, Instrumentation inst);
3. 使用VirtualMachine vm = VirtualMachine.attach(targetPid)关联到目标进程
其中Instrumentation非常重要, 后续还会说明。关于Agent-Class属性可以通过maven-assembly-plugin插件来设置:
[Java] 纯文本查看 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>single</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestEntries>
                        <Premain-Class>xxx</Premain-Class>
                        <Agent-Class>xxx</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </execution>
    </executions>
</plugin>
Instrumentation
核心内容都在java.lang.instrumentation包下, 主要的类有:
[Java] 纯文本查看 复制代码
public interface Instrumentation {
    ....
    java.lang.instrument.Instrumentation
    java.lang.instrument.ClassFileTransformer  
    ....
}
ClassFileTransformer提供了类的转换功能, 可以对字节码进行修改。 而Instrumentation除了可以管理ClassFileTransformer之外, 还有一些其他功能。比如:
[Java] 纯文本查看 复制代码
getAllLoadedClasses()
该方法可以获取当前虚拟机加载的所有Class对象。记住是所有。那在这里问题2就很好解决了。
我只需要遍历这个结果集和给定的名字是否匹配即可。再通过getClassLoader()可以获取这个类到底被谁加载了。
同时, getProtectionDomain().getCodeSource().getLocation().getFile() 还可以获取到当前类
的具体路径, 对于排查问题会更有帮助。 
线程使用情况
[Java] 纯文本查看 复制代码
java.lang.management.ThreadMXBean

以下方法比较有用:
getThreadCount()                //线程数
getDaemonThreadCount()          //daemon线程数
getPeakThreadCount()            //峰值
getTotalStartedThreadCount()    //启动过的线程数
   

操作系统
[Java] 纯文本查看 复制代码
java.lang.management.MemoryMXBean
java.lang.management.MemoryManagerMXBean

垃圾回收
[Java] 纯文本查看 复制代码
java.lang.management.GarbageCollectorMXBean

以下方法比较有用:
getName()                       // 收集器英文名称
getCollectionCount()            // 该收集器收集总次数
getCollectionTime()             // 该收集器收集总时间(ms)

注意:会有多个GarbageCollectorMXBean。

编译器
[Java] 纯文本查看 复制代码
java.lang.management.CompilationMXBean

以下方法比较有用:
getName()                       //返回JIT编译器名称
getTotalCompilationTime()       //返回在编译上花费的累积耗费时间的近似值(以毫秒为单位)

注意:需要调用isCompilationTimeMonitoringSupported方法来确定是否支持编译期的监控。

类加载
[Java] 纯文本查看 复制代码
java.lang.management.ClassLoadingMXBean

以下方法比较有用:
getLoadedClassCount()           //返回当前加载到 Java 虚拟机中的类的数量。
getTotalLoadedClassCount()      //返回自 Java 虚拟机开始执行到目前已经加载的类的总数。
getUnloadedClassCount()         //返回自 Java 虚拟机开始执行到目前已经卸载的类的总数。
isVerbose()                     //测试是否已为类加载系统启用了 verbose 输出。

运行时数据
[Java] 纯文本查看 复制代码
java.lang.management.RuntimeMXBean

以下方法比较有用:
getName()                       //返回表示正在运行的 Java 虚拟机的名称
getStartTime()                  //返回 Java 虚拟机的启动时间
getManagementSpecVersion()      //返回正在运行的 Java 虚拟机实现的管理接口的规范版本。
getSpecName()                   //返回 Java 虚拟机规范名称。
getSpecVendor()                 //返回 Java 虚拟机规范供应商。
getSpecVersion()                //返回 Java 虚拟机规范版本。
getVmName()                     //返回 Java 虚拟机实现名称。本机为Java HotSpot(TM) 64-Bit Server VM
getVmVendor()                   //返回 Java 虚拟机实现供应商
getVmVersion()                  //返回 Java 虚拟机实现版本
getInputArguments()             //返回传入的JVM启动参数
getClassPath()                  //返回类路径
getBootClassPath()              //返回bootstrap的path 

进阶class文件格式
对于理解JVM和深入理解Java语言, 学习并了解class文件的格式都是必须要掌握的功课。 原因很简单, JVM不会理解我们写的Java源文件, 我们必须把Java源文件编译成class文件, 才能被JVM识别, 对于JVM而言, class文件相当于一个接口, 理解了这个接口, 能帮助我们更好的理解JVM的行为;另一方面, class文件以另一种方式重新描述了我们在源文件中要表达的意思, 理解class文件如何重新描述我们编写的源文件, 对于深入理解Java语言和语法都是很有帮助的。
[Java] 纯文本查看 复制代码
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

会话保持
会话保持有2个重要的参数。
  • 会话保持时间, 常量。一般定义为10分钟就够了。
  • 触摸时间, 每次发起请求会更新触摸时间
可以后台起daemon线程, 周期性检查触摸时间和当前时间的差值是否超过了会话保持时间。如果超过需要关闭连接。
通信协议
既然确定了网络通信使用nio, 那我们务必要制定一套简单的通信协议, 能简明的告知服务端和客户端请求响应信息。这里我们拿dubbo来举例:
包装
写到这里, 核心的内容应差不多了。剩下的就是一些边角料。但是可以使你的project更加优雅和健全。
命令行解析
解析来自于命令行的参数并不是一件特别容易的事情。但是好在有优秀的工具:
[Java] 纯文本查看 复制代码
jcommander ([url=http://jcommander.org/]http://jcommander.org/[/url])
jopts ([url=https://github.com/jopt-simple/jopt-simple]https://github.com/jopt-simple/jopt-simple[/url])
当然这里同样要考虑自己项目的复杂度, 我们的目标是尽可能做一个精简的监控程序。


2 个回复

倒序浏览
ヾ(◍°∇°◍)ノ゙
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马