BTrace 告诉你如何在不重启 JVM 的情况下在线调试

Hello 大家好, 我是阿粉,不知道你有没有遇到过这种场景,线上服务跑了一段时间过后偶尔会出现问题,光靠代码和数据分析找不到原因,而且这种情况也不是很常见所以对应的代码也没有加日志输出,如果说重新加上日志进行发布的话,就会破坏现场只能再等一段时间了,或者有的时候想看下接口的参数,从而判断接口参数有没有问题。

这个时候就在想有没有一个好的方法,可以不用重新修改源代码也不用发布升级就可以增加一些日志看到运行状态和入口参数呢?答案当然是肯定的,下面我们就来看一下神器 BTrace

我们模拟一个场景,这个场景就是线上有个服务目前出现问题了,在某些请求触发的时候就会报错,我们现在就想看看报错的时候方法接口的入参的详细信息是什么。

先写一个 demo 代码,内部循环调用一个方法,具体如下,这段代码通过一个循环来模拟一直调用某个方法,这个方法print 的入参是一个 person 包装类,方法的内部处理逻辑这里就忽略了,并且没 3 秒执行一次。现在我们的需求很简单,就是想知道每次运行的时候参数 person 的每个属性值都是什么,话句话说也就是 agename 的值是多少,当然我们也不能修改源代码增加打印和重新发布。

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
28
29
30
31
32
33
34
35
36
37

package com.javajeek.demo;

public class BtraceTest {
    public void print(Person person) {
        System.out.println("--------处理Person ing------");
        try {
          	//...其他逻辑
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void test(Person person) {
        while (true) {
            this.print(person);
        }
    }

    public static void main(String[] args) {
        BtraceTest btraceTest = new BtraceTest();
        Person person = new Person(18, "ziyou");
        btraceTest.test(person);
    }

    static class Person {
        private int age;
        private String name;

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
    }
}

运行结果没什么好说的,就是每隔 3 秒输出一句话,如下。

乍一看大家可能都有点懵,觉得很难实现,这玩意又不是在本地 debug,还能打断点不成。这个时候就需要上我们的神器了,虽然不是本地打断点调试,但是输出一下参数的属性值以及一些 JVM 的状态还是可以实现的。

BTrace

在提供解决方案之前,我们先看下什么是 BTraceBTracesun公司推出的一款 Java 动态、安全追踪(监控)工具,可以在不用重启 JVM 的情况下监控系统运行情况,方便的获取程序运行时的数据信息,如方法参数、返回值、全局变量和堆栈信息。简单来说就是 BTrace 是一个工具,可以帮助我们实时获取运行时的一些数据情况。

下载安装

首先我们先下载压缩包 wget https://github.com/btraceio/btrace/releases/download/v2.2.0/btrace-v2.2.0-bin.tar.gz 下载最新的版本压缩包,下载完成过后解压一下,这里阿粉是在 macOS 上面演示的,如果是 Windows 的小伙伴可以直接到 GitHub 上面下载然后手动解压也是一样的。GitHub 地址 https://github.com/btraceio/btrace/releases/tag/v2.2.0

1
2
wget https://github.com/btraceio/btrace/releases/download/v2.2.0/btrace-v2.2.0-bin.tar.gz
tar xzvf btrace-v2.2.0-bin.tar.gz

解压完了过后,我们可以看到目录结构如下,其中 samples 目录里面有很示例代码感兴趣的小伙伴可以去看看:

使用

在使用 BTrace 的时候我们需要编写一个 Java 脚本,在这个脚本里面表达我们要处理的事情,如果想知道 JVM 的运行情况怎么样,某个类的某个方法的返回值是什么,方法入参是什么等等任何想知道的信息。这里贴一下我们案例里面的解决方法的脚本代码,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.openjdk.btrace.core.annotations.*;
import org.openjdk.btrace.core.types.*;

import static org.openjdk.btrace.core.BTraceUtils.*;
@BTrace
public class AllMethodsTest {
    @OnMethod(
            clazz = "com.lazada.marketing.search.ads.diamond.BtraceTest",
            method = "print",
            location = @Location(Kind.ENTRY)
    )
    public static void m(Object request) {
        printFields(request);
    }
}

可以看到核心的代码就那么几行,跟我们写 Java 代码是非常相似的,也是导入包,写一个类,再定义方法,同时加上了一些自己的注解。通过注解的内容我们可以要处理哪个类以及哪个类的方法。关于 BTrace 有哪些注解,以及每个注解是怎么用的具体可以参考这篇文章 https://ningyu1.github.io/site/post/39-btrace/ 很详细,阿粉觉得很不错,提到有类的注解,方法的注解,以及参数相关的注解。

简单说下上面的这段代码中我们使用到的几个注解,@BTraceOnMethod@Location 代表的含义:

@BTrace:表示这个类是一个 BTrace 程序,BTrace 编译器会强制查找该注解,BTrace 代理也会检查这个是否有该注解。如果没有,则提示错误,并且不会执行。

OnMethod:该注解可用来指定目标类,目标方法,以及目标方法里的“位置”。其中 clazzmethod 分别表示要执行的类和方法,可以用正则来表示。

@Location:表示方法中特定的位置。取值可以参考上面的文章中的表格。

执行

有了上面的运行程序以及 BTrace 的示例代码我们就可以来满足我们的要求了,首先我们的 demo 代码是在运行中的,我们通过jps 命令查询到对应的 pid,操作如下,对应的 pid84287

查到了 pid 过后我们就可以执行 BTrace 程序了,通过命令 ./bin/btrace 84287 samples/AllMethodsTest.java,执行的结果如下图所示。可以看到入参的详细信息都被输出出来了,至此我们的要求已经达到了。小伙伴们是不是很开心,很激动,很兴奋呢?这个使用只是 BTrace 强大功能的冰山一角,还有很多更好用的示例都在 samples 目录下,感兴趣的小伙伴可以去研究研究。

总结

上面的一个示例,阿粉只是很浅显的给大家介绍了一个很简单但是很常用的使用方式,更多强大的使用大家可以自己研究一下,很多小伙伴看到这个功能以为肯定会有一个疑惑,那就是 BTrace 是怎么实现这个功能的,具体的实现阿粉给大家分享一篇美团的技术博客文章,里面介绍的比较详细也比较好理解,大家可以去看一下https://tech.meituan.com/2019/02/28/java-dynamic-trace.html,BTrace 虽然好用,但是使用起来相对还是比较麻烦的,更好用的一个工具相信大家都知道那就是阿里开源的 Arthas,大家可以去学习一下。

Java Geek Tech wechat
欢迎订阅 Java 极客技术,这里分享关于 Java 的一切。