BTrace是Java的安全可靠的动态跟踪工具。 他的工作原理是通过 instrument + asm 来对正在运行的java程序中的class类进行动态增强。
说他是安全可靠的,是因为它对正在运行的程序是只读的。也就是说,他可以插入跟踪语句来检测和分析运行中的程序,不允许对其进行修改。因此他存在一些限制:
- 不能创建对象
- 不能创建数组
- 不能抛出和捕获异常
- 不能调用任何对象方法和静态方法
- 不能给目标程序中的类静态属性和对象的属性进行赋值
- 不能有外部、内部和嵌套类
- 不能有同步块和同步方法
- 不能有循环(for, while, do..while)
- 不能继承任何的类
- 不能实现接口
- 不能包含assert断言语句
这些限制其实是可以使用unsafe模式绕过。通过声明 *@BTrace(unsafe = true)
annotation 并且以unsafe 模式-u运行btrace
实际使用非安全模式跟踪时,发现一个问题,一个进程如果被安全模式btrace探测过一次, 后面再使用非安全模式进行探测时非安全模式不生效。
安装并使用
- 下载Btrace并解压
- 定义环境变量BTRACE_HOME为解压目录
- bin目录加入到path
运行BTrace
btrace <pid> <btrace-script>
- pid 为java进程号,可以使用jps来查询
- btrace-script 为 btrace脚本
也可以先使用btracec对btrace脚本继续预先编译再进行执行。
visualvm-plugin
btrace还提供了一个vaisualvm上的一个插件,可以执行btrace脚本。尝试了下,可以attach到本机的jvm进程上,但是远程主机的JVM进程不行。晚上有的说通过端口转发绑定的方式可以,但是还是没有试出来。
安装
运行jvisualvm.exe, 选择工具->插件->可用插件 选择 BTrace Workbench进行在线安装。
在线不能安装的,也可以通过手动安装。先从java visualvm 插件中心找到相应版本的插件更新文件地址进行下载,从更新文件中获取所需要的插件包的nbm下载地址进行下载。最后在 插件->已下载tab页->添加插件。
执行btrace脚本
选择需要监控的进程,右击 trace application
在btrace的工作台中直接编写脚本并执行
BTrace 脚本
先来看简单的BTrace脚本的使用,用于跟踪方式执行时间
@BTrace
public class TimeLogger {
@TLS private static long startTime = 0;
@OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get")
public static void startExecute(){
startTime = timeNanos();
}
@OnMethod(clazz="org.springframework.data.redis.core.DefaultValueOperations", method="get",
location=@Location(Kind.RETURN)
)
public static void endExecute(@Duration long duration){
long time = timeNanos() - startTime;
println(strcat("execute time(nanos): ", str(time)));
}
}
- @BTrace 声明了这个类是BTrace脚本
- @OnMethod 声明了关注点,必须声明在公有的静态方法上public static void
- 静态方法为 在关注点上执行的跟踪动作
跟踪方式执行时间程序的逻辑大致是: 在org.springframework.data.redis.core.DefaultValueOperations 对象执行get方法时,先记录一下当前时间,在get方法return时获取当前时间,从而获得方法执行所消耗的时间。值得注意的是,@TLS声明的变量是 ThreadLocal的, 每个线程都会有一份这个自己的startTime 变量。
执行以上的TimeLogger来看一下:
$ btrace 13778 TimeLogger.java
execute time(nanos): 214692000
execute time(nanos): 1215000
execute time(nanos): 3210000
执行后,当被监控的程序运行了这些检查点的方法时,btrace会在控制台对执行时间进行输出。通过重定向符>也可以将输出重定向到文件.
API
@BTrace
声明这个类是个BTrace脚本.unsafe参数表示是否不安全的模式执行.
方法annotation
- @OnMethod: 声明 探查点(probe point)
- clazz: 全路径类名,支持正则表达式,格式为/正则表达式/
- +类名 匹配子类
- @前缀 匹配anotation声明的类
- method: 方法名,支持正则表达式,格式为/正则表达式/, anotation使用@
- location: 用@Location来表明在什么时候时候去执行脚本
- Kind.ENTRY 进入方法时
- Kind.RETURN 方法返回时
- Kind.THROW 抛出异常时
- Kind.ARRAY_SET 设置数组元素时
- Kind.ARRAY_GET 获取数组元素时
- Kind.NEWARRAY 创建新数组时
- Kind.NEW 创建新对象时
- Kind.CALL 调用方法时
- Kind.CATCH 捕获异常时
- Kind.FIELD_SET 获取对象属性时
- Kind.FIELD_SET 设置对象属性时
- Kind.ERROR 方法由于发生未被捕获的异常结束时
- Kind.SYNC_ENTRY 进入同步块时
- Kind.SYNC_EXIT 离开同步块时
- type 方法类型, 不含方法名、参数民、异常声明。
- @OnTimer 定时器,间隔出发动作。
- 参数: 间隔时间,单位毫秒
- @OnError BTrace代码发生异常时回调
- @OnExit BTrace代码调用exit来结束探测时回调该注释的方法
- @OnEvent 接受客户端事件时会回调。目前,当客户端命令上执行Ctrl-C (SIGINT)时会发送一个时间到服务器端,从而触发@OnEvent注释的方法。
- @OnLowMemory 内存低于设置的阈值时回调方法
- pool 内存池名称
- threshold 阈值大小
- @OnProbe 支持使用xml格式来声明探测点点和探测动作。
未声明注解的方法参数
未声明注解的方法参数的映射,根据探测点类型locaiton的不同而不同:
- Kind.ENTRY 方法参数
- Kind.RETURN 方法返回值
- Kind.THROW 被抛出的异常
- Kind.ARRAY_SET 数值下标
- Kind.ARRAY_GET 数组下标
- Kind.CATCH 被捕获的异常
- Kind.FIELD_SET 被设置属性的值
- Kind.NEW 创建的对象的类型
- Kind.ERROR 被抛出的异常
字段注解
- Export 将字段保罗给jstat访问
- Property 将字段暴露注册为MBean 属性,可以通过JMX进行查看
- TLS 将字段声明为TheadLocal字段,每个线程拥有自己独立的字段
参数annotation介绍
- @Self 声明探测的当前对象this
- @Return 方法返回对象
- @ProbeClassName 当前探测点所在的类名
- @ProbeMethodName 当前探测点所在的方法名
- fqn 是否获取全路径方法名称fully qualified name (FQN)
- @Duration 执行时间,单位纳秒,一般同 Kind.RETURN 和 Kind.ERROR 配合使用
- @TargetInstance 配合 Kind.CALL使用,声明了被调用方法所在的对象
- @TargetMethodOrField Kind.CALL使用,声明了被调用方法所在的对象的方法
BTrace原理分析
BTrace 主要使用了 Instrumentation + ASM技术来实现对正在运行进程的探测。
基本实现逻辑
虚拟机其实提供了一个hook,那就是Instrumentation,可以将独立于应用程序的代理程序agent程序随着着应用程序一起启动或者attach挂载到正在运行中的应用程序。而在代理程序中可以对class进行修改或者重新定义。
btrace的agent程序在btrace-agent.jar,通过挂在vm = VirtualMachine.attach(pid); vm.loadAgent 进行挂载到正在运行中的java进程。 源码如下:
btrace-client.jar/com.sun.btrace.client.Main
/**
* Attach the BTrace client to the given Java process.
* Loads BTrace agent on the target process if not loaded
* already.
*/
public void attach(String pid, String sysCp, String bootCp) throws IOException {
String agentPath = "/btrace-agent.jar";
String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
tmp = tmp.substring(0, tmp.indexOf("!"));
tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
agentPath = tmp + agentPath;
agentPath = new File(new URI(agentPath)).getAbsolutePath();
attach(pid, agentPath, sysCp, bootCp);
}
/**
* Attach the BTrace client to the given Java process.
* Loads BTrace agent on the target process if not loaded
* already. Accepts the full path of the btrace agent jar.
* Also, accepts system classpath and boot classpath optionally.
*/
public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
try {
VirtualMachine vm = null;
byte[] code = client.compile(fileName, classPath, includePath); // 编译btrace脚本
vm = VirtualMachine.attach(pid);
// 此处 省略了诺干代码..
vm.loadAgent(agentPath, agentArgs); // 挂在 agent 包后,会运行 com.sun.btrace.agent.Main.main 开启server socket后台线程,监听客户端的连接
client.submit(fileName, code, btraceArgs, createCommandListener(client)); // 将btrace脚本编译后的字节码提交给 正在运行的agent的程序
}
btrace-agent.jar/com.sun.btrace.agent.Main
Thread agentThread = new Thread(new Runnable() {
@Override
public void run() {
BTraceRuntime.enter();
try {
startServer();
} finally {
BTraceRuntime.leave();
}
}
});
BTraceRuntime.initUnsafe();
BTraceRuntime.enter();
try {
agentThread.setDaemon(true);
if (isDebug()) debugPrint("starting agent thread");
agentThread.start();
} finally {
BTraceRuntime.leave();
}
public static final int BTRACE_DEFAULT_PORT = 2020;
//-- Internals only below this point
private static void startServer() {
int port = BTRACE_DEFAULT_PORT;
String p = argMap.get("port");
if (p != null) {
try {
port = Integer.parseInt(p);
} catch (NumberFormatException exp) {
error("invalid port assuming default..");
}
}
ServerSocket ss;
try {
if (isDebug()) debugPrint("starting server at " + port);
System.setProperty("btrace.port", String.valueOf(port));
if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
System.setProperty("btrace.output", scriptOutputFile);
}
ss = new ServerSocket(port);
} catch (IOException ioexp) {
ioexp.printStackTrace();
return;
}
while (true) {
try {
if (isDebug()) debugPrint("waiting for clients");
Socket sock = ss.accept();
if (isDebug()) debugPrint("client accepted " + sock);
Client client = new RemoteClient(inst, sock);
handleNewClient(client).get();
} catch (RuntimeException | IOException | ExecutionException re) {
if (isDebug()) debugPrint(re);
} catch (InterruptedException e) {
return;
}
}
}
Instrumentation.ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理
Instrumentation.retransformClasses 对于已经加载的类触发重新加载类定义, 进行ClassFileTransformer转换处理
com.sun.btrace.agent.Main 在会启动一个socket后台线程与btrace client 进行交互. 客户端会将BTRACE 程序的提交到agent去,再通过字节码操作对class进行操作。
在被探测点上增加探测点动作是通过 在Instrumentation代理程序中通过修改字节码来谈价探测点动作。