Java 生产神器 BTrace

前言

  在《说说Java单元测试》文章中,强调了单元测试的重要性,也提倡大家一定要写单元测试,能帮我们筛选掉很多低级错误,找出一些没必要的bug,避免生产事故。单元测试通过后,我们开始集成,随着服务集成的日渐增多,业务逻辑也变得越来越复杂,在这样的前提下,解决bug就变得异常复杂。在本地环境中,我们可以通过日志分析 + debug的方式,进行排查解决,再不济,我们还可以开启远程调试进行解决。但当生产系统上出现了bug时,我们可不能开启远程调试,这样会造成线程阻塞,足以让生产系统爆炸的。那么,我们该怎么办呢?代码执行到哪一行了?我们两眼一抹黑,不知从何下手,真是一个悲伤的故事。当然了,我们也可以通过修改代码的方式新增日志,打印方法的入参,出参,上线,重启 … 直至问题得以解决。办法虽然原始,好在能解决问题,但其过程非常痛苦,让人精疲力尽,有很多朋友估计都有过这样的经历。现在回过头来想,如果有这么一个工具,在不修改代码,不重启应用,不上线的前提下,就能查看到代码执行到哪一行,查看某个方法的出参,入参,查看方法的耗时,是该有多好。今天要介绍的就是这么一个工具 - BTrace ( Github主页:https://github.com/btraceio/btrace)

简介

  BTrace 是一个安全,动态的Java追踪工具。通过动态(字节码),动态替换的原理以追踪正在运行的Java程序。简单的说,我们可以使用它在不修改代码,不重启应用,不上线的前提下,查看指定方法的运行环境,如:方法出入参,运行环境,方法耗时等运行时环境。正因为 BTrace 是动态追踪,尽量避免影响线上服务的可用性,在编写BTrace 脚本时,也有如下约定:

  1. 不能创建对象,数组。
  2. 不能throw,catch 异常。
  3. 不能有循环(for,while,do..while)。
  4. 不能实现接口。
  5. 不能有同步块及同步方法。
  6. 不能有断言语句。
  7. 不能有外部,内部,嵌套 或本地类。
  8. 不能进行任何实例好或静态方法调用,只能从com.sun.btrace.BtraceUtils类的公共静态方法。
    ….

别看限制这么多,其实我们可操作的比这更多,其使用语法也非常简单,如下所示:

1
btrace <pid> <btrace-script>

其中 pid 是 需要追踪的 Java 进程 ID (可以通过jps命令查看),btrace-script 脚本是我们需要编写的追踪文件,语法会在下面详细介绍。

环境配置

1. 执行btrace环境

  在使用之前,我们需要下载并安装,像设置JDK环境一样,设置Btrace环境即可。

以Linux为例:

  1. 下载 btrace:

    1
    wget https://github.com/btraceio/btrace/releases/download/v1.3.11.3/btrace-bin-1.3.11.3.tgz
  2. 设置环境,编辑 /etc/profile 文件,向其添加如下内容,(其中 /usr/local/btrace/ 为 btrace 的安装路径)。

    1
    2
    export BTRACE_HOME=/usr/local/btrace/;
    export PATH=$PATH:$BTRACE_HOME;
  3. 使环境变量生效。

    1
    source /etc/profile;
  4. 设置完成后,可以使用 btrace –version 命令验证其有效性。

2. BTrace 脚本编写环境

  比较遗憾的是在 mvnrepository 仓库中,btrace是非常老的版本,通常建议通过Github下载其最新jar包上传至私服中使用,也可以引用本地进行使用,如下所示:

  1. 在 pom.xml 文件中的 properties 标签中,添加如下内容:
    1
    <btrace.home>/java/jar/btrace</btrace.home>

其中 /java/jar/btrace/ 为 btrace的本地路径,可修改为实际路径。

  1. 在 dependencies 标签下,添加如下依赖:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!--btrace start-->
    <dependency>
    <groupId>com.sun.tools.btrace</groupId>
    <artifactId>btrace-agent</artifactId>
    <version>1.3.11.3</version>
    <scope>system</scope>
    <systemPath>${btrace.home}/build/btrace-agent.jar</systemPath>
    </dependency>
    <dependency>
    <groupId>com.sun.tools.btrace</groupId>
    <artifactId>btrace-boot</artifactId>
    <version>1.3.11.3</version>
    <scope>system</scope>
    <systemPath>${btrace.home}/build/btrace-boot.jar</systemPath>
    </dependency>
    <dependency>
    <groupId>com.sun.tools.btrace</groupId>
    <artifactId>btrace-client</artifactId>
    <version>1.3.11.3</version>
    <scope>system</scope>
    <systemPath>${btrace.home}/build/btrace-client.jar</systemPath>
    </dependency>
    <!--btrace end-->

也可以通过Maven plugin 插件的形式运行,这里不再演示,有兴趣的可以在其Github主页上查看方法。

编写 BTrace脚本

  通过上面的一顿操作,是时候表演真正的技术了,下面以运行一段程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@POST
@Path("/load")
@Override
public UserDTO loadUserInfo() {
return getUserInfo("www.andyqian.com","andyqian","andyqian");
}

/**
* 获取用户信息
* @param blog 博客地址
* @param name andyqian
* @param officialAccount 公众号
* @return user DTO
*/
private UserDTO getUserInfo(String blog,String name,String officialAccount){
UserDTO userDTO = new UserDTO();
userDTO.setBlog(blog);
userDTO.setName(name);
userDTO.setOfficialAccount(officialAccount);
// 该段代码纯粹用于增加方法耗时, 无其他任何意义。
try {
Thread.sleep(1000);
}catch (InterruptedException te){
te.printStackTrace();
}
return userDTO;
}

BTrace 脚本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@BTrace
public class BtraceTest {

/**
* 打印用户信息
*/
@OnMethod(clazz="com.jq.wechat.facade.srv.impl.AuthRestServiceImpl", method="getUserInfo",location=@Location(value=Kind.RETURN))
public static void printUserInfo(@Duration long duration,String blog, String name, String officialAccount){
//
BTraceUtils.print("====method duration: "+duration/1000000+" 毫秒=== ");
BTraceUtils.println("blog:"+blog+" name:"+name+" account: "+ officialAccount);
//1. 建议最后一行加上这个,(因为在测试过程中,最后一行未能显示)
BTraceUtils.println();
}
}

执行脚本结果后,我们请求接口多次,其脚本追踪结果如下:

1
2
3
4
andy@andyqian:/java/andyqian/wechat$ btrace 18596 BtraceTest.java 
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian
====method duration: 1000 毫秒=== blog:www.andyqian.com name:andyqian account: andyqian

备注:其中 18596 是我系统演示时的Java进程ID。你使用时,请使用 jps -l 命令查看自己的进程ID。

当参数是对象,我们可以使 BTraceUtils.printFields()方法打印对象属性,也可以使用BTraceUtils.getInt(Field field, Object obj)打印对象中的指定Int类型属性。


  写到这里,篇幅不知不觉已经很长了,上面介绍了BTrace的最基本用法,其实BTrace还是有很多用法值得我们掌握的,我们放在下一篇来介绍。


相关阅读:

Java 基本功 之 CAS

CORS跨域实践

说说面试那些事

记一个有趣的Java OOM!

这里写图片描述

扫码关注,一起进步

个人博客: http://www.andyqian.com