Java动态,安全追踪工具
在我们日常的开发中,总是难以避免的要解决线上的问题.如果线上的问题我们在本地调试的时候无论调试多少次发现明明本地调用了这个方法呀,怎么线上就是没调呢?还有就是出了问题的时候由于没有打日志,所以不得不去价格logger,然后换个包,然后再重启,然后再调用,如果在用户很多的时候这么搞,无疑面临着巨大的风险,还不得不去处理用户的大量的投诉,在领导面前也只能默默的低着头承受着批评
那么有没有一种方法可以不用重启应用,又可以在线上追踪代码呢? 答案当然是有的,就是使用Btrace这个工具了
BTrace是sun公司推出的一款Java 动态、安全追踪(监控)工具,可以在不用重启的情况下监控系统运行情况,方便的获取程序运行时的数据信息,如方法参数、返回值、全局变量和堆栈信息等,并且做到最少的侵入,占用最少的系统资源。
应用场景:
- 服务慢,但是不知道慢在哪一步,哪个函数慢
- 谁调用了System.gc(),调用栈如何?
- 谁构造了一个超大的ArrayList?
- 执行某个方法抛出异常时,分析运行时参数
......
快速开始
btrace的官方的网址是http://github.com/btraceio/btrace , 可以从里面下载最新的版本, 目前版本是1.3.11.2
下载好之后doc里面有很多例子, 我们这里来跑一个UserGuide的例子
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.OnMethod;
import static com.sun.deploy.trace.Trace.println;
/**
* @author luozhiyun on 2019-01-06.
*/
@BTrace
public class HelloWorld {
@OnMethod(clazz="java.lang.Thread", method="start")
public static void onThreadStart() {
println("thread start!");
}
}
然后在要监控的机器上打jps, 找到应用的pid, 然后 btrace $pid HelloWorld.java就可以跑起来了
如果还想要监控其他内容,直接修改HelloWorld.java的内容,然后再执行一次btrace就可以了,完全不需要重启应用!!!
拦截
1.直接指出类和方法 ,如HelloWorld的例子
2.正则表达式拦截
如下, 拦截所有的有关InputStream类的readXXX方法,正则表达式要写在两个/中间
package com.sun.btrace.samples;
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
/**
* This BTrace class demonstrates that we can
* probe into multiple classes and methods by a
* single probe specification using regular
* expressions for class and/or method names as
* given below. In the example, we put probe into
* all readXXX methods of all InputStream classes.
*/
@BTrace public class MultiClass {
@OnMethod(
clazz="/java\.io\..*Input.*/",
method="/read.*/"
)
public static void onread(@ProbeClassName String pcn) {
println("read on " + pcn);
}
}
3.拦截所有有这个注解的类或方法
如: @OnMethod( clazz="@javax.jws.WebService", method="@javax.jws.WebMethod" )
4.拦截接口的实现类
如:@OnMethod( clazz="+java.lang.Runnable", method="run" )
5.拦截构造器
@OnMethod(clazz="java.net.ServerSocket", method="<init>")
6.拦截静态内部类
@OnMethod(clazz="com.vip.MyServer$MyInnerClass", method="hello")
只要在类和内部类前面加上$
拦截时机
Kind.ENTRY
在@onMethod这个注解里面可以加上一个location参数,表示拦截的这个方法的拦截时机,如果不加,默认就是Kind.ENTRY
Kind.RETURN
如果要获得返回结果或执行时间, 那么就要加上Kind.RETURN.
OnMethod(clazz = "java.net.ServerSocket", method = "getLocalPort", location = @Location(Kind.RETURN))
public static void onGetPort(@Return int port, @Duration long duration)
duration的单位是纳秒,要除以 1,000,000 才是毫秒。
Kind.Error, Kind.Throw和 Kind.Catch
这几个主要用于异常情况的跟踪
在拦截函数的参数定义里注入一个Throwable的参数,代表异常。
@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(Kind.ERROR))
public static void onBind(Throwable exception, @Duration long duration)
Kind.Line
下例监控代码是否到达了Socket类的第363行
@OnMethod(clazz = "java.net.ServerSocket", location = @Location(value = Kind.LINE, line = 363))
public static void onBind4() {
println("socket bind reach line:363");
}
Kind.Call
下例就是打印出run()方法里面调用的所有其他方法
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.println;
/**
* @author luozhiyun on 2019-01-06.
*/
@BTrace
public class HelloWorld {
@OnMethod(clazz="BtraceCase", method="run",
location =@Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER))
public static void onThreadStart(@Self Object self, @TargetInstance Object instance,
@TargetMethodOrField String method, @Duration long duration) {
println("self: " + self);
println("instance: " + instance);
println("method: " + method);
println("duration: " + duration);
}
}
BtraceCase
import java.util.Random;
/**
* @author luozhiyun on 2019-01-06.
*/
public class BtraceCase {
public static Random random = new Random();
public int size;
public static void main(String[] args) throws Exception {
Thread.sleep(1000*15);
new BtraceCase().run();
}
public void run() throws Exception {
while (true) {
add(random.nextInt(10), random.nextInt(10));
}
}
public int add(int a, int b) throws Exception {
Thread.sleep(random.nextInt(10) * 100);
return a + b;
}
}
打印结果:
self: BtraceCase@63947c6b
instance: BtraceCase@63947c6b
method: add
duration: 402211413
self: BtraceCase@63947c6b
instance: java.util.Random@2b193f2d
method: nextInt
duration: 2602
所调用的类及方法名所注入到@TargetInstance与 @TargetMethodOrField中.如果想获得执行时间,必须把Where定义成AFTER
打印this,参数与返回值
import com.sun.btrace.AnyType;
@OnMethod(clazz = "java.io.File", method = "createTempFile", location = @Location(value = Kind.RETURN))
public static void o(@Self Object self, String prefix, String suffix, @Return AnyType result)
Self:
如果是静态函数,self为空 , 用@Self可以表示当前方法,也就是this
参数:
参数列表要么不要定义, 要定义就要定义完整,否则BTrace无法处理不同参数的同名函数
结果:
结果的类型用AnyType来定义,特别是用正则表达式匹配多个函数的时候,连void都可以表示
一点经验
-
用于匹配方法入参或返回类型时,因嫌麻烦不想引入外部依赖(一般也没有必要),外部类型请用AnyType代替,而不是Object!因为你可能用Object来准确匹配方法返回参数或返回类型。
-
启动跟踪脚本时,请使用和启动Java进程相同的Linux账号,不然会因为权限问题而attach失败。
-
另一个和BTrace类似的Java诊断工具greys-anatomy,由阿里释出,感兴趣的也可以学习一下。
-
若报错"Port 2020 unavailable.",则使用
btrace -p 2021 ...
来指定其它端口。 -
Linux下已经有个命令也叫btrace,注意别用混了。
-
由于Btrace会把脚本逻辑直接侵入到运行的代码中,所以在使用上做很多限制:
1、不能创建对象
2、不能使用数组
3、不能抛出或捕获异常
4、不能使用循环
5、不能使用synchronized关键字
6、属性和方法必须使用static修饰根据官方声明,不恰当的使用BTrace可能导致JVM崩溃,如在BTrace脚本使用错误的class文件,所以在上生产环境之前,务必在本地充分的验证脚本的正确性。