动态追踪技术底层分析

什么是动态追踪

  • 不用关闭java程序重启,无侵入式的实现,即可统计java程序的运行处理信息
  • 通过java agent技术实现

Java Agent 技术

  • JVM级别的aop
  • 事前、事后、事中
  • 比如要打印方法的入参和出参,此时是需要对java代码进行修改的,但是java程序已经运行了,数据就在运行时数据区中,而class文件就在方法区中,如果要改变某一个方法,就需要替换class文件,修改相应的字节码
  • 一个JVM只能调用一个arthas
main方法
  • premain方法
  • agentmain方法,arthas就是使用的这种

premain实例

agent的实际项目
package com.example.javaagent.app;

//VM参数中加入:-javaagent:F:\work_vip\javaagent-demo\agent\target\agent-1.0-SNAPSHOT.jar
public class MainRun {
    public static void main(String[] args) {
        hello("world");
    }

    private static void hello(String name) {
        System.out.println("hello " + name );
        try {
            Thread.sleep(Integer.MAX_VALUE);//线程休眠
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
agent构建步骤
  • 1.编写agent
  • AgentApp类中包含premain方法和agentmain方法
package com.example.javaagent;

import java.lang.instrument.Instrumentation;
/**
 * instrument 一共有两个 main 方法,一个是 premain,另一个是 agentmain
 *  但在一个 JVM 中,只会调用一个
 */

public class AgentApp {
    //在main 执行之前的修改
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("==============enter premain==============");
        //System.out.println(agentOps);
        inst.addTransformer(new Agent());
    }
    //控制类运行时的行为
    public static void agentmain(String agentOps, Instrumentation inst) {
        System.out.println("==============enter agentmain==============");
    }
}
  • 2.编写transformer
package com.example.javaagent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Agent implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        String loadName = className.replaceAll("/", ".");
       // System.out.println(className);
        if (className.endsWith("MainRun")) {
            try {
                //javassist 完成字节码增强(打印方法的执行时间<纳秒>)
                CtClass ctClass = ClassPool.getDefault().get(loadName);
                CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
                ctMethod.addLocalVariable("_begin", CtClass.longType);
                ctMethod.insertBefore("_begin = System.nanoTime();");
                ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);");

                return ctClass.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return classfileBuffer;
    }
}
  • 去实现一个class文件变异的接口
  • 对MainRun这个类进行变异,同时对hello方法进行变异,增加了一个局部变量-----当前系统时间,并在方法执行前插入,同时在方法结束后打印这个参数和当前时间的差值,也就是方法的执行时间
  • 3.打包
  • 在\resources\META-INF目录下,新建一个MANIFEST.MF文件
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
premain-class: com.example.javaagent.AgentApp
agentmain-class: AgentApp
  • 在maven配置中增加,避免在idea自带maven打包时mf文件被替换
<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
  • 然后在agent项目里执行mvn install

Agentmain实例

  • 这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:
  • 获取机器上运行的所有 JVM 进程 ID;
  • 选择要诊断的 jvm,选择进程id
  • 将 jvm 使用 attach 函数链接上;
  • 使用 loadAgent 函数加载 agent,动态修改字节码;
  • 卸载 jvm。
  • 这种模式不同于premain方法,premain方法所有的干预都会显示到被监控的程序里面,而agentmain则不会,它不会影响监控程序的输出,而是会将坚决信息回传回agentmain的监控程序,并显示在监控程序里面,arthas就是如此,例如使用arthas的watch命令,最终的监控结果会在arthas自己的命令行界面显示
Java Attach API
实现方法1
package ex10.attach;


import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Properties;

//Java Attach API
public class AttachDemo {
    public static void main(String[] args) throws Exception {
        //VM进程号,通过 jps命令获取
        //attach向目标 JVM ”附着”(Attach)代理工具程序
        VirtualMachine vm = VirtualMachine.attach("8900");
        // get system properties in target VM
        Properties props = vm.getSystemProperties();
        String version = props.getProperty("java.version");
        System.out.println(version);
        // VirtualMachineDescriptor是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
        List<VirtualMachineDescriptor> vmDescriptors = vm.list();
        //从JVM上面解除代理
        vm.detach();
    }
}
  • 注意,使用上述代码,需要引入E:\Java\JDK\lib\tools.jar这个jar包
实现方法2
package ex10.attach;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Properties;
import java.util.Set;

/**
 * @author King老师
 * Attach使用入门
 */

public class JvmAttach {

