基于Btrace的监控调试

Posted weixin_42412601

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Btrace的监控调试相关的知识,希望对你有一定的参考价值。

目录

简介

1、Btrace是什么?

在生产环境中经常遇到格式各样的问题,如OOM或者莫名其妙的进程死掉。一般情况下是通过修改程序,添加打印日志;然后重新发布程序来完成。然而,这不仅麻烦,而且带来很多不可控的因素。有没有一种方式,在不修改原有运行程序的情况下获取运行时的数据信息呢?如方法参数、返回值、全局变量、堆栈信息等。Btrace就是这样一个工具,它可以在不修改原有代码的情况下动态地追踪java运行程序,通过hotswap技术,动态将跟踪字节码注入到运行类中,对运行代码侵入较小,对性能上的影响可以忽略不计。

2、Btrace的使用限制?

由于Btrace会把脚本逻辑直接侵入到运行的代码中,所以在使用上做很多限制:

  • 不能创建对象
  • 不能使用数组
  • 不能抛出或捕获异常
  • 不能使用循环
  • 不能使用synchronized关键字
  • 属性和方法必须使用static修饰

注意:BTrace植入过的代码,会一直在,直到应用重启为止。所以即使Btrace退出了,业务函数每次执行时都会执行Btrace植入的代码

3、Btrace能做什么?

1、接口性能变慢,分析每个方法的耗时情况;
2、当在Map中插入大量数据,分析其扩容情况;
3、分析哪个方法调用了System.gc(),调用栈如何;
4、执行某个方法抛出异常时,分析运行时参数

安装

1、安装包:
链接:https://pan.baidu.com/s/1MnL6wXRy0y5l573jlgo2BQ
提取码:kh9f

2、配置环境变量
linux下安装设置环境变量:

vi /etc/profile

BTRACE_HOME=/root/btrace/btrace-bin-1.3.11.3
PATH=$PATH:$BTRACE_HOME/bin
export PATH JAVA_HOME

source /etc/profile

window下安装设置环境变量:

BTRACE_HOME  E:\\btrace-1.3.11.3
%BTRACE_HOME%\\bin;

设置完后:验证是否安装成功

btrace --version
BTrace v.1.3.11.3 (20181217)

使用

1、获取方法的参数值

在工程里新建一个简单的controller,用于演示如何利用BTrace脚本来实时获取方法的参数值:

@Controller
public class BtraceController 

    @ResponseBody
    @GetMapping("/hello")
    public String hello(String name)
        try 
            Thread.sleep(1000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        return "hello "+name;
    

依赖:

        <!--btrace start-->
        <dependency>
            <groupId>com.sun.btrace</groupId>
            <artifactId>btrace-agent</artifactId>
            <version>1.3.11</version>
            <type>jar</type>
            <scope>system</scope>
            <systemPath>E:/btrace-1.3.11.3/build/btrace-agent.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.sun.btrace</groupId>
            <artifactId>btrace-boot</artifactId>
            <version>1.3.11</version>
            <type>jar</type>
            <scope>system</scope>
            <systemPath>E:/btrace-1.3.11.3/build/btrace-boot.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.sun.btrace</groupId>
            <artifactId>btrace-client</artifactId>
            <version>1.3.11</version>
            <type>jar</type>
            <scope>system</scope>
            <systemPath>E:/btrace-1.3.11.3/build/btrace-client.jar</systemPath>
        </dependency>
        <!--btrace end-->

clean install一下,部署到服务器里,模仿生产环境。

编写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;
// 注明这是一个BTrace脚本
@BTrace
public class PrintArgSimple 
    // 指定需要拦截的方法
	@OnMethod(
            // 类的路径
	        clazz="com.example.designpattern.controller.BtraceController",
            // 方法名
	        method="hello",
            // 在什么时候进行拦截
	        location=@Location(Kind.ENTRY)
	)
	// 被拦截的类名;被拦截的方法名;被拦截的方法的参数值
	public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) 
		// 打印数组
		BTraceUtils.printArray(args);
        // 打印行
		BTraceUtils.println(pcn+","+pmn);
		BTraceUtils.println();
    

