asmmonitor学习

本文主要是分析tomcat应用监控插件——asmMonitor的实现原理和配置。asmMonitor部署在被监控的应用服务器(tomcat)下,监控应用并采集监控项数据,向服务器端上报。
详细功能如下:

  • 使用javaagent来指定字节码注入的jar包
  • 使用asm注入方式无侵入地监控应用系统的方法调用
  • 使用jmx监控jvm内存和线程
  • 使用druid采集sql执行情况
  • 使用thrift协议上报监控数据到服务器端
  • 使用servlet容器监听器,监听tomcat启动
  • 使用侵入式的springmvc拦截器,拦截http请求

配置

配置javaagent

配置JVM启动参数javaagent,来指定代理类(或jar包),在类加载时做字节码修改(注入)。
在Unix/Linux,修改<tomcat_home>/bin/catalina.sh
在Windows,修改<tomcat_home>/bin/catalina.bat
添加如下配置:

export JAVA_OPTS="$JAVA_OPTS -javaagent:/full/path/to/asmMonitor-1.0.0.jar"

配置ServletContextListener监听器

ServletContextListener监听器是在tomcat启动时,监听初始化servlet容器的事件。
每个部署在tomcat的应用,都有个web.xml文件,在需要监控的在web.xml文件添加:

    <listener>
        <listener-class>com.ecar.asmmonitor.listener.SocketListener</listener-class>
    </listener>
配置HandlerInterceptorAdapter拦截器

HandlerInterceptorAdapter拦截器,是用于拦截springmvc中所有http请求。
我们使用com.ecar.asmmonitor.interceptor.MonitorInterceptor拦截器,监控应用的http接口请求和响应时间,上报服务器端。
拦截器配置一般在spring相关的bean配置文件,配置如下:

