BTrace 简要介绍

BTrace是Java的安全可靠的动态跟踪工具。 他的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。

说他是安全可靠的,是因为它对正在运行的程序是只读的。也就是说,他可以插入跟踪语句来检测和分析运行中的程序,不允许对其进行修改。因此他存在一些限制:

  • 不能创建对象
  • 不能创建数组
  • 不能抛出和捕获异常
  • 不能调用任何对象方法和静态方法
  • 不能给目标程序中的类静态属性和对象的属性进行赋值
  • 不能有外部、内部和嵌套类
  • 不能有同步块和同步方法
  • 不能有循环(for, while, do..while)
  • 不能继承任何的类
  • 不能实现接口
  • 不能包含assert断言语句

这些限制其实是可以使用unsafe模式绕过。通过声明 *@BTrace(unsafe = true) annotation 并且以unsafe 模式-u运行btrace

实际使用非安全模式跟踪时,发现一个问题,一个进程如果被安全模式btrace探测过一次, 后面再使用非安全模式进行探测时非安全模式不生效。

安装并使用

  • 下载Btrace并解压
  • 定义环境变量BTRACE_HOME为解压目录
  • bin目录加入到path

运行BTrace

btrace  
  • pid 为java进程号,可以使用jps来查询
  • btrace-script 为 btrace脚本

也可以先使用btracec对btrace脚本继续预先编译再进行执行。

visualvm-plugin

btrace还提供了一个vaisualvm上的一个插件,可以执行btrace脚本。尝试了下,可以attach到本机的jvm进程上,但是远程主机的JVM进程不行。晚上有的说通过端口转发绑定的方式可以,但是还是没有试出来。

安装

运行jvisualvm.exe, 选择工具->插件->可用插件 选择 BTrace Workbench进行在线安装。

BTrace 简要介绍_第1张图片
jvisualvm_btrace.jpg

在线不能安装的,也可以通过手动安装。先从java visualvm 插件中心找到相应版本的插件更新文件地址进行下载,从更新文件中获取所需要的插件包的nbm下载地址进行下载。最后在 插件->已下载tab页->添加插件

执行btrace脚本

选择需要监控的进程,右击 trace application

BTrace 简要介绍_第2张图片
jvisualvm_btrace2.png

在btrace的工作台中直接编写脚本并执行

BTrace 简要介绍_第3张图片
jvisualvm_btrace3.png

BTrace 脚本

先来看简单的BTrace脚本的使用,用于跟踪方式执行时间

@BTrace
public class TimeLogger {

  @TLS private static long startTime = 0;

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get")
  public static void startExecute(){
    startTime = timeNanos();
  }

  @OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get",
                                            location=@Location(Kind.RETURN)
  )
  public static void endExecute(@Duration long duration){
    long time = timeNanos() - startTime;
    println(strcat("execute time(nanos): ", str(time)));
  }

}

  • @BTrace 声明了这个类是BTrace脚本
  • @OnMethod 声明了关注点,必须声明在公有的静态方法上public static void
  • 静态方法为 在关注点上执行的跟踪动作

跟踪方式执行时间程序的逻辑大致是: 在org.springframework.data.redis.core.DefaultValueOperations 对象执行get方法时,先记录一下当前时间,在get方法return时获取当前时间,从而获得方法执行所消耗的时间。值得注意的是,@TLS声明的变量是 ThreadLocal的, 每个线程都会有一份这个自己的startTime 变量。

执行以上的TimeLogger来看一下:

$ btrace 13778 TimeLogger.java 
execute time(nanos): 214692000
execute time(nanos): 1215000
execute time(nanos): 3210000

执行后,当被监控的程序运行了这些检查点的方法时,btrace会在控制台对执行时间进行输出。通过重定向符>也可以将输出重定向到文件.

API

@BTrace

声明这个类是个BTrace脚本.unsafe参数表示是否不安全的模式执行.

方法annotation

  • @OnMethod: 声明 探查点(probe point)
  • clazz: 全路径类名,支持正则表达式,格式为/正则表达式/
  • +类名 匹配子类
  • @前缀 匹配anotation声明的类
  • method: 方法名,支持正则表达式,格式为/正则表达式/, anotation使用@
  • location: 用@Location来表明在什么时候时候去执行脚本
  • Kind.ENTRY 进入方法时
  • Kind.RETURN 方法返回时
  • Kind.THROW 抛出异常时
  • Kind.ARRAY_SET 设置数组元素时
  • Kind.ARRAY_GET 获取数组元素时
  • Kind.NEWARRAY 创建新数组时
  • Kind.NEW 创建新对象时
  • Kind.CALL 调用方法时
  • Kind.CATCH 捕获异常时
  • Kind.FIELD_SET 获取对象属性时
  • Kind.FIELD_SET 设置对象属性时
  • Kind.ERROR 方法由于发生未被捕获的异常结束时
  • Kind.SYNC_ENTRY 进入同步块时
  • Kind.SYNC_EXIT 离开同步块时
  • type 方法类型, 不含方法名、参数民、异常声明。
  • @OnTimer 定时器,间隔出发动作。
  • 参数: 间隔时间,单位毫秒
  • @OnError BTrace代码发生异常时回调
  • @OnExit BTrace代码调用exit来结束探测时回调该注释的方法
  • @OnEvent 接受客户端事件时会回调。目前,当客户端命令上执行Ctrl-C (SIGINT)时会发送一个时间到服务器端,从而触发@OnEvent注释的方法。
  • @OnLowMemory 内存低于设置的阈值时回调方法
  • pool 内存池名称
  • threshold 阈值大小
  • @OnProbe 支持使用xml格式来声明探测点点和探测动作。