    public static void main(String[] args)
            throws Exception {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //只找对应启动类是JVMObject结尾的
            if (vmd.displayName().endsWith("JVMObject")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                Properties props = virtualMachine.getSystemProperties();
                //打印attach上的VM所有的系统属性
                System.out.println(getChildProcessConfig(props));
                //打印attach上的VM的JDK版本信息
                String version = props.getProperty("java.version");
                System.out.println("----version:"+version);
                virtualMachine.detach();
            }
        }
    }
    //获取所有属性
    private static Properties getChildProcessConfig( Properties props) {
        Properties properties = System.getProperties();
        Set<String> stringPropertyNames = properties.stringPropertyNames();
        Properties prop = new Properties();
        for (String string : stringPropertyNames) {
            prop.setProperty(string, properties.getProperty(string));
        }
        return prop;
    }

}
Instrument

实战案例

package com.example.javaagent.app;


import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;
import java.util.Properties;

public class JvmAttach {

    public static void main(String[] args)
            throws Exception {
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().endsWith("MainRun")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                Properties props = virtualMachine.getSystemProperties();
                String version = props.getProperty("java.version");

                	 virtualMachine.loadAgent("arthas-boot.jar ","...");
                
                System.out.println("version:"+version);
                virtualMachine.detach();
            }
        }
    }
}
  • 获取对应方法的jvm,并加载对应的代理jar包,然后去修改字节码
  • 这里是利用arthas实现的,例如arthas的watch方法,自己会把监视的结果回传回来,这是底层实现的,代码隐藏了,但是本质就是通过网络通讯回传回来的

总结

  • 上述只模拟了一个premain的方法,agentmain的模式没有模拟,只是以springboot项目为基础,拿arthas演示了一下watch方法,上面的实战实例和java attach api内容可以不自己看,如果想仔细看,去看第二期的内容,而模拟的springboot项目的部分内容如下
package cn.enjoyedu.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 类说明:
 */
@RestController
@RequestMapping("/btrace")
public class DemoController {

    @RequestMapping("/test")
    public String test(@RequestParam("name") String name){
        return "hello,"+name;
    }

    @RequestMapping("/exception")
    public String exception(){
        try {
            System.out.println("start.......");
            System.out.println(1/0);
            System.out.println("end.........");
        } catch (Exception e) {

        }

        return "success";
    }

}

借助Btrace手写动态追踪框架

  • 出现了非常久了,但是对技术要求很高,而arthas使用门槛很低
  • 在arthas诞生之前,都是通过Btrace实现追踪
  • btrace的github地址:
  • https://github.com/btraceio/btrace, BTrace基于ASM、Java Attach API、Instrument开发
  • ASM是字节码增强工具,但是接口比较难懂,像CGLIB就是基于ASM实现的,ASM可以在JVM运行过程中动态创建一个class文件

实战演练

  • 下载Btrace,并配置环境变量(类似jdk)
  • 命令行输入btrace,显示内容则成功
  • 1.仍然以springboot哪个hello,test项目为例
  • DemoController
package cn.enjoyedu.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 类说明:
 */
@RestController
@RequestMapping("/btrace")
public class DemoController {

    @RequestMapping("/test")
    public String test(@RequestParam("name") String name){
        return "hello,"+name;
    }

    @RequestMapping("/exception")
    public String exception(){
        try {
            System.out.println("start.......");
            System.out.println(1/0);
            System.out.println("end.........");
        } catch (Exception e) {

        }

        return "success";
    }

}
  • 2.需要引入btrace的三个类
  • TestBrace
package cn.enjoyedu.btrace;

import com.sun.btrace.AnyType;
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.str;
import static com.sun.btrace.BTraceUtils.strcat;

/**
 * 类说明:检查方法的输入和输出
 */
@BTrace
public class TestBrace {

    // 跟踪的方法
    @OnMethod(
            clazz = "cn.enjoyedu.demo.controller.DemoController",
            method = "test",
            location = @Location(Kind.ENTRY)
    )
    public static void checkEntry(
           @ProbeClassName String pcn,
           @ProbeMethodName String pmn,
           AnyType[] args
    ){
        //打印了方法的参数
        //BTraceUtils这个类就具有远程打印功能,就是在BTrace本身打印,而不是在springboot那个项目里面打印
        BTraceUtils.println("Class: "+pcn);
        BTraceUtils.println("Method: "+pmn);
        BTraceUtils.printArray(args);
        BTraceUtils.println("===========================");
        BTraceUtils.println();
    }