<!--配置拦截器, 多个拦截器,顺序执行 -->  
<mvc:interceptors>    
    <mvc:interceptor>    
        <!-- 匹配的是url路径, 如果不配置或/**,将拦截所有的Controller -->  
        <mvc:mapping path="/**" />  
        <bean class="com.ecar.asmmonitor.interceptor.MonitorInterceptor"></bean>    
    </mvc:interceptor>  
    <!-- 当设置多个拦截器时,先按顺序调用preHandle方法,然后逆序调用每个拦截器的postHandle和afterCompletion方法 -->  
</mvc:interceptors> 

注入原理

instrument代理类

代理:是独立于应用程序之外的代理(agent)程序,agent程序通过增强字节码动态修改或者新增类,利用这样特性可以设计出更通用的监控、框架、中间件程序。
JVMTI:这里使用的注入方式是instrument代理类,实际上就是使用jvm暴露的一些接口(JVMTI全称JVM Tool Interface),这些接口是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
instrument:就是一个属于JVMTIAgent类型的JVMTI动态库,这个代理类是在某个类的字节码文件读取之后、类定义之前修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容。

instrument agent有两种加载字节码的方式:

  • Agent_OnLoad :启动时加载,在启动jvm时加载
  • Agent_OnAttach :运行时加载,在运行jvm时加载

无论是启动时还是运行时加载 instrument agent ,主要都是监听 ClassFileLoadHook 事件, 并最终调用 premain 方法 / agentmain 方法ClassFileLoadHook 这个事件 发生在读取字节码文件之后,
这样就可以对原来的字节码做修改。

启动时加载
  1. 编写代理类,实现premain 方法
public class AsmMain {
    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer(new MonitorTransformer());
    }
}
  1. 打包成jar包,在jar包的META-INF/MANIFEST.MF文件指定代理类
Manifest-Version: 1.0
Premain-Class: com.ecar.asmmonitor.AsmMain
Can-Redefine-Classes: true
  1. 配置jvm启动参数javaagent参数,指定代理jar包
-javaagent:/full/path/to/asmMonitor-1.0.0.jar
运行时加载
  1. 编写代理类,实现agentmain方法
public class AsmMain {
    public static void agentmain(String args, Instrumentation inst) {
        inst.addTransformer(new MonitorTransformer());
    }
}
  1. 打包成jar包,在jar包的META-INF/MANIFEST.MF文件指定代理类
Manifest-Version: 1.0
Agent-Class: com.ecar.asmmonitor.AsmMain
Can-Redefine-Classes: true
  1. 借助Attach Tools API的attach方式,通过传入进程pid加载代理类
import com.sun.tools.attach.VirtualMachine;
public class RunAttach
{
    public static void main(String[] args) throws Exception
    {
        // args[0]传入的是某个jvm进程的pid
        String targetPid = args[0];
 
        VirtualMachine vm = VirtualMachine.attach(targetPid);
 
        vm.loadAgent("F:/workspaces/j2se练习代码/jvm_high_api/agentmain.jar",
                "toagent");
    }
}
字节码转换器类(ClassFileTransformer)

真实处理class字节码的类,实现ClassFileTransformer接口的transform方法,修改字节码,下面是实例:

        //字节码转化器类
    public class PrintTimeTransformer implements ClassFileTransformer {

        //实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构
        //loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到)
        //className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer"
        //classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
        //protectionDomain:要定义或重定义的类的保护域
        //classfileBuffer:类文件格式的输入字节缓冲区(不得修改)
        //一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
        @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
                throws IllegalClassFormatException {
            //简化测试demo,直接写待修改的类(com/blueware/agent/TestTime)
            if (className != null && className.equals("com/blueware/agent/TestTime")) {
                //读取类的字节码流
                ClassReader reader = new ClassReader(classfileBuffer);
                //创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
                //接受一个ClassVisitor子类进行字节码修改
                reader.accept(new TimeClassVisitor(writer, className), 8);
                //返回修改后的字节码流
                return writer.toByteArray();
            }
            return null;
        }
    }
ASM字节码操控框架

前面的javaagent只是用来监控字节码读取的事件,找准注入字节码的时机,真正的字节码注入操作,还是需要专业的包。
ASM:是一个 Java 字节码操控框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

包括如下类:

  • 类读取类,ClassReader
  • 类写入类,ClassWriter
  • 类访问类,ClassVisitor
  • ClassVisitor实现类,自定义字节码注入逻辑,覆盖里面的visitMethod方法扫描到每个方法时都会进入,例如下面的TimeClassVisitor类
  • 方法访问者类,MethodVisitor
  • MethodVisitor实现类,例如下面的AdviceAdapter类,自定义在方法的前后注入代码。
    TimeClassVisitor
    //定义扫描待修改class的visitor,visitor就是访问者模式
    public class TimeClassVisitor extends ClassVisitor {
        private String className;

        public TimeClassVisitor(ClassVisitor cv, String className) {
            super(Opcodes.ASM5, cv);
            this.className = className;
        }

        //扫描到每个方法都会进入,参数详情下一篇博文详细分析
        @Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            final String key = className + name + desc;
            //过来待修改类的构造函数
            if (!name.equals("<init>") && mv != null) {
                mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                    //方法进入时获取开始时间
                    @Override public void onMethodEnter() {
                        //相当于com.blueware.agent.TimeUtil.setStartTime("key");
                        this.visitLdcInsn(key);
                        this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setStartTime", "(Ljava/lang/String;)V", false);
                    }

                    //方法退出时获取结束时间并计算执行时间
                    @Override public void onMethodExit(int opcode) {
                        //相当于com.blueware.agent.TimeUtil.setEndTime("key");
                        this.visitLdcInsn(key);
                        this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setEndTime", "(Ljava/lang/String;)V", false);
                        //向栈中压入类名称
                        this.visitLdcInsn(className);
                        //向栈中压入方法名
                        this.visitLdcInsn(name);
                        //向栈中压入方法描述
                        this.visitLdcInsn(desc);
                        //相当于com.blueware.agent.TimeUtil.getExclusiveTime("com/blueware/agent/TestTime","testTime");
                        this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "getExclusiveTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", false);
                    }
                };
            }
            return mv;
        }
    }
  public class TimeUtil {
        private static Map<String, Long> startTimes = new HashMap<String, Long>();
        private static Map<String, Long> endTimes   = new HashMap<String, Long>();

        private TimeUtil() {
        }

        public static long getStartTime(String key) {
            return startTimes.get(key);
        }

        public static void setStartTime(String key) {
            startTimes.put(key, System.currentTimeMillis());
        }

        public static long getEndTime(String key) {
            return endTimes.get(key);
        }

        public static void setEndTime(String key) {
            endTimes.put(key, System.currentTimeMillis());
        }

        public static long getExclusiveTime(String className, String methodName, String methodDesc) {
            String key = className + methodName + methodDesc;
            long exclusive = getEndTime(key) - getStartTime(key);
            System.out.println(className.replace("/", ".") + "." + methodName + " exclusive:" + exclusive);
            return exclusive;
        }
    }

监控细节

上报细节

tomcat启动,初始化servlet容器时,会触发SocketListener监听器,开启下列监听:

1.采集Tomcat的启动时间数据,每次启动tomcat时上报一次
2.采集心跳数据,每30秒上报一次心跳
3.采集JVM和线程状态的数据(使用JMX),每60秒上报一次数据
4.上报应用接口的数据
5.上报警告的数据
6.采集SQL的数据,每60秒上报一次数据
7.启动上报线程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,928评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,748评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,282评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,065评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,101评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,855评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,521评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,414评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,931评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,053评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,191评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,873评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,529评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,074评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,188评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,491评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,173评论 2 357