把脚本放到服务器里执行:

[root@localhost btrace]# jps -l
21557 /root/project/designpattern-0.0.1-SNAPSHOT.jar
21621 sun.tools.jps.Jps
[root@localhost btrace]# btrace 21557 PrintArgSimple.java 
[zhangsan, ]
com.example.designpattern.controller.BtraceController,hello

访问接口:http://192.168.169.129:8019/hello?name=zhangsan

2、拦截构造函数、同名函数

2.1 拦截构造函数

添加controller,重新部署

    @ResponseBody
    @RequestMapping(value = "/constructor")
    public Teacher constructor(Teacher teacher)
        return teacher;
    
public class Teacher 
    private Integer id;
    private Integer age;
    private String name;
    public Teacher(Integer id, Integer age, String name) 
        this.id = id;
        this.age = age;
        this.name = name;
    
    public Integer getId() 
        return id;
    
    public void setId(Integer id) 
        this.id = id;
    
    public Integer getAge() 
        return age;
    
    public void setAge(Integer age) 
        this.age = age;
    
    public String getName() 
        return name;
    
    public void setName(String name) 
        this.name = name;
    

btrace脚本:
改类的相对路径

@BTrace
public class PrintConstructor 
	@OnMethod(
			//拦截的类
	        clazz="com.example.designpattern.controller.Teacher",
	        //指定拦截构造函数
			method="<init>"
	)
	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();
    

启动btrace脚本并访问接口:

[root@localhost btrace]# jps -l
22729 /root/project/designpattern-0.0.1-SNAPSHOT.jar
22794 sun.tools.jps.Jps[root@localhost btrace]# btrace 22729 PrintConstructor.java
ClassName: com.example.designpattern.controller.Teacher
MethodName: <init>
[1, 14, zhangsan, ]

2.1 拦截同名方法

controller:

    /**
     * 测试拦截同名函数
     * @param name
     * @return
     */
    @ResponseBody
    @RequestMapping("/same1")
    public String same(@RequestParam("name")String name) 
        return  "hello,"+name;
    
    @ResponseBody
    @RequestMapping("/same2")
    public String same(@RequestParam("name")String name,@RequestParam("id")int id) 
        return  "hello,"+name+","+id;
    

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;
@BTrace
public class PrintSame 
	@OnMethod(
			clazz = "com.example.designpattern.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 = "com.example.designpattern.controller.BtraceController",
			//方法
			method = "same"
	)
	public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name,int id ) 
		BTraceUtils.println("ClassName: " + pcn);
		BTraceUtils.println("MethodName: " + pmn);
		BTraceUtils.println("id: " + id);
		BTraceUtils.println("name: " + name);
		BTraceUtils.println();
	

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

测试:

[root@localhost btrace]# btrace 24450 PrintSame.java
ClassName: com.example.designpattern.controller.BtraceController
MethodName: same
name: aa

ClassName: com.example.designpattern.controller.BtraceController
MethodName: same
id: 123
name: aa

3、拦截返回值、异常、行号

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

1、拦截返回值

bstace脚本:

@BTrace
public class PrintReturn 
    @OnMethod(
            clazz="com.example.designpattern.controller.BtraceController",
            method="hello",
            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();
    

访问hello接口

[root@localhost btrace]# jps -l
20460 /root/project/designpattern-0.0.1-SNAPSHOT.jar
20492 sun.tools.jps.Jps
[root@localhost btrace]# btrace 20460 PrintReturn.java
ClassName: com.example.designpattern.controller.BtraceController
MethodName: hello
ResultValue: hello 23

2、异常

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

    /**
     * 测试拦截时机--异常
     * @return
     */
    @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脚本,代码如下,这是我直接从官方示例里拿来的代码:

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="<init>"
    )
    public static void onthrow(@Self Throwable self) //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="java.lang.Throwable",
        method="<init>",
        location=@Location(Kind.RETURN)
    )
    public static void onthrowreturn() 
        if (currentException != null) 
        	BTraceUtils.Threads.jstack(currentException);
        	BTraceUtils.println("=====================");
            currentException = null;
        
    