    @OnMethod(
            clazz = "cn.enjoyedu.demo.Service.NormalService",
            method = "getBoolean",/*这里需要修改*/
            location = @Location(Kind.RETURN)/*这里需要修改*/
    )
    public static void checkReturn(/*这里需要修改*/
           @ProbeClassName String pcn,
           @ProbeMethodName String pmn,
           @Return boolean result /*这里需要修改*/
    ){
        BTraceUtils.println("Class: "+pcn);
        BTraceUtils.println("Method: "+pmn);
        BTraceUtils.println(strcat("result:",str(result)));/*这里需要修改*/
        BTraceUtils.println("===========================");
        BTraceUtils.println();
    }
}
  • MoreBtrace
package cn.enjoyedu.btrace;

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.*;

/**
 * 类说明:
 */
@BTrace
public class MoreBtrace {

    // 在testBtrace的实例中加入很多的功能
    // 注解写的更复杂了
    // Location是动作,方法调用
    // clazz、method是正则规则
    // where是after之后
    @OnMethod(
            clazz = "cn.enjoyedu.demo.controller.DemoController",
            method = "test",
            location = @Location(value = Kind.CALL,
                    clazz = "/.*/", method = "/.*/",
                    where = Where.AFTER))
    public static void onInvoke(@Self Object self, @TargetInstance Object instance,
                              @TargetMethodOrField String method,
                              @Duration long duration){
        BTraceUtils.println(strcat("self: ", str(self)));
        BTraceUtils.println(strcat("instance: ", str(instance)));
        BTraceUtils.println(strcat("method: ", str(method)));
        BTraceUtils.println(strcat("duration(ns): ", str(duration )));
        println("===========================");
        BTraceUtils.println();
    }

    @OnMethod(
            clazz = "cn.enjoyedu.demo.controller.DemoController",
            location = @Location(value = Kind.LINE, line = 26))
    public static void onBind() {
        println("execute line 20");
        println("---------------------------");
        BTraceUtils.println();
    }

    @OnMethod(
            clazz = "/cn\\.enjoyedu\\.demo\\.controller\\..*/",
            method = "/.*/",
            location = @Location(Kind.RETURN))
    public static void slowQuery(@ProbeClassName String pcn,
                                 @ProbeMethodName String probeMethod,
                                 @Duration long duration){
        if(duration > 1000000 * 100){
            println(strcat("class:", pcn));
            println(strcat("method:", probeMethod));
            println(strcat("duration:", str(duration / 1000000)));
            println("*************************");
            BTraceUtils.println();
        }

    }
}
  • TraceException
package cn.enjoyedu.btrace;

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;

/**
 * 类说明:
 */
@BTrace
public class TraceException {
    @TLS
    static Throwable currentException;

    // introduce probe into every constructor of java.lang.Throwable
    // class and store "this" in the thread local variable.
    @OnMethod(
            clazz="java.lang.Throwable",
            method="<init>"
    )
    public static void onthrow(@Self Throwable self) {  // @Self其实就是拦截了this
        //new Throwable()
        currentException = self;
    }

    @OnMethod(
            clazz="java.lang.Throwable",
            method="<init>"
    )
    public static void onthrow1(@Self Throwable self, String s) {
        //new Throwable(String msg)
        currentException = self;
    }

    @OnMethod(
            clazz="java.lang.Throwable",
            method="<init>"
    )
    public static void onthrow1(@Self Throwable self, String s, Throwable cause) {
        //new Throwable(String msg, Throwable cause)
        currentException = self;
    }

    @OnMethod(
            clazz="java.lang.Throwable",
            method="<init>"
    )
    public static void onthrow2(@Self Throwable self, Throwable cause) {
        //new Throwable(Throwable cause)
        currentException = self;
    }

    // when any constructor of java.lang.Throwable returns
    // print the currentException's stack trace.
    @OnMethod(
            clazz = "cn.enjoyedu.demo.controller.DemoController",
            method = "exception",
            location=@Location(Kind.ERROR)
    )
    public static void onthrowreturn() {
        if (currentException != null) {
            // 打印异常堆栈
            BTraceUtils.Threads.jstack(currentException);
            BTraceUtils.println("=====================");
            // 打印完之后就置空
            currentException = null;
        }
    }
}

location=@Location(Kind.ERROR)可以把吞掉的异常打印出来

  • 3.具体的监控步骤
  • 进入TestBrace所在的命令行界面,然后输入:
    btrace 监控的程序的进程号 TestBrace.java
  • 此时在浏览器中访问springboot项目中的test方法,输入king,然后就能在Btrace的命令行界面看到相应的输出

