BTrace 简要介绍

作者: HelloArmin | 来源:发表于2016-10-28 14:49 被阅读3138次

    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> <btrace-script>
    
    • pid 为java进程号,可以使用jps来查询
    • btrace-script 为 btrace脚本

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

    visualvm-plugin

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

    安装

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

    jvisualvm_btrace.jpg

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

    执行btrace脚本

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

    jvisualvm_btrace2.png

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

    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 简要介绍

        本文链接:https://www.haomeiwen.com/subject/fbkeuttx.html