[root@localhost btrace]# btrace 20871 PrintOnThrow.java >1.log

3、行号

我们还可以使用BTrace拦截某一行代码,以此判断该行代码是否有被执行。在btrace包下新建一个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;
@BTrace
public 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();
    

注意:line=-1 打印所有行号
在命令行里运行该脚本,访问相应的接口后,输出的信息如下:

E:\\Java_IDEA\\monitor_tuning\\src\\main\\java\\org\\zero01\\monitor_tuning\\btrace>btrace 14952 PrintLine.java
ClassName: org.zero01.monitor_tuning.controller.BTraceController
MethodName: exception
line: 43

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

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

5.1、拦截复杂参数

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

    @RequestMapping("arg2")
    public Teacher arg2(Teacher teacher) 
        return  teacher;
    

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

import java.lang.reflect.Field;
import com.example.designpattern.controller.Teacher;
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;
@BTrace
public class PrintArgComplex 
	@OnMethod(
	        clazz="com.example.designpattern.controller.BtraceController",
	        method="arg2",
	        location=@Location(Kind.ENTRY)
	)
	public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, Teacher teacher) 
		BTraceUtils.printFields(teacher);
		Field filed2 = BTraceUtils.field("com.example.designpattern.controller.Teacher", "name");
		BTraceUtils.println(BTraceUtils.get(filed2, teacher));
		BTraceUtils.println(pcn+","+pmn);
		BTraceUtils.println();
    

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

F:\\ideaWebProject\\designpattern\\src\\main\\java\\com\\example\\designpattern\\btrace>btrace -cp "F:\\ideaWebProject\\designpattern\\target\\classes"  1668 PrintArgComplex.java
id=1, age=22, name=aaa, 
aaa
com.example.designpattern.controller.BtraceController,arg2

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

如果是linux下,在执行btrace脚本的时候,需要将User对象加入到classpath里面来。如果某个对象是jdk原生的对象,则无需要这个操作。因此,需要将jdk原生类型之外的类型加入到classpath里面来。

adog@E531:chapter4$ btrace -cp /home/adog/IdeaProjects/monitor_tuning0822/target/classes 7571 PrintArgComplex.java

5.2、环境变量

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

@BTrace
public 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);
    

5.3、正则匹配拦截

其实我们在编写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;

@BTrace
public 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();
    

6、获取方法的执行时间

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TraceMethodTime 
    //将字段声明为TheadLocal字段,每个线程拥有自己独立的字段
    @TLS
    private static long startTime = 0;

    @OnMethod(
            clazz = "com.example.designpattern.controller.BtraceController",
            method ="/.*/",
            location=@Location(Kind.ENTRY)
    )
    public static void methodStart()
        startTime = timeNanos();
    
    @OnMethod(
            clazz = "com.example.designpattern.controller.BtraceController",
            method ="/.*/",
            location=@Location(Kind.RETURN)
    )
    //@Duration 执行时间,单位纳秒,一般同 Kind.RETURN 和 Kind.ERROR 配合使用
    public static void methodEnd(@Duration long uration)
        //1秒=1000000000纳秒
        long endTime = timeNanos();
        println(strcat("execute time(nanos): ", str(endTime-startTime)));
        println("spend time:"+(endTime-startTime));
    

7、定时获取Counter类的属性值totalCount。

import com.sun.btrace.annotations.*;  
import static com.sun.btrace.BTraceUtils.*;  
 
@BTrace  
public class TracingScript   
   private static Object totalCount = 0;  
    
   @OnMethod(  
      clazz="com.learnworld.Counter",  
      method="add",  
      location=@Location(Kind.RETURN)  
   )   
   public static void traceExecute(@Self com.learnworld.Counter counter)  
     totalCount = get(field("com.learnworld.Counter","totalCount"), counter);  
      
      
   @OnTimer(1000)  
   public static void print()  
     println("====== ");  
     println(strcat("totalCount: ",str(totalCount)));  
     
  