未声明注解的方法参数

未声明注解的方法参数的映射,根据探测点类型locaiton的不同而不同:

  • Kind.ENTRY 方法参数
  • Kind.RETURN 方法返回值
  • Kind.THROW 被抛出的异常
  • Kind.ARRAY_SET 数值下标
  • Kind.ARRAY_GET 数组下标
  • Kind.CATCH 被捕获的异常
  • Kind.FIELD_SET 被设置属性的值
  • Kind.NEW 创建的对象的类型
  • Kind.ERROR 被抛出的异常

字段注解

  • Export 将字段保罗给jstat访问
  • Property 将字段暴露注册为MBean 属性,可以通过JMX进行查看
  • TLS 将字段声明为TheadLocal字段,每个线程拥有自己独立的字段
参数annotation介绍
  • @Self 声明探测的当前对象this
  • @Return 方法返回对象
  • @ProbeClassName 当前探测点所在的类名
  • @ProbeMethodName 当前探测点所在的方法名
  • fqn 是否获取全路径方法名称fully qualified name (FQN)
  • @Duration 执行时间,单位纳秒,一般同 Kind.RETURN 和 Kind.ERROR 配合使用
  • @TargetInstance 配合 Kind.CALL使用,声明了被调用方法所在的对象
  • @TargetMethodOrField Kind.CALL使用,声明了被调用方法所在的对象的方法

BTrace原理分析

BTrace 主要使用了 Instrumentation + ASM技术来实现对正在运行进程的探测。

基本实现逻辑

虚拟机其实提供了一个hook,那就是Instrumentation,可以将独立于应用程序的代理程序agent程序随着着应用程序一起启动或者attach挂载到正在运行中的应用程序。而在代理程序中可以对class进行修改或者重新定义。

btrace的agent程序在btrace-agent.jar,通过挂在vm = VirtualMachine.attach(pid); vm.loadAgent 进行挂载到正在运行中的java进程。 源码如下:

btrace-client.jar/com.sun.btrace.client.Main

    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already.
     */
    public void attach(String pid, String sysCp, String bootCp) throws IOException {

            String agentPath = "/btrace-agent.jar";
            String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
            tmp = tmp.substring(0, tmp.indexOf("!"));
            tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
            agentPath = tmp + agentPath;
            agentPath = new File(new URI(agentPath)).getAbsolutePath();
            attach(pid, agentPath, sysCp, bootCp);

    }


    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already. Accepts the full path of the btrace agent jar.
     * Also, accepts system classpath and boot classpath optionally.
     */
    public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
        try {
            VirtualMachine vm = null;
             byte[] code = client.compile(fileName, classPath, includePath);   // 编译btrace脚本
            vm = VirtualMachine.attach(pid);

           // 此处 省略了诺干代码..
            vm.loadAgent(agentPath, agentArgs); // 挂在 agent 包后,会运行 com.sun.btrace.agent.Main.main 开启server socket后台线程,监听客户端的连接
            client.submit(fileName, code, btraceArgs, createCommandListener(client)); // 将btrace脚本编译后的字节码提交给 正在运行的agent的程序
           
    }

btrace-agent.jar/com.sun.btrace.agent.Main


Thread agentThread = new Thread(new Runnable() {
            @Override
            public void run() {
                BTraceRuntime.enter();
                try {
                    startServer();
                } finally {
                    BTraceRuntime.leave();
                }
            }
        });
        BTraceRuntime.initUnsafe();
        BTraceRuntime.enter();
        try {
            agentThread.setDaemon(true);
            if (isDebug()) debugPrint("starting agent thread");
            agentThread.start();
        } finally {
            BTraceRuntime.leave();
        }

        public static final int BTRACE_DEFAULT_PORT = 2020;

    //-- Internals only below this point
    private static void startServer() {
        int port = BTRACE_DEFAULT_PORT;
        String p = argMap.get("port");
        if (p != null) {
            try {
                port = Integer.parseInt(p);
            } catch (NumberFormatException exp) {
                error("invalid port assuming default..");
            }
        }
        ServerSocket ss;
        try {
            if (isDebug()) debugPrint("starting server at " + port);
            System.setProperty("btrace.port", String.valueOf(port));
            if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
                System.setProperty("btrace.output", scriptOutputFile);
            }
            ss = new ServerSocket(port);
        } catch (IOException ioexp) {
            ioexp.printStackTrace();
            return;
        }

        while (true) {
            try {
                if (isDebug()) debugPrint("waiting for clients");
                Socket sock = ss.accept();
                if (isDebug()) debugPrint("client accepted " + sock);
                Client client = new RemoteClient(inst, sock);
                handleNewClient(client).get();
            } catch (RuntimeException | IOException | ExecutionException re) {
                if (isDebug()) debugPrint(re);
            } catch (InterruptedException e) {
                return;
            }
        }
    }

Instrumentation.ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理
Instrumentation.retransformClasses 对于已经加载的类触发重新加载类定义, 进行ClassFileTransformer转换处理

com.sun.btrace.agent.Main 在会启动一个socket后台线程与btrace client 进行交互. 客户端会将BTRACE 程序的提交到agent去,再通过字节码操作对class进行操作。

在被探测点上增加探测点动作是通过 在Instrumentation代理程序中通过修改字节码来谈价探测点动作。

你可能感兴趣的:(BTrace 简要介绍)