引言
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 为java进程号,可以使用jps来查询
·btrace-script 为 btrace脚本
也可以先使用btracec对btrace脚本继续预先编译再进行执行。
visualvm-plugin
btrace还提供了一个vaisualvm上的一个插件,可以执行btrace脚本。尝试了下,可以attach到本机的jvm进程上,但是远程主机的JVM进程不行。晚上有的说通过端口转发绑定的方式可以,但是还是没有试出来。
安装
运行jvisualvm.exe, 选择工具->插件->可用插件 选择 BTrace Workbench进行在线安装。
jvisualvm_btrace.jpg
在线不能安装的,也可以通过手动安装。先从java visualvm 插件中心找到相应版本的插件更新文件地址进行下载,从更新文件中获取所需要的插件包的nbm下载地址进行下载。最后在 插件->已下载tab页->添加插件。
执行btrace脚本
选择需要监控的进程,右击 trace application
jvisualvm_btrace2.png
在btrace的工作台中直接编写脚本并执行
jvisualvm_btrace3.png
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会在控制台对执行时间进行输出。通过重定向符>也可以将输出重定向到文件.
那么,BTrace这么神奇的功能是如何实现的呢?既然这是个开源的代码,那么直接从代码找原理。https://github.com/btraceio/btrace。
总体来说,BTrace是基于动态字节码修改技术(Hotswap)来实现运行时java程序的跟踪和替换。大体的原理可以用下面的公式描述:
v
Client(Java compile api + attach api) + Agent(脚本解析引擎 + ASM + JDK6 Instumentation) + Socket
BTrace的入口类在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/client/Main.java中。在其main方法中,可以看到起最终的核心逻辑是在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/client/Client.java中。方法调用如下:
client.compile
client.attach
client.submit
Client
首先是client.compile方法,使用的是Java compile api,将我们传递的java源文件编译为.class文件,当然你如果使用btracec提前编译了源代码,那么这里就不会有这一步。
针对官方脚本的一个例子:
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class HelloWorld {
@OnMethod(
clazz="java.lang.Thread",
method="start"
)
public static void func() {
println("about to start a thread!");
}
}
@OnMethod告诉Btrace解析引擎需要代理的类和方法。 这个例子的作用是当java.lang.Thread类的任意一个对象调用 start 方法后,会调用func方法。
client端在编译完脚本之后,进行了一次字节码修改,但是仅仅是做了一些兼容性,例如域访问控制器、简写等。
接着client.attach中使用java的attach api将agent动态attach到目标jvm进程中(ava agent,通常有两种方式添加到jvm进程中:动态attach;在目标jvm启动之前添加agent参数)。
VirtualMachine vm = VirtualMachine.attach(pid);
...
vm.loadAgent(agentPath, agentArgs);
最后client的submit方法,会向agent发送监控命令以及传递对应code的字节码。
Agent
BTrace的agent实现类就在https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/agent/Main.java中,具体的实现可以看其main方法,此agent的premain和agentmain方法都是调用了这个方法。这里需要注意的一点:必须要上jdk6,因为jdk5虽然已经有了instrument api,但是其仅仅支持premain方法,也就是仅仅支持在main方法运行之前执行一些动作,而jdk6后加入了agentmain方法和VirtualMachine,是可以在main方法运行后执行的(如果是通过命令行启动的,那么agentmain方法不会被调用)。此外,在jdk6之前,程序启动之后是无法再设置boot class加载路径和system class加载路径的。而jdk6之后,instrument新增的appendToBootstrapClassLoaderSearch和appendToSystemClassLoaderSearch是可以动态添加classpath的。
agent被提交到目标jvm进程后,首先会添加boot classpath.
...
inst.appendToBootstrapClassLoaderSearch(jf);
...
inst.appendToSystemClassLoaderSearch(jf);
接着开启一个serversocket等待client的连接。之后client和agent之间的数据通讯,比如生成.class发送到agent,agent将线上程序打印的数据回传给 client都是通过socket来进行的。当agent接收到监控命令后,主要有以下两部分工作:
重写类:遍历当前所有的class,根据正则找到匹配的类,用asm重写
替换类:替换掉原来的class
agent接受到client发来的监控指令以及对应的参数后,会load所有的class,根据正则去匹配指定的类和方法,并使用脚本解析引擎去处理发送过来的字节码然后使用ASM将脚本里标注的类java.lang.Thread的字节码重写,植入跟踪代码或新的逻辑。在上面那个例子中,Java.lang.Thread这个类的字节码被重写并在start方法体尾部植入了func方法的调用。
BTrace的agent利用instrumentation的retransformClasses方法将原始字节码替换掉,使用的transfomer见https://github.com/btraceio/btrace/blob/master/src/share/classes/com/sun/btrace/runtime/BTraceTransformer.java。如下:
new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String className, Class c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
// BTrace解析脚本,利用asm重写bytecode,然后classLoader加载
}
}, true);
其中,在agent的agentmain中通过handleNewClient方法启动一个异步线程进行class transformer,而在这个异步线程中最终是通过调用
2.怎样才能方便快捷无误地写出BTrace脚本呢2.1使用VisualVm来写如果你使用VisualVm的话,那么也可以使用VisualVm来编写BTrace脚本。不过需要你安装btrace的插件。在VisualVm中安装插件异常的爽,只需要在插件列表中点击一下就可以了。如下图(由于我已经安装过了,所以不再【可用插件】列表中,在【已安装】列表中)
Btrace插件列表.png
BTrac已安装列表.png
安装好BTrace插件之后就可以使用VIsualVm来打开一个java应用程序了。如下打开BTrace脚本的编写界面
打开Btrace插件界面.png
Btrace编写脚本界面.png
在这里面编写BTrace脚本其实和在记事本上编写脚本差不多。哪为啥要在这写呢?
添加classpath比较方便
将脚本绑定到应用程序方便
总结
以 上就是我对Java开发大型互联网架构深入解析BTrace 介绍及原理实践 问题及其优化总结,分享给大家,觉得收获的话可以点个关注收藏转发一波喔,谢谢大佬们支持!
最后,每一位读到这里的网友,感谢你们能耐心地看完。希望在成为一名更优秀的Java程序员的道路上,我们可以一起学习、一起进步!都能赢取白富美,走向架构师的人生巅峰!
进阶地址:https://ke.qq.com/course/230866?flowToken=1000327
想了解学习Java方面的技术内容以及Java技术视频的内容可加群:722040762 验证码:(666 必过)欢迎大家的加入哟!