8、定位某个超过阈值的函数

BTrace脚本

**
 * 探测某个包路径下的方法执行时间是否超过某个阈值的程序,如果超过了该阀值,则打印当前线程的栈信息。
 */
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class PrintDurationTracer 
	@OnMethod(clazz = "/com\\\\.techstar\\\\.monitordemo\\\\..*/", method = "/.*/", location = @Location(Kind.RETURN))
	public static void trace(@ProbeClassName String pcn, @ProbeMethodName String pmn, @Duration long duration) 
		//duration的单位是纳秒
		if (duration > 1000 * 1000 * 2) 
			BTraceUtils.println(Strings.strcat(Strings.strcat(pcn, "."), pmn));
			BTraceUtils.print(" 耗时:");
			BTraceUtils.print(duration);
			BTraceUtils.println("纳秒,堆栈信息如下");
			jstack();
		
	

拦截结果:

192:Btrace apple$ btrace 39644 PrintDurationTracer.java 
com.techstar.monitordemo.controller.Adder.execute 耗时:1715294657纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)
com.techstar.monitordemo.controller.Main.main(Main.java:10)
com.techstar.monitordemo.controller.Adder.execute 耗时:893795666纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)
com.techstar.monitordemo.controller.Main.main(Main.java:10)
com.techstar.monitordemo.controller.Adder.execute 耗时:1331363658纳秒,堆栈信息如下
com.techstar.monitordemo.controller.Adder.execute(Adder.java:13)

9、性能分析

压测的时候经常发现某一个服务变慢了,但是由于这个服务有很多的业务逻辑和方法构成,这个时候就不好定位到底慢在哪个地方。BTrace可以解决这个问题,只需要大概定位问题可能存在的地方,通过包路径模糊匹配,就可以找到问题。

BTrace脚本

/**
 *
 * Description:
 * This script demonstrates new capabilities built into BTrace 1.2
 * Shortened syntax - when omitting "public" identifier in the class
 * definition one can safely omit all other modifiers when declaring methods
 * and variables
 * Extended syntax for @ProbeMethodName annotation - you can use
 * parameter to request a fully qualified method name instead of
 * the short one
 * Profiling support - you can use @linkplain Profiler instance to gather
 * performance data with the smallest overhead possible
 */
@BTrace
class Profiling 
	@Property
	Profiler profiler = BTraceUtils.Profiling.newProfiler();

	@OnMethod(clazz = "/com\\\\.techstar\\\\..*/", method = "/.*/")
	void entry(@ProbeMethodName(fqn = true) String probeMethod) 
		BTraceUtils.Profiling.recordEntry(profiler, probeMethod);
	

	@OnMethod(clazz = "/com\\\\.techstar\\\\..*/", method = "/.*/", location = @Location(value = Kind.RETURN))
	void exit(@ProbeMethodName(fqn = true) String probeMethod, @Duration long duration) 
		BTraceUtils.Profiling.recordExit(profiler, probeMethod, duration);
	

	@OnTimer(5000)
	void timer() 
		BTraceUtils.Profiling.printSnapshot("Performance profile", profiler);
	

10、死锁排查

我们怀疑程序是否有死锁,可以通过以下的脚步扫描追踪,非常简单方便。

/**
 * This BTrace program demonstrates deadlocks
 * built-in function. This example prints
 * deadlocks (if any) once every 4 seconds.
 */
@BTrace
public class PrintDeadlock 
	@OnTimer(4000)
	public static void print() 
		deadlocks();
	

11、分析哪个方法调用了S

以上是关于基于Btrace的监控调试的主要内容,如果未能解决你的问题,请参考以下文章

生产环境中系统调试--BTrace

性能工具之Java调试工具BTrace入门

04 使用 BTrace 进行拦截调试

04 使用 BTrace 进行拦截调试

利用jvisualvm使用btrace进行线上调试案例

Btrace打印自定义引用类方法参数