博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Btrace的监控调试
阅读量:5961 次
发布时间:2019-06-19

本文共 18535 字,大约阅读时间需要 61 分钟。

btrace快速入门

简介:

BTrace是Java的安全可靠的动态跟踪工具。 它的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。也就是说btrace可以在Java程序运行时,动态地向目标应用程序的字节码注入追踪代码。

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

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

这些限制其实是可以使用unsafe模式绕过。通过在BTrace脚本中声明 @BTrace(unsafe = true) 注解 ,并且使用 -u 选项,指定以 unsafe 模式运行btrace即可

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

btrace的github地址:

安装btrace:

到github上下载btrace的压缩包,我这里下载的是1.3.11版本,下载地址如下:

解压下载的压缩包,解压后可以看到目录如下:

基于Btrace的监控调试

然后就是配置环境变量,首先创建一个BTRACE_HOME,该环境变量的值就是btrace的安装目录:

基于Btrace的监控调试

接着在path变量里,配置BTRACE_HOME环境变量下的bin目录:

基于Btrace的监控调试

配置好btrace后,我们有两种方式可以运行BTrace脚本:

  • 一是在JVisualVM中添加BTrace插件,添加classpath
  • 二是直接使用命令行运行:btrace < pid > < trace_script >

我们先来演示第二种使用命令行的运行方式,在工程里新建一个简单的controller,用于演示如何利用BTrace脚本来实时获取方法的参数值:

package org.zero01.monitor_tuning.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;/** * @program: monitor_tuning * @description: BTrace演示 * @author: 01 * @create: 2018-07-14 22:33 **/@RestController@RequestMapping("/btrace")public class BTraceController {    @RequestMapping("/arg1")    public String arg1(@RequestParam("name") String name) {        return "hello: " + name;    }}

编写完这个Controller后就可以启动项目了,为了方便演示,我们直接将BTrace脚本写在这个工程里,实际上BTrace脚本与我们的项目代码是分离的,没有依赖关系的。但我们得引入编写BTrace脚本所需的jar包,pom.xml配置的依赖如下

com.sun.btrace
btrace-agent
1.3.11
jar
system
E:\BTrace\btrace-bin-1.3.11\build\btrace-agent.jar
com.sun.btrace
btrace-boot
1.3.11
jar
system
E:\BTrace\btrace-bin-1.3.11\build\btrace-boot.jar
com.sun.btrace
btrace-client
1.3.11
jar
system
E:\BTrace\btrace-bin-1.3.11\build\btrace-client.jar

注:systemPath配置的是BTrace安装目录里的jar包路径,如果不想指向路径的话,可以使用maven命令,把这些jar包安装到本地的maven仓库中,命令示例如下:

mvn install:install-file -Dfile=E:\BTrace\btrace-bin-1.3.11\build\btrace-boot.jar -DgroupId=com.sun.btrace -DartifactId=btrace-boot -Dversion=1.3.11 -Dpackaging=jar

然后新建一个btrace包,在该包内新建一个 PrintArgSimple 类,代码如下:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.AnyType;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.Kind;import com.sun.btrace.annotations.Location;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;/** * @program: monitor_tuning * @description: BTrace脚本 * @author: 01 * @create: 2018-07-14 22:33 **/@BTrace  // 注明这是一个BTrace脚本public class PrintArgSimple {    // 指定需要拦截的方法    @OnMethod(            // 类的路径            clazz = "org.zero01.monitor_tuning.controller.BTraceController",            // 方法名            method = "arg1",            // 在什么时候进行拦截            location = @Location(Kind.ENTRY)    )    public static void anyRead(@ProbeClassName String pcn, // 被拦截的类名                               @ProbeMethodName String pmn, // 被拦截的方法名                               AnyType[] args // 被拦截的方法的参数值    ) {        // 打印数组        BTraceUtils.printArray(args);        // 打印行        BTraceUtils.println("className: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println();    }}

打开命令行,进入到PrintArgSimple.java文件所在的路径,然后使用命令运行该BTrace脚本:

C:\Users\admin>e:E:\>cd E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace\E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>jps113007396 Jps8504 MonitorTuningApplication5656 RemoteMavenServer12188 Main13916 LauncherE:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace 8504 PrintArgSimple.java

