BTrace 是什么
BTrace 是檢查和解決線上的問(wèn)題的殺器贷帮,BTrace 可以通過(guò)編寫(xiě)腳本的方式,獲取程序執(zhí)行過(guò)程中的一切信息柒瓣,并且儒搭,注意了吭狡,不用重啟服務(wù)玫氢,是的逆趣,不用重啟服務(wù)汉操。寫(xiě)好腳本昧谊,直接用命令執(zhí)行即可创倔,不用動(dòng)原程序的代碼涯冠。
原理
總體來(lái)說(shuō)磁滚,BTrace 是基于動(dòng)態(tài)字節(jié)碼修改技術(shù)(Hotswap)來(lái)實(shí)現(xiàn)運(yùn)行時(shí) java 程序的跟蹤和替換拣挪。大體的原理可以用下面的公式描述:
Client(Java compile api + attach api) + Agent(腳本解析引擎 + ASM + JDK6 Instumentation) + Socket
其實(shí) BTrace 就是使用了 java attach api 附加 agent.jar 擦酌,然后使用腳本解析引擎+asm來(lái)重寫(xiě)指定類(lèi)的字節(jié)碼,再使用 instrument 實(shí)現(xiàn)對(duì)原有類(lèi)的替換菠劝。
安裝和配置
本次安裝和配置在 Linux Ubuntu 14.04 下進(jìn)行赊舶。目前 BTrace 的最新版本為 1.3.9,代碼托管在 [github] 上赶诊。
第一步笼平,在github 上下載 releases 版 btrace-bin-1.3.9.tgz,zip 版的沒(méi)有 build 目錄舔痪。
第二步寓调,解壓 btrace-bin-1.3.9.tgz 到一個(gè)目錄即可,例如 /home/fengzheng/soft/btrace
, 到這一步其實(shí)就可以用了锄码,只是執(zhí)行腳本的時(shí)候需要在 btrace 命令前加上絕對(duì)路徑夺英,如果想在任意目錄可執(zhí)行晌涕,進(jìn)行下一步
第三步,配置環(huán)境變量秋麸,配置的環(huán)境變量包括 JAVA_HOME
和 BTRACE_HOME
渐排,例如我的配置如下:
export JAVA_HOME=/home/fengzheng/soft/jdk1.8.0_111
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
export BTRACE_HOME=/home/fengzheng/soft/btrace
export PATH=$PATH:$BTRACE_HOME/bin
之后執(zhí)行命令 source /etc/profile
,使環(huán)境變量立即生效灸蟆。接下來(lái)在任意目錄執(zhí)行 btrace
命令驯耻,都可以執(zhí)行成功了。
簡(jiǎn)單測(cè)試用例
btrace 最簡(jiǎn)單的語(yǔ)法是 btrace $pid script.java
炒考,所以需要知道要探測(cè)的 Java
程序的進(jìn)程id可缚,然后編寫(xiě)一個(gè)探測(cè)腳本即可。
- 寫(xiě)一個(gè)常駐內(nèi)存的
Java
程序斋枢,這里寫(xiě)了一個(gè)無(wú)限循環(huán)帘靡,每隔5秒鐘輸出一組計(jì)算結(jié)果,內(nèi)容如下:
package kite.lab.utils;
/**
* NumberUtil
*
* @author fengzheng
* @date 2017/2/15
*/
public class NumberUtil {
public int sum(){
int result = 0;
for(int i = 0; i< 100; i++){
result += i * i;
}
return result;
}
public static void main(String[] args){
while (true) {
Thread.currentThread().setName("計(jì)算");
NumberUtil util = new NumberUtil();
int result = util.sum();
System.out.println(result);
try {
Thread.sleep(5000);
}catch (InterruptedException e){
}
}
}
}
順便說(shuō)一下命令行編譯和運(yùn)行 Java 的過(guò)程:
編譯:javac -d . NumberUtil.java
瓤帚,定位到 NumberUtil.java 所在目錄描姚,然后執(zhí)行此命令行,將會(huì)在當(dāng)前目錄(.表示當(dāng)前目錄)生成包名所示的目錄結(jié)構(gòu)戈次,kite/lab/utils/NumberUtil.class
執(zhí)行:java kite.lab.utils.NumberUtil
即可
- 執(zhí)行上面的程序后轩勘,可用
jps
命令查看 pid(一般情況下用哪個(gè)賬號(hào)啟動(dòng)的程序,就要用哪個(gè)賬號(hào)執(zhí)行 jps 怯邪,root 賬號(hào)除外)绊寻,執(zhí)行 jps 命令看到如下結(jié)果:
root@ubuntu:/home/fengzheng/codes/btrace# jps
10906 Jps
10860 NumberUtil
可以看到剛剛執(zhí)行的 java 進(jìn)程為 10860
編寫(xiě) btrace 腳本,腳本內(nèi)容簡(jiǎn)單如下:
package kite;
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.Strings.strcat;
import static com.sun.btrace.BTraceUtils.jstack;
import static com.sun.btrace.BTraceUtils.println;
import static com.sun.btrace.BTraceUtils.str;
/**
* NumberUtilBTrace
*
* @author fengzheng
* @date 2017/6/20
*/
@BTrace
public class NumberUtilBTrace {
@OnMethod(
clazz="kite.lab.utils.NumberUtil",
method="sum",
location=@Location(Kind.RETURN)
)
public static void func(@Return int result) {
println("trace: =======================");
println(strcat("result:", str(result)));
jstack();
}
}
意思是在執(zhí)行結(jié)束后(location=@Location(Kind.RETURN) 表示執(zhí)行結(jié)束)輸出結(jié)果和堆棧信息
預(yù)編譯:執(zhí)行之前可以用預(yù)編譯命令檢查腳本的正確性悬秉,預(yù)編譯命令為 btracec澄步,它是一個(gè) javac-like 命令,btracec NumberUtilBTrace.java
調(diào)用命令行執(zhí)行和泌,btrace 10860 NumberUtilBTrace.java 村缸,(如果要保存到本地文件中,可以使用轉(zhuǎn)向命令 btrace 10860 NumberUtilBTrace.java > mylog.log )打印的信息如下:
trace: =======================
result:328350
kite.lab.utils.NumberUtil.sum(NumberUtil.java:16)
kite.lab.utils.NumberUtil.main(NumberUtil.java:27)
- 按ctrl + c 允跑,會(huì)給出退出提示王凑,再按 1 退出
使用場(chǎng)景
BTrace 是一個(gè)事后工具,所謂事后工具就是在服務(wù)已經(jīng)上線了聋丝,但是發(fā)現(xiàn)存在以下問(wèn)題的時(shí)候,可以用 BTrace工碾。
- 比如哪些方法執(zhí)行太慢弱睦,例如監(jiān)控執(zhí)行時(shí)間超過(guò)1s的方法
- 查看哪些方法調(diào)用了 System.gc() ,調(diào)用棧是怎樣的
- 查看方法參數(shù)或?qū)ο髮傩?/li>
- 哪些方法發(fā)生了異常
多說(shuō)一點(diǎn)渊额,為了更好解決問(wèn)題况木,最好還要配合事前準(zhǔn)備和進(jìn)行中監(jiān)控垒拢,事前準(zhǔn)備就是埋點(diǎn)嘛,在一些可能出現(xiàn)問(wèn)題的方法中進(jìn)行日志輸出火惊,進(jìn)行中監(jiān)控就是利用一些實(shí)時(shí)監(jiān)控工具求类,例如 VisualVM 、jmc 這些帶界面的工具或者 jdk 提供的命令行工具等屹耐,再高級(jí)一點(diǎn)的就是利用 Graphite 這樣的Metrics 工具配合 web 界面展示出來(lái)尸疆。
使用限制
為了保證trace語(yǔ)句只讀,最小化對(duì)被檢測(cè)程序造成影響, BTrace對(duì)trace腳本有一些限制(比如不能改變被trace代碼中的狀態(tài))
- BTrace class不能新建類(lèi), 新建數(shù)組, 拋異常, 捕獲異常,
- 不能調(diào)用實(shí)例方法以及靜態(tài)方法(com.sun.btrace.BTraceUtils除外)
- 不能將目標(biāo)程序和對(duì)象賦值給BTrace的實(shí)例和靜態(tài)field
- 不能定義外部, 內(nèi)部, 匿名, 本地類(lèi)
- 不能有同步塊和方法
- 不能有循環(huán)
- 不能實(shí)現(xiàn)接口, 不能擴(kuò)展類(lèi)
- 不能使用assert語(yǔ)句, 不能使用class字面值
攔截方法定義
@OnMethod 可以指定 clazz 惶岭、method寿弱、location。由此組成了在什么時(shí)機(jī)(location 決定)監(jiān)控某個(gè)類(lèi)/某些類(lèi)(clazz 決定)下的某個(gè)方法/某些方法(method 決定)按灶。
如何定位
-
精準(zhǔn)定位
直接定位到一個(gè)類(lèi)下的一個(gè)方法症革,上面測(cè)試用的例子就是
-
正則表達(dá)式定位
正則表達(dá)式在兩個(gè)"/" 之間,例如下面的例子,監(jiān)控 javax.swing 包下的所有方法鸯旁,注意正式環(huán)境中噪矛,范圍盡可能小一點(diǎn),太大了性能會(huì)有影響铺罢。
@OnMethod(clazz="/javax\\.swing\\..*/", method="/.*/") public static void swingMethods( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod) { print("entered " + probeClass + "." + probeMethod); }
通過(guò)在攔截函數(shù)的定義里注入@ProbeClassName String probeClass, @ProbeMethodName String probeMethod 參數(shù)艇挨,告訴腳本實(shí)際匹配到的類(lèi)和方法名。
-
按接口或繼承類(lèi)定位
例如要匹配繼承或?qū)崿F(xiàn)了 com.kite.base 的接口或基類(lèi)的畏铆,只要在類(lèi)前加上 + 號(hào)就可以了雷袋,例如
@OnMethod(clazz="+com.kite.base", method="doSome")
-
按注解定位
在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")
攔截時(shí)機(jī)
攔截時(shí)機(jī)由 location 決定辞居,當(dāng)然也可為同一個(gè)定位加入多個(gè)攔截時(shí)機(jī)楷怒,即可以在進(jìn)入方法時(shí)攔截、方法返回時(shí)攔截瓦灶、拋出異常時(shí)攔截
-
Kind.Entry與Kind.Return
分別表示函數(shù)的開(kāi)始和返回鸠删,不寫(xiě) location 的情況下,默認(rèn)為 Kind.Entry,僅獲取參數(shù)值贼陶,可以用 Kind.Entry 刃泡,要獲取返回值或執(zhí)行時(shí)間就要用 Kind.Return
-
Kind.Error, Kind.Throw和 Kind.Catch
表示異常被 throw 、異常被捕獲還有異常發(fā)生但是沒(méi)有被捕獲的情況碉怔,在攔截函數(shù)的參數(shù)定義里注入一個(gè)Throwable的參數(shù)烘贴,代表異常
@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location =@Location(Kind.ERROR)) public static void onBind(Throwable exception, @Duration long duration){ }
-
Kind.Call 和 Kind.Line
Kind.Call 表示被監(jiān)控的方法調(diào)用了哪些其他方法,例如:
@OnMethod(clazz = "com.kite", method = "login", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER)) public static void onBind(@Self Object self, @TargetInstance Object instance, @TargetMethodOrField String method, @Duration long duration){ println(strcat("self: ", str(self))); println(strcat("instance: ", str(instance))); println(strcat("method: ", str(method))); println(strcat("duration(ms): ", str(duration / 1000000))); }
@Self 表示當(dāng)前監(jiān)控的函數(shù)所在類(lèi)撮胧,如果是靜態(tài)類(lèi)則為空桨踪,@TargetInstance 表示函數(shù)中調(diào)用的方法或?qū)傩运诘念?lèi),如果是靜態(tài)方法則為空芹啥,@TargetMethodOrField 表示調(diào)用的方法或?qū)傩远屠耄绻@取執(zhí)行時(shí)間铺峭,那么 where 必須設(shè)置為 Where.AFTER
Kind.Line 監(jiān)測(cè)類(lèi)是否執(zhí)行到了設(shè)置的行數(shù),例如:
@OnMethod(clazz = "com.kite.demo", location = @Location(value = Kind.LINE, line = 20)) public static void onBind() { println("執(zhí)行到第20行"); }
幾個(gè)例子
查看誰(shuí)調(diào)用了GC
@OnMethod(clazz = "java.lang.System", method = "gc")
public static void onSystemGC() {
println("entered System.gc()");
jstack();
}
打印耗時(shí)超過(guò)100ms的方法
@OnMethod(clazz = "/com\\.kite\\.controller\\..*/",method = "/.*/",location = @Location(Kind.RETURN))
public static void slowQuery(@ProbeClassName String pcn,@ProbeMethodName String probeMethod, @Duration long duration){
if(duration > 1000000 * 100){
println(strcat("類(lèi):", pcn));
println(strcat("方法:", probeMethod));
println(strcat("時(shí)長(zhǎng):", str(duration / 1000000)));
}
}
BTrace 提供了一系列的 sample, 可到 github 上查看汽纠。
注意問(wèn)題
如果出現(xiàn) Unable to open socket file: target process not responding or HotSpot VM not loaded
這個(gè)問(wèn)題卫键,可能的原因是執(zhí)行 BTrace 腳本的用戶(hù)和 Java 進(jìn)程運(yùn)行的用戶(hù)不是同一個(gè),使用 ps -aux | grep $pid
查看一下 Java 進(jìn)程的執(zhí)行用戶(hù)虱朵,保證和 BTrace 腳本執(zhí)行用戶(hù)相同即可莉炉。
歡迎關(guān)注微信公眾號(hào):gushidefengzheng