注解的使用

OnMethod
  • @OnMethod 可以指定 clazz 、method、location。
  • 由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截
clazz
  • clazz 支持,精准定位、正则表达式定位、按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如@OnMethod(clazz="+com.kite.base", method=“doSome”)>、按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService",method="@javax.jws.WebMethod")>method 支持精准定位、正则表达式定位、按注解定位
location
  • 1.Kind.Entry 与 Kind.Return分别表示函数的开始和返回,不写 location 的情况下,默认为 Kind.Entry,仅获取参数值,可以用 Kind.Entry ,要获取返回值或执行时间就要用 Kind.Return
  • 2.Kind.Error, Kind.Throw 和 Kind.Catch,表示异常被 throw 、异常被捕获还有异常发生但是没有被捕获的情况,在拦截函数的参数定义里注入一个Throwable 的参数,代表异常
  • 3.Kind.Call 表示被监控的方法调用了哪些其他方法,Kind.Line 监测类是否执行到了设置的行数

BTrace 注解

  • BTrace 注解可以分为:
  • 类注解 @BTrace
  • 方法注解如@OnMethod
  • 参数注解如:@ProbeClassName
参数注解
  • @ProbeClassName
    用于标记处理方法的参数,仅用户@OnMethod, 该参数的值就是被跟踪的类名称
  • @ProbeMethodName
    用于表姐处理方法的参数,仅用户 @OnMethod,该参数值是被跟踪方法名称
  • @Self
    当前截取方法的封闭实例参数
  • @Return
    当前截取方法的的返回值, 只对 location=@Location(Kind.RETURN) 生效
  • @Duration
    当前截取方法的执行时间
  • @TargetInstance
    当前截取方法内部调用的实例
  • @TargetMethodOrField
    当前截取方法内部被调用的方法名
方法注解
  • @OnMethod
    用于指定跟踪方法到目标类,目标方法和目标位置
    格式
@Location 属性有:
  • value 默认值为 Kind.ENTRY 即参数的入口位置
  • where 限定探测位置 默认值为 Where.BEFORE 也可以设置为 Where.AFTER
  • clazz
  • method
  • field
  • type
  • line
@Kind 注解的值有
  • Kind.ENTRY-被 trace 方法参数
  • Kind.RETURN-被 trace 方法返回值
  • Kind.THROW -抛异常
  • Kind.ARRAY_SET, Kind.ARRAY_GET -数组索引
  • Kind.CATCH -捕获异常
  • Kind.FIELD_SET -属性值
  • Kind.LINE -行号
  • Kind.NEW -类名
  • Kind.ERROR -抛异常
@OnTimer
  • 用于指定跟踪操作定时执行。value 用于指定时间间隔
@OnError
  • 当 trace 代码抛异常或者错误时,该注解的方法会被执行.如果同一个 trace 脚本中其他方法抛异常,该注解方法也会被执行。

Btrace 的限制

  • BTrace 最终借 Instrument 实现 class 的替换。出于安全考虑,Instrument 在使用上存在诸多的限制,这就好比给一架正在飞行的飞机换
    发动机一样一样的,因此 BTrace 脚本的限制如下:
  • 不允许创建对象
  • 不允许创建数组
  • 不允许抛异常
  • 不允许 catch 异常
  • 不允许随意调用其他对象或者类的方法,只允许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输出工具)
  • 不允许改变类的属性
  • 不允许有成员变量和方法,只允许存在 static public void 方法
  • 不允许有内部类、嵌套类
  • 不允许有同步方法和同步块
  • 不允许有循环
  • 不允许随意继承其他类(当然,java.lang.Object 除外)
  • 不允许实现接口
  • 不允许使用 assert
  • 不允许使用 Class 对象
  • 如此多的限制,其实可以理解。BTrace 要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。

工具总结

  • 其实作为 Java 的动态追踪技术,站在比较底层的角度上来说,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。Arthas 都是针前面这些技术的一个封装而已。
  • Btrace 功能虽然强大,但都是比较难入门,这就是为什么 Btrace 出来这么多年,还是只在小范围内被使用。相对来说,Arthas 显的友好而且安全的多。
  • 但无论工具如何强大,一些基础知识是需要牢固掌握的,否则,工具中出现的那些术语,也会让人一头雾水。工具常变,但基础更加重要。如果你想要一个适应性更强的技术栈,还是要多花点时间在原始的排查方法上。