注:因为代码中有中文注释,可能会报:编码GBK的不可映射字符 错误,但是并不影响运行,可以不用管它

在命令行中运行了BTrace脚本后,到浏览器上,访问我们之前所编写的接口,如下:

基于Btrace的监控调试

访问成功后,命令行输出如下,可以看到BTrace脚本成功拦截了我们输入的参数值,以及访问的controller和方法:

[小明, ]className: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: arg1

以上就是如何在命令行里运行BTrace脚本。接着我们再来演示如何在JVisualVM里运行BTrace脚本,在此之前我们还需要安装一个BTrace的插件。关于BTrace插件的安装,我已经在 一文中介绍过了,这里就不赘述了。复制好之前编写的BTrace脚本,然后按照下图粘贴到JVisualVM里运行即可:

基于Btrace的监控调试

注:在JVisualVM里运行BTrace脚本其实会有很多迷之问题,所以一般推荐使用命令行的运行方式


拦截构造函数、同名函数

在上一小节中,我们简单介绍了BTrace的安装以及两种运行方式。并且也写了一个小demo用于动态地拦截方法参数,而本节将介绍一下如何拦截构造函数和同名方法。

首先我们新建一个实体类,代码如下:

package org.zero01.monitor_tuning.vo;public class User {    private int id;    private String name;    public User(int id, String name) {        super();  // 必须加上super不然无法拦截到参数        this.id = id;        this.name = name;    }    ... gtter setter 略 ...}

在BTraceController类中,新增如下方法,并启动项目:

@RequestMapping("/constructor")public User constructor(User user) {    return user;}

然后和之前一样,在btrace包下新建一个BTrace脚本,代码如下:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.AnyType;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;/** * @program: monitor_tuning * @description: 演示拦截构造函数 * @author: 01 * @create: 2018-07-14 22:33 **/@BTracepublic class PrintConstructor {    @OnMethod(            clazz = "org.zero01.monitor_tuning.vo.User",            method = "
" // 指定拦截构造函数 ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.printArray(args); BTraceUtils.println(); }}

同样的,在命令行中运行该脚本:

E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>jps1184 MonitorTuningApplication1130015208 Launcher5656 RemoteMavenServer1052 JpsE:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace 1184 PrintConstructor.java

使用postman或浏览器访问接口:

基于Btrace的监控调试

访问成功后,命令行输出如下,则是成功拦截到了:

ClassName: org.zero01.monitor_tuning.vo.UserMethodName: 
[1, 小明, ]

演示完构造函数的拦截后,然后我们来看看如何拦截同名的方法,在 BTraceController 类里,增加如下两个同名方法:

@RequestMapping("/same1")public String same(@RequestParam("name") String name) {    return "hello: " + name;}@RequestMapping("/same2")public User same(@RequestParam("id") int id,                 @RequestParam("name") String name) {    return new User(id, name);}

然后在btrace包下新建一个BTrace脚本,代码如下:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;@BTracepublic class PrintSame {    @OnMethod(            clazz = "org.zero01.monitor_tuning.controller.BTraceController",            method = "same"    )    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) {        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println("name: " + name);        BTraceUtils.println();    }    @OnMethod(            clazz = "org.zero01.monitor_tuning.controller.BTraceController",            method = "same"    )    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) {        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println("id: " + id);        BTraceUtils.println("name: " + name);        BTraceUtils.println();    }}

从以上的BTrace脚本中可以看到,其实拦截重载方法的话,只需要在BTrace脚本的方法中声明与之对应的参数即可。

接着就是在命令行里运行该脚本,访问相应的接口后,输出如下:

E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>jps9024 MonitorTuningApplication113003956 Launcher5656 RemoteMavenServer9272 JpsE:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace 9024 PrintSame.javaClassName: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: samename: 小明ClassName: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: sameid: 1name: 小明

拦截返回值、异常、行号

本小节将介绍常用的拦截方式,如何去拦截返回值、异常以及行号。

我们通过Kind来指定拦截方式,常用的拦截方式如下:

Kind.ENTRY  // 入口拦截,默认值Kind.RETURN  // 拦截返回值Kind.THROW  // 发生异常时拦截Kind.LINE  // 拦截某一行

我们先来演示拦截返回值,就以之前编写的arg1方法作为示例。

在btrace包下新建一个BTrace脚本,代码如下:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.AnyType;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.Kind;import com.sun.btrace.annotations.Location;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;import com.sun.btrace.annotations.Return;@BTracepublic class PrintReturn {    @OnMethod(            clazz="org.zero01.monitor_tuning.controller.BTraceController",            method="arg1",            location=@Location(Kind.RETURN)  // 拦截返回值    )    public static void anyRead(@ProbeClassName String pcn,                                @ProbeMethodName String pmn,                                @Return AnyType result // 接收返回值) {        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println("ResultValue: " + result);        BTraceUtils.println();    }}

在命令行里运行该脚本,访问相应的接口后,输出如下,可以看到成功拦截到了该方法的返回值:

E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace 9024 PrintReturn.javaClassName: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: arg1ResultValue: hello: 小明

已经工作了的小伙伴们,应该会遇到一个常见的现象。那就是在很多旧项目的遗留代码中,总是能看到很多不妥的处理异常的方式。例如经常能看到把异常使用try-catch包起来,但是又不打印异常堆栈,也不抛出去。结果就是把异常隐藏了起来,没能让异常终止逻辑,导致了代码继续执行,还无法定位问题的所在。就如同以下这个方法一样:

@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这样的调试工具,那么就可以利用BTrace脚本来拦截异常并打印异常堆栈,从而定位问题。

在btrace包下新建一个BTrace脚本,代码如下,这是我直接从官方示例里拿来的代码:

/* * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation.  Oracle designates this * particular file as subject to the Classpath exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */package org.zero01.monitor_tuning.btrace;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.Kind;import com.sun.btrace.annotations.Location;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.Self;import com.sun.btrace.annotations.TLS;@BTrace public class PrintOnThrow {        // store current exception in a thread local    // variable (@TLS annotation). Note that we can't    // store it in a global variable!    @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="
" ) public static void onthrow(@Self Throwable self) { // @Self其实就是拦截了this //new Throwable() currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="
" ) public static void onthrow1(@Self Throwable self, String s) { //new Throwable(String msg) currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="
" ) 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="
" ) 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="java.lang.Throwable", method="
", location=@Location(Kind.RETURN) ) public static void onthrowreturn() { if (currentException != null) { // 打印异常堆栈 BTraceUtils.Threads.jstack(currentException); BTraceUtils.println("====================="); // 打印完之后就置空 currentException = null; } }}

在命令行里运行该脚本,访问相应的接口后,输出的异常堆栈如下:

基于Btrace的监控调试

可以看到,即便异常被隐藏了起来,而我们通过btrace脚本一样能把异常重新给揪出来,这样即便是线上正在运行的项目,我们也能够不关闭、不重启服务,就能够监控到项目里是否有发生异常。


我们还可以使用BTrace拦截某一行代码,以此判断该行代码是否有被执行。在btrace包下新建一个BTrace脚本,代码如下:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.Kind;import com.sun.btrace.annotations.Location;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;@BTracepublic class PrintLine {    @OnMethod(            clazz="org.zero01.monitor_tuning.controller.BTraceController",            method="exception",            location=@Location(value=Kind.LINE, line=43)  // 拦截第43行    )    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) {        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println("line: " + line);        BTraceUtils.println();    }}

在命令行里运行该脚本,访问相应的接口后,输出的信息如下:

E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace 14952 PrintLine.javaClassName: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: exceptionline: 43

如果没有任何输出的话,就代表那一行没有被执行到,所以没被拦截。这种拦截某一行的方式,不适用于判断是否有异常,只能单纯用于判断某一行是否被执行了。


拦截复杂参数、环境变量、正则匹配拦截

在以上小节中,我们已经知道了如何拦截简单的参入,本小节中,将介绍如何拦截复杂参数、环境变量以及使用正则匹配拦截。

拦截复杂参数就是拦截实体对象类型的参数,在 BTraceController 类里,增加如下方法:

@RequestMapping("/arg2")public User arg2(User user) {    return user;}

使用BTrace拦截复杂参数,需要使用反射的方式进行拦截,也就是需要传递包名+属性名。在btrace包下新建一个BTrace脚本,代码如下:

package org.zero01.monitor_tuning.btrace;import java.lang.reflect.Field;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.Kind;import com.sun.btrace.annotations.Location;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;import org.zero01.monitor_tuning.vo.User;@BTracepublic class PrintArgComplex {    @OnMethod(            clazz = "org.zero01.monitor_tuning.controller.BTraceController",            method = "arg2",            location = @Location(Kind.ENTRY)    )    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) {        //print all fields        BTraceUtils.print("print all fields: ");        BTraceUtils.printFields(user);        //print one field        Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name");        BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user));        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println();    }}

在命令行里运行该脚本,访问相应的接口后,输出的信息如下:

E:\Java_IDEA\monitor_tuning\src\main\java\org\zero01\monitor_tuning\btrace>btrace -cp "E:\Java_IDEA\monitor_tuning\target\classes" 13336 PrintArgComplex.javaprint all fields: {id=0, name=小明, }print one field: 小明ClassName: org.zero01.monitor_tuning.controller.BTraceControllerMethodName: arg2

注:这里使用到了一个 -cp 参数,该参数表示指定一个classpath路径


其实我们在编写BTrace脚本时,是可以使用正则表达式匹配类名和方法名的,并非必须要指定一个完整的名称。如下示例:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;import com.sun.btrace.annotations.OnMethod;import com.sun.btrace.annotations.ProbeClassName;import com.sun.btrace.annotations.ProbeMethodName;@BTracepublic class PrintRegex {    @OnMethod(            // 类名也可以使用正则表达式进行匹配            clazz = "org.zero01.monitor_tuning.controller.BTraceController",              // 正则表达式需要写在两个斜杠内            method = "/.*/"      )    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) {        BTraceUtils.println("ClassName: " + pcn);        BTraceUtils.println("MethodName: " + pmn);        BTraceUtils.println();    }}

通过编写BTrace脚本我们可以打印出JVM的信息以及环境变量等,就类似于jinfo命令一样。代码示例:

package org.zero01.monitor_tuning.btrace;import com.sun.btrace.BTraceUtils;import com.sun.btrace.annotations.BTrace;@BTracepublic class PrintJinfo {    static {        // 打印系统属性        BTraceUtils.println("System Properties:");        BTraceUtils.printProperties();        // 打印JVM参数        BTraceUtils.println("VM Flags:");        BTraceUtils.printVmArguments();        // 打印环境变量        BTraceUtils.println("OS Enviroment:");        BTraceUtils.printEnv();        // 退出脚本        BTraceUtils.exit(0);    }}

在命令行里运行该脚本,输出的信息如下:

基于Btrace的监控调试


注意事项

在以上小节中,我们介绍了BTrace的常见用法,也编写了相应的demo进行一一演示。但是还有一些需要注意事项,在此说明一下:

  • BTrace脚本默认只能本地运行,也就是只能调试本地的Java进程。如果需要在本地调试远程的Java进程的话,是需要自己去修改BTrace源码的
  • BTrace脚本在生产环境下可以使用,但是被修改的字节码不会被还原。所以我们需要先在本地调试好BTrace脚本,然后才能放到生产环境下使用。并且需要注意BTrace脚本中不能含有影响性能或消耗资源较多的代码,不然会导致线上的服务性能降低。

参考文档

btrace下载

转载于:https://blog.51cto.com/zero01/2143096

你可能感兴趣的文章
checkUser----dede
查看>>
Ngnix的日志管理和用定时任务完成日志切割
查看>>
IOS控件 Tableview 下拉刷新,加载数据
查看>>
log4j 2使用教程
查看>>
centos 安装pip
查看>>
我的友情链接
查看>>
rhel5下ORACLE 10g之ASM创建
查看>>
Java基础学习总结(19)——Java环境变量配置
查看>>
关于Notice: Undefined index:问题解决方法
查看>>
MyBatis学习总结(9)——使用MyBatis Generator自动创建代码
查看>>
MyBatis学习总结(六)——调用存储过程
查看>>
利用rrdcached优化ganglia IO
查看>>
开源史上最成功的8个开源产品
查看>>
JNDI学习总结(1)——JNDI入门
查看>>
2015年终总结
查看>>
使用Emacs阅读邮件和新闻组:Gnus 中文FAQ
查看>>
支付宝 iOS SDK 的简单使用
查看>>
锐捷RG-12010交换机VSU虚拟化配置
查看>>
在JSP页面中调用另一个JSP页面中的变量
查看>>
CISCO路由与交换笔记
查看>>