Btrace 入门到熟练小工完全指南

3,144 阅读3分钟
原文链接: calvin1978.blogcn.com

BTrace是神器,每一个需要每天解决线上问题,但完全不用BTrace的Java工程师,都是可疑的。

BTrace的好处,是可以通过自己编写的脚本,随时获取应用的一切调用信息,而不像原来那样,不断的修改代码,加入System.out.println(), 然后重启,然后重启,然后重启!!!而且,特别严格的约束,保证自己的消耗特别小,只要定义脚本时不作大死,直接在生产环境打开也没太大影响。

在网上搜索BTrace能出来不少文章,都有点旧了,而且不够详细,于是决定,重新写一份。

码这么多的字好辛苦,请保留原文链接:calvin1978.blogcn.com/articles/bt…

1. 概述

1.1 快速开始

BTrace搬家了!! 已经搬离了Sun,搬到了github.com/btraceio/bt…,目前的版本已经是1.38。

在Release页面里下载最新Zip版,解压就能用,UserGuide和Samples也在里面。

先抄一个UserGuide里的例子:

查看图片

然后ps找出要监控的java应用的pid, ./btrace $pid HelloWorld.java 就跑起来了。

是不是很简单??基本上不用任何BTrace的知识,都能猜出HelloWorld会干啥。通过JVM Attach API,btrace把自己绑进了被监控的进程,按HelloWorld里的定义,进行了AOP式的代码植入。

最开心就是这里,如果还想监控其他内容,直接修改HelloWorld.java,再执行一次btrace就可以了,不需要重启应用!! 重启应用!!

1.2 典型的场景

1. 服务慢,能找出慢在哪一步,哪个函数里么?

2. 谁调用了System.gc(),调用栈如何?

3. 进入这个函数,这行代码,抛出这个异常时,出参入参,this的属性值是什么?

1.3 一些重要的事

在Btrace里最重要的规定,就是避免Btrace脚本的消耗过大影响真正业务,所以定义了一系列不允许的事情,比如不允许调用任何类的任何方法只能调用BTraceUtils 里的一系列方法,不允许For 循环等等,更多规定看User Guide。 当然,可以用-u 运行在unsafe mode来规避限制,但不推荐。

在以前的例子里,甚至还不能字符串相加,必须用strcat

println(strcat(strcat(probeClass, "."), probeMethod));

好在新版里已经可以写回:

println(probeClass + '.' + probeMethod);

另外,BTrace也不是完美,它植入过的代码,会一直在,直到应用重启为止。不过,只在BTrace attach状态时才会被执行。所以植入过的方法,虽然BTrace已退出了,每次执行时还是会多出一次是否Attach状态的判断。

最后,新版的BTrace并不在maven的中央仓库,只好自己加依赖的jar和sources文件。

1.4 其他命令行选项

1.4.1 定义classpath

如果在HelloWorld.java里使用了JDK外的其他类,比如Netty的:

./btrace -cp .:netty-all-4.0.41.Final.jar $pid HelloWorld.java

但上面定义的classpath只在编译脚本时使用,其实脚本里需要显式使用Netty类的机会很少(后面真正用到的时候会提起)。而在运行时,因为已经绑到目标应用的JVM里,用的是目标JVM的classpath。

1.4.2 结果输出到文件

./btrace -o mylog $pid HelloWorld.java

很坑新人的参数,首先,这个mylog会生成在应用的启动目录,而不是btrace的启动目录。其次,执行过一次-o之后,btrace的console output就被屏蔽了,再执行btrace不加-o 也不会再输出到console,直到应用重启为止。

所以有时也直接用转向了事:
./btrace $pid HelloWorld.java > mylog

1.4.3.预编译脚本

虽然btrace可以实时编译Java源文件,但如果你的脚本是要给运维同学执行的,线上运行时才发现写错了就尴尬了。此时可以用btracec命令预编译一下:

./btracec HelloWorld.java

2. 拦截方法定义

2.1 精准定位

就是HelloWorld的例子,精确定义要监控的类与方法。

2.2 正则表达式定位

可以用表达式,批量定义需要监控的类与方法。正则表达式需要写在两个 "/" 中间。

下例监控javax.swing下的所有类的所有方法....可能会非常慢,建议范围还是窄些。

查看图片

通过在拦截函数的定义里注入@ProbeClassName String probeClass, @ProbeMethodName String probeMethod 参数,告诉脚本实际匹配到的类和方法名。

另一个例子,监控Statement的executeUpdate(), executeQuery() 和 executeBatch() 三个方法,见jdbcQueries.java

2.3 按接口,父类,Annotation定位

比如我想匹配所有的Filter类,在接口或基类的名称前面,加个+ 就行
@OnMethod(clazz="+com.vip.demo.Filter", method="doFilter")

也可以按类或方法上的annotaiton匹配,前面加上@就行
@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")

2.4 其他

构造函数的名字是 " @OnMethod(clazz="java.net.ServerSocket", method="")

如果有多个同名的函数,想区分开来,可以在拦截函数上定义不同的参数列表(见后)。

3. 拦截时机

可以为同一个函数的不同的Location,分别定义多个拦截函数。

3.1 Kind.Entry与Kind.Return

@OnMethod( clazz="java.net.ServerSocket", method="bind" )
不写Location,默认就是刚进入函数的时候(Kind.ENTRY)。

但如果你想获得函数的返回结果或执行时间,则必须把切入点定在返回时。

查看图片

3.2 Kind.Error, Kind.Throw和 Kind.Catch

包括异常抛出(Throw),异常被捕获(Catch),异常没被捕获被抛出函数之外(Error),主要用于对某些异常情况的跟踪。

可以注入一个Throwable的参数,代表异常,Kind.Error也支持@Duration
查看图片

3.3 Kind.Call与Kind.Line

查看图片

这里定义监控bind()函数里面所注入到 instance 与 method中。
静态函数中,instance的值为空。
如果想获得执行时间,必须 把Where定义成AFTER。

注意这里,一定不要像下面这样大范围的匹配,否则这性能是神仙也没法救了:

查看图片

下例监控代码是否到达了Socket类的第363行。

查看图片

line还可以为-1,然后每行都会打印出来,加参数int line 获得的当前行数。此时会显示函数里完整的执行路径,但肯定又非常慢。
 

又到了文章砍半的时候,后续请看下篇

发现自己还是喜欢这些不漂亮的照片。

查看图片

查看图片