如何开发一款java应用运行时的监控程序?
前言
每个程序员都或多或少遇到过相当多的疑难杂症问题排查的时刻。我自己也是工作中遇到许多稀奇古怪的问题。最开始我们排查问题使用的是jprofiler。特别是使用jprofiler来排查调用链路的耗时问题。如下图所示:
但是jprofiler只能用于排查一些本地的问题。对于一些生产环境的由于网络隔离在加上权限受限, jprofiler就不是那么好使了。这时候萌生了自己做个小工具的想法。同时参考了一些工具和apm的实现, 简单实现了所需的功能。
我们现在思考下, 假设要开发一个java程序的监控工具,比如包含以下功能, 都需要怎么实现?
1. 实时或周期性的获取java进程运行数据, 包括但不限于内存,线程,操作系统,GC等。 2. 如何在运行时知道一个class是被哪个classloader加载的? 3. 如何动态的知道一个方法的执行时间?(对于基础的排查性能问题很有用) 4. 如何动态知道一个方法被调用时候的完整调用栈? 5. 如何动态的知道一次调用下一个方法的入参,返回值? ...
基础部分
在这个【基础部分】里, 我们可以很轻松的解决上边的问题1。这要感谢JDK5后提供的两大神器:Instrument和management。前置提供了应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序, 后者可以实时获取应用程序的实时运行数据。
Agent
我们遇到的第一个问题, 是如何将自己的监控程序和目标进程关联起来。
比如我1个monitor.jar,里边包含了我们的监控程序, 如何和生产环境正在运行的tomcat进程进行关联?
答案是JDK提供的agent机制。简单来说只需要做以下事情:
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插件来设置:
<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>
更多内容可参考下文
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
Instrumentation
核心内容都在java.lang.instrumentation包下, 主要的类有:
public interface Instrumentation { .... java.lang.instrument.Instrumentation java.lang.instrument.ClassFileTransformer .... }
ClassFileTransformer提供了类的转换功能, 可以对字节码进行修改。 而Instrumentation除了可以管理ClassFileTransformer之外, 还有一些其他功能。比如:
getAllLoadedClasses() 该方法可以获取当前虚拟机加载的所有Class对象。记住是所有。那在这里问题2就很好解决了。 我只需要遍历这个结果集和给定的名字是否匹配即可。再通过getClassLoader()可以获取这个类到底被谁加载了。 同时, getProtectionDomain().getCodeSource().getLocation().getFile() 还可以获取到当前类 的具体路径, 对于排查问题会更有帮助。
线程使用情况
java.lang.management.ThreadMXBean 以下方法比较有用: getThreadCount() //线程数 getDaemonThreadCount() //daemon线程数 getPeakThreadCount() //峰值 getTotalStartedThreadCount() //启动过的线程数
操作系统
java.lang.management.OperatingSystemMXBean 以下方法比较有用: getName() // 操作系统名称. 本机为 Mac OS X getArch() // 系统架构. 本机为 x86_64 getAvailableProcessors() // 处理器个数. 本机为 8 getSystemLoadAverage() // 过去1分钟的load getVersion() // 操作系统版本
内存
java.lang.management.MemoryMXBean java.lang.management.MemoryManagerMXBean
垃圾回收
java.lang.management.GarbageCollectorMXBean 以下方法比较有用: getName() // 收集器英文名称 getCollectionCount() // 该收集器收集总次数 getCollectionTime() // 该收集器收集总时间(ms) 注意:会有多个GarbageCollectorMXBean。
编译器
java.lang.management.CompilationMXBean 以下方法比较有用: getName() //返回JIT编译器名称 getTotalCompilationTime() //返回在编译上花费的累积耗费时间的近似值(以毫秒为单位) 注意:需要调用isCompilationTimeMonitoringSupported方法来确定是否支持编译期的监控。
类加载
java.lang.management.ClassLoadingMXBean 以下方法比较有用: getLoadedClassCount() //返回当前加载到 Java 虚拟机中的类的数量。 getTotalLoadedClassCount() //返回自 Java 虚拟机开始执行到目前已经加载的类的总数。 getUnloadedClassCount() //返回自 Java 虚拟机开始执行到目前已经卸载的类的总数。 isVerbose() //测试是否已为类加载系统启用了 verbose 输出。
运行时数据
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
系统级别采集
之前整理了很多数据获取的方式,但是都是基于java进程本身的。这里介绍常用的基于linux系统本身的采集方式:
cpu: /proc/stat memory: /proc/meminfo load: /proc/loadavg 网卡: /proc/net/dev TCP&UDP: /proc/net/snmp io: /proc/diskstats
需要注意以下细节:
1. proc文件系统是提供系统运行状态的利器。很多开源的linux监控系统都是基于proc来进行分析和开发。 2. mac os并不支持proc。 3. 不同的linux发行版在proc的输出展示上有微小的不同。比如redhat和fedora在展示TCP数据时列名稍稍有差异。
进阶
class文件格式
对于理解JVM和深入理解Java语言, 学习并了解class文件的格式都是必须要掌握的功课。 原因很简单, JVM不会理解我们写的Java源文件, 我们必须把Java源文件编译成class文件, 才能被JVM识别, 对于JVM而言, class文件相当于一个接口, 理解了这个接口, 能帮助我们更好的理解JVM的行为;另一方面, class文件以另一种方式重新描述了我们在源文件中要表达的意思, 理解class文件如何重新描述我们编写的源文件, 对于深入理解Java语言和语法都是很有帮助的。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
网络通信
思考2个问题:
1. 本机应用部署了monitor.jar, 我们如果想通过命令行交互, 怎么做? 2. 网络打通的远程服务器部署了monitor.jar, 命令行交互又该怎么做?
很容易的, 我们可以想到socket。摆在我们面前的有如下方案:
netty mina nio
这里我建议直接上nio。原因如下:
1. 我们要给monitor.jar减负。如果依赖了外部库, 会加重监控工具的臃肿程度; 2. nio已经足够用了。这个交互并不需要多么高的性能。杀鸡焉用牛刀; 3. mina的社区太不活跃了。虽然是个成熟的软件, 但是万一哪里踩坑都不好处理;
会话保持
会话保持有2个重要的参数。
- 会话保持时间, 常量。一般定义为10分钟就够了。
- 触摸时间, 每次发起请求会更新触摸时间
可以后台起daemon线程, 周期性检查触摸时间和当前时间的差值是否超过了会话保持时间。如果超过需要关闭连接。
通信协议
既然确定了网络通信使用nio, 那我们务必要制定一套简单的通信协议, 能简明的告知服务端和客户端请求响应信息。这里我们拿dubbo来举例:
可以看到dubbo有明确的byte位来指明哪些byte存放什么内容。这样做的好处有2, 1是结构清晰, 2是可以构造出尽可能小的报文数据。在高并发的请求下是及其有用的(节省资源), 虽然我们这个project这里并不是很重要, 但是保持一个好习惯总是没错的。
表达式语言
通信的时候需要考虑是否有更灵活的表达方式。如果可以ognl是一个不错的选择。
核心
在很多场景下, 我们核心要处理的并不是直接获取数据, 而是通过改造目标类, 从而达到各种灵活的功能。 所以如何完成目标类的改造是核心问题。
选型
大部分情况下你可以有以下4种选择:
javassist: 对于字节码以及指令良好的封装 bytebuddy: 更优良的设计和封装, 并获得 Duke's Choice Awards 2015。也算是个明星项目了, 目前社区还很活跃, 国人开发的skywalking便是基于它 asm: 成熟度高, 但相对更底层一些。需要开发者对字节码以及指令有一定了解 自研: 省省力气吧-_-
这里我的选择是直接上asm。理由是完成这个项目必不可少的需要了解字节码和方法指令, 那就干脆深入的去了解。其实ASM本身也提供了很多工具。
比如org.objectweb.asm.util.ASMifier就是一个带有Main方法的类。只需传入指定类的全名, 则会输出该类用asm描述的文本形式。
如何完成目标类的字节码更新
这里还是需要用到instrument工具。见:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform); void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
- 首先通过addTransformer添加一个转换器。请务必canRetransform为true才可以完成运行时转换。
- 然后调用retransformClasses来执行该转换器。此时你的transformer可以做任何你想做的猥琐事情。
如何完成目标方法的改造
我们再回头看下问题3,4,5.
3. 如何动态的知道一个方法的执行时间?(对于基础的排查性能问题很有用) 4. 如何动态知道一个方法被调用时候的完整调用栈? 5. 如何动态的知道一次调用下一个方法的入参,返回值?
可以看到可以规划为1个问题,那就是如何在目标方法调用前和返回前拦截?
ASM早就考虑到这种需求了, 所以给我们提供了完美的支持:
org.objectweb.asm.commons.AdviceAdapter extends org.objectweb.asm.MethodVisitor protected void onMethodEnter() { } protected void onMethodExit(int opcode) { }
此时我们只需在对应方法的实现下完成自己的拦截代码即可。注意:onMethodExit方法是包含了正常返回以及异常返回的(抛出异常)。在这里问题3已经很好解决了。不考虑其他特殊情况下可以ThreadLocal存储一份时间来在onMethodEnter和onMethodExit之间比较即可。
问题4的解决方案也很简单。在前置方法使用 Thread.currentThread().getStackTrace()即可拿到完整的栈列表。只不过要注意考虑去掉一些无意义的调用行, 比如java.lang.Method.invokexxxx这种。
涉及到方法指令集操作的可以通过继承 org.objectweb.asm.commons.GeneratorAdapter 来完成。GeneratorAdapter提供了相当多的工具方法,比如我们常用的:
loadArgArray() //获取参数列表 box() //装箱 loadThis() //实例方法加载index=0的slot ...
问题5在这里也很好解决, 通过GeneratorAdapter.loadArgArray()即可获取到。这里涉及到一点操作数栈的知识,下边会说。
如何取消目标方法的改造
当我们执行完成我们的监控后或连接关闭时, 需要取消类和方法的改造。否则改造后的类一直存在, 直到应用重启。
如何取消类的改造也很简单, 参考ClassFileTransform的官方文档:
If the implementing method determines that no transformations are needed, it should return <code>null</code>.
所以我们只需要在transform列表的末尾添加一个返回为空的transform即可。
类加载隔离
这里的自定义类加载器是必须的。我们很难在一个单独的jar包中完成所有的事情, 部分功能很有可能交给第三方去做, 如下:
网络通信: Netty, Mina 字节码指令: asm 工具类: common-lang, guava
其中网络通信我们可以使用原生nio, 工具类可以自己来写, 但是asm的使用显得不可避免。这时会不可避免的和应用代码产生冲突。所以自定义ClassLoader很有必要。
jvm指令集
不管我们最终选择asm还是bytebuddy。对于jvm的指令集还是有必要深入了解一番的。相关的指令集可以大概分为以下几类:
栈操作
dup(包括dup2,dup2_x1等) swap pop
常量
xCONST(x代表了各种类型, 包括ACONST_NULL) BIPUSH SIPUSH LDC LDC2
逻辑运算
xADD xSUB xMUL xDIV xREM
转型
I2F,F2D,L2D ...
对象&字段
NEW PUTFIELD GETFIELD
方法
INVOKEVIRTUAL 声明过的方法 INVOKESPECIAL private&构造器 INVOKESTATIC INVOKEDYNAMIC 动态方法
数组
xALOAD xASTORE
跳转&返回
IFEQ,IFGE,IFNE... xRETURN,RETURN
如上列举出的指令相对会简单一些且会经常用到。这里有2个地方可以方便的查询这些指令:
1. 当然是官网的jvm规范 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html 2. 这里的解释相对官网会更通俗好懂一些 https://cs.au.dk/~mis/dOvs/jvmspec/ref-Java.html
操作数栈&本地变量表
先上一张图,这张图比较老了。
在JDK8之后, permanent区域已经被移除, 被新的MetaSpace所取代, 配合此文更佳:
http://ifeve.com/java-permgen-removed/
包装
写到这里, 核心的内容应差不多了。剩下的就是一些边角料。但是可以使你的project更加优雅和健全。
命令行解析
解析来自于命令行的参数并不是一件特别容易的事情。但是好在有优秀的工具:
jcommander (http://jcommander.org/) jopts (https://github.com/jopt-simple/jopt-simple)
当然这里同样要考虑自己项目的复杂度, 我们的目标是尽可能做一个精简的监控程序。
awk&shell
如果你打算把你的project分享到gayhub上, awk&shell 务必要熟悉. 将监控程序做到自动化(下载,安装)
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
区块链教程Fabric1.0源代码分析configtx#ChannelConfig-兄弟连
Fabric 1.0源代码笔记 之 configtx(配置交易) #ChannelConfig(通道配置) 1、ChannelConfig概述 ChannelConfig代码分布在common/config目录下。目录结构如下: channel_util.go,channel相关工具函数。 orderer_util.go,orderer(系统通道)相关工具函数。 application_util.go,应用通道相关工具函数。 consortiums_util.go,联盟相关工具函数。 api.go,核心接口定义,如Org、ApplicationOrg、Channel、Orderer、Application、Consortium、Consortiums、ValueProposer接口定义。 root.go,Root结构体及方法。 channel.go,ChannelGroup结构体及方法。 orderer.go,OrdererGroup结构体及方法。 application.go,ApplicationGroup结构体及方法。 2、工具函数 2.1、channel相关工具函数 //用ke...
- 下一篇
关于wordpress的一些坑
最近玩了一下以前玩的wordpress,但是wordpress有很多漏洞,我把自己遇到的问题和解决记一下, 本人所用wordpress版本为当前最新4.9.8版本 问题一 https域名下 wordpress资源加载不全(css,图片啥的引入会有问题),解决的办法: https://www.dadiaoge.com/wordpress-https.html (1)网站根目录\wp-includes\functions.php找到代码require( ABSPATH . WPINC . '/option.php' ), 添加如下代码; add_filter('script_loader_src', 'agnostic_script_loader_src', 20,2); function agnostic_script_loader_src($src, $handle) { return preg_replace('/^(http|https):/', '', $src); } add_filter('style_loader_src', 'agnostic_style_loader_s...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS8编译安装MySQL8.0.19
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案