簡介
在生產環境中經常遇到格式各樣的問題,如OOM或者莫名其妙的進程死掉。一般情況下是通過修改程序,添加打印日誌;然後重新發布程序來完成。然而,這不僅麻煩,而且帶來很多不可控的因素。有沒有一種方式,在不修改原有運行程序的情況下獲取運行時的數據信息呢?如方法參數、返回值、全局變量、堆棧信息等。Btrace就是這樣一個工具,它可以在不修改原有代碼的情況下動態地追蹤java運行程序,通過hotswap技術,動態將跟蹤字節碼注入到運行類中,進行監控甚至到達修改程序的目的
1. 下載
https://github.com/btraceio/btrace/releases/latest
2. 安裝
配置環境變量
3. 本地jar包依賴
本地寫腳本的準備
Btrace是腳本,不過也是.java的格式,寫腳本前建議引入以下幾個jar包,方便於代碼編寫的提示
jar包引用: 安裝目錄下build中有三個jar包
btrace-agent.jar
btrace-boot.jar
btrace-client.jar
三個核心jar直接拷貝到工程中臨時使用即可
<dependency>
<groupId>com.sun.tools.btrace</groupId>
<artifactId>btrace-agent</artifactId>
<version>1.3.11</version>
<scope>system</scope>
<systemPath>D:/programmesoft/btrace/build/btrace-agent.jar</systemPath>
<dependency>
<dependency>
<groupId>com.sun.tools.btrace</groupId>
<artifactId>btrace-boot</artifactId>
<version>1.3.11</version>
<scope>system</scope>
<systemPath>D:/programmesoft/btrace/build/btrace-boot.jar</systemPath>
</dependency>
<dependency>
<groupId>com.sun.tools.btrace</groupId>
<artifactId>btrace-client</artifactId>
<version>1.3.11</version>
<scope>system</scope>
<systemPath>D:/programmesoft/btrace/build/btrace-client.jar</systemPath>
</dependency>
4. 簡單入門
4.1 待測試代碼如下
FirstSample
該程序有三個方法,main, fun1, fun2
main方法一個while循環不斷的調用fun1方法,傳入了兩個字符串"aa",“bb”,fun1調用fun2方法,傳入一個字符串"bb",然後休眠3秒鐘,fun2方法會返回一個"fun2"+傳入的參數
import java.util.concurrent.TimeUnit;
public class FirstSample {
public static void main(String[] args) {
FirstSample firstSample = new FirstSample();
while (true){
firstSample.fun1("aa","bb");
}
}
public void fun1(String str1,String str2){
try {
String result = fun2(str2);
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private String fun2(String str2) {
System.out.println("fun2 參數爲:"+str2);
return "fun2"+str2;
}
}
4.2 Btrace入門案例
假如我們想要知道每次調用fun1都是一些什麼參數,我們得腳本如下
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
@BTrace
public class FirstSampleBtrace {
@OnMethod(
clazz = "me.zks.jvm.troubleshoot.btrace.FirstSample"
,method = "fun1"
,location = @Location(value = Kind.ENTRY))
public static void m(@Self Object self,String str1,String str2){
BTraceUtils.print("fun1 str1:"+str1+" str2: "+str2+" ");
}
}
其中第4行@BTrace註解表示使用Btrace來監控
@OnMethod裏面定義了內容如下
- clazz = "me.zks.jvm.troubleshoot.btrace.FirstSample"定義了哪個對象
- method = "fun1"定義了哪個方法
- location = @Location(value = Kind.ENTRY))定義了攔截的時機,我這裏定義的是進入程序的時候進行攔截。更多攔截的請參考:
m方法: m取名是任意取得
裏面的參數 - @Self Object
- String str1, String str2 這個是傳入的參數,這個參數必須要和我們定義的方法參數一致,如果方法有重載,我們會根據這個參數的來判斷去攔截哪個方法
- BTraceUtils.print(“fun1 str1:”+str1+" str2: "+str2); BtraceUtil的輸出方法,用來輸出結果
將上面的FIrstSample運行起來
程序每三秒執行一次
fun1參數爲:aa,bb
fun2參數爲:bb
fun1參數爲:aa,bb
fun2參數爲:bb
fun1參數爲:aa,bb
fun2參數爲:bb
fun1參數爲:aa,bb
fun2參數爲:bb
...
用jps查看此程序的PID,發現PID爲5044
C:\Users\Administrator>jps
2404
2420 RemoteMavenServer
3540 Launcher
5044 FirstSample
3372 Jps
啓動腳本
btrace -v 5044 FirstSampleBtrace.java
其中
- -v 代表可以輸出到控制檯,我們方便控制檯查看
- 5044是PID
- FirstSampleBtrace.java是我們得腳本名
其中輸入-h 可以看使用方法
C:\Users\Administrator>btrace -h
Usage: btrace <options> <pid> <btrace source or .class file> <btrace arguments>
where possible options include:
--version Show the version
-v Run in verbose mode
-o <file> The path to store the probe output (will disable showing the output in console)
-u Run in trusted mode
-d <path> Dump the instrumented classes to the specified path
-pd <path> The search path for the probe XML descriptors
-classpath <path> Specify where to find user class files and annotation processors
-cp <path> Specify where to find user class files and annotation processors
-I <path> Specify where to find include files
-p <port> Specify port to which the btrace agent listens for clients
-statsd <host[:port]> Specify the statsd server, if any
C:\Users\Administrator>
顯示
DEBUG: loading D:\programmesoft\btrace\build\btrace-agent.jar
DEBUG: agent args: port=2020,statsd=,debug=true,bootClassPath=.,systemClassPath=
C:\Program Files\Java\jdk1.8.0_92\jre/../lib/tools.jar,probeDescPath=.
DEBUG: loaded D:\programmesoft\btrace\build\btrace-agent.jar
DEBUG: registering shutdown hook
DEBUG: registering signal handler for SIGINT
DEBUG: submitting the BTrace program
DEBUG: opening socket to 2020
DEBUG: setting up client settings
DEBUG: sending instrument command: []
DEBUG: entering into command loop
DEBUG: received com.sun.btrace.comm.OkayCommand@74a10858
DEBUG: received com.sun.btrace.comm.RetransformationStartNotification@23fe1d71
DEBUG: received com.sun.btrace.comm.OkayCommand@28ac3dc3
DEBUG: received com.sun.btrace.comm.MessageCommand@1d371b2d
fun1 str1:aa str2: bb DEBUG: received com.sun.btrace.comm.MessageCommand@543c6f6d
fun1 str1:aa str2: bb DEBUG: received com.sun.btrace.comm.MessageCommand@13eb8acf
fun1 str1:aa str2: bb DEBUG: received com.sun.btrace.comm.MessageCommand@51c8530f
fun1 str1:aa str2: bb DEBUG: received com.sun.btrace.comm.MessageCommand@7403c468
fun1 str1:aa str2: bb DEBUG: received com.sun.btrace.comm.MessageCommand@43738a82
fun1 str1:aa str2: bb
我們通過打印的輸出可以看到程序開啓了2020端口,fun1 str1:aa str2: bb 達到了我們監控的目的
4.3 Btrace案例2-返回時攔截得到執行的時間
假如我們想要知道每次調用fun2都調用了什麼參數,並且fun1使用了多長的時間,代碼如下
FirstSampleBtrace
import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;
@BTrace
public class FirstSampleBtrace {
// @OnMethod(
// clazz = "me.zks.jvm.troubleshoot.btrace.FirstSample"
// ,method = "fun1"
// ,location = @Location(value = Kind.ENTRY))
// public static void m(@Self Object self,String str1,String str2){//,String str2
// BTraceUtils.print("fun1 str1:"+str1+" str2: "+str2);
// }
@OnMethod(clazz = "me.zks.jvm.troubleshoot.btrace.FirstSample",
method = "fun2",
location = @Location(value = Kind.ENTRY))
public static void m1(@Self Object self,String str1){
BTraceUtils.print("fun2 str1:"+str1+" ");
}
@OnMethod(clazz = "me.zks.jvm.troubleshoot.btrace.FirstSample",
method = "fun1",
location = @Location(value = Kind.RETURN))
public static void m2(@Self Object self,String str1,String str2,@Return Void a,@Duration long time){
BTraceUtils.print("fun1 str1:"+str1+" str2:"+str2+" Duration is: "+time+" ");
}
}
運行後即可知道打印出攔截的時間
D:\git\jvmtroubleshoot\src\main\java\me\zks\jvm\troubleshoot\btrace>btrace -v 6392 FirstSampleBtrace.java
DEBUG: assuming default port 2020
DEBUG: assuming default classpath '.'
DEBUG: compiling FirstSampleBtrace.java
DEBUG: compiled FirstSampleBtrace.java
DEBUG: attaching to 6392
DEBUG: checking port availability: 2020
DEBUG: attached to 6392
DEBUG: loading D:\programmesoft\btrace\build\btrace-agent.jar
DEBUG: agent args: port=2020,statsd=,debug=true,bootClassPath=.,systemClassPath=C:\Program Files\Java\jdk1.8.0_92\jre/../lib/tools.jar,probeDescPat
DEBUG: loaded D:\programmesoft\btrace\build\btrace-agent.jar
DEBUG: registering shutdown hook
DEBUG: registering signal handler for SIGINT
DEBUG: submitting the BTrace program
DEBUG: opening socket to 2020
DEBUG: setting up client settings
DEBUG: sending instrument command: []
DEBUG: entering into command loop
DEBUG: received com.sun.btrace.comm.OkayCommand@fcd6521
DEBUG: received com.sun.btrace.comm.RetransformationStartNotification@27d415d9
DEBUG: received com.sun.btrace.comm.OkayCommand@5c18298f
DEBUG: received com.sun.btrace.comm.MessageCommand@5204062d
fun2 str1:bb DEBUG: received com.sun.btrace.comm.MessageCommand@4fcd19b3
fun1 str1:aa str2:bb Duration is: 3001048056 DEBUG: received com.sun.btrace.comm.MessageCommand@376b4233
fun2 str1:bb DEBUG: received com.sun.btrace.comm.MessageCommand@2fd66ad3
fun1 str1:aa str2:bb Duration is: 2999950327 DEBUG: received com.sun.btrace.comm.MessageCommand@5d11346a
fun2 str1:bb DEBUG: received com.sun.btrace.comm.MessageCommand@7a36aefa
4.4 Btrace退出方法
btrace退出時按ctrl+c 然後選擇1進行退出,然後確認Y
fun1 str1:aa str2:bb Duration is: 2999939243 Please enter your option:
1. exit
2. send an event
3. send a named event
4. flush console output
1
DEBUG: sending exit command
終止批處理操作嗎(Y/N)? y
發現Btrace的一個bug,版本1.3.09~1.3.11都有,return的時候,當ctrl+c 然後選擇1時會導致入侵的程序crash,具體報錯Btrace java.lang.NoSuchMethodError
Exception in thread "main" java.lang.NoSuchMethodError: me.zks.jvm.troubleshoot.btrace.FirstSample.$btrace$me$zks$jvm$troubleshoot$btrace$FirstSampleBtrace$m2(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Void;)V
at me.zks.jvm.troubleshoot.btrace.FirstSample.fun1(Unknown Source)
at me.zks.jvm.troubleshoot.btrace.FirstSample.main(Unknown Source)
這個應該是Btrace的一個bug,我已經向作者提了issues
原因可能是由於TimeUnit.SECONDS.sleep(3)和Thread.sleep(3)導致,我猜測對於線程執行sleep()或join()方法時,Btrace有bug導致
所以說,我們得平時學習的時候,用Object.wait的等待阻塞來測試,而不應用Thread.sleep來測試,並且注意,如果我們攔截的方法中有Thread.sleep,這幾個版本最好是做好jvm crash的打算,這點需要注意
Object o = new Object();
synchronized (o){
o.wait(1000);
}
以後的測試,我將會使用上面的方法進行停頓。
4.5 Btrace案例4 異常拋出和異常捕獲
Kind.ERROR
待測試代碼如下
代碼在不斷的調用divide方法,隨機傳一個數字進去, divide
package me.zks.jvm.troubleshoot.btrace;
import java.util.Random;
public class BtraceThrow {
public static void main(String[] args) {
Random random = new Random();
BtraceThrow btraceThrow = new BtraceThrow();
while (true){
int randomNum =random.nextInt(4);
try {
int result = btraceThrow.divide(randomNum);
System.out.println("result: "+result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public int divide(int a) throws Exception{
Object object = new Object();
//每次暫停1秒
synchronized (object){
object.wait(1000);
}
return 100/a;
}
}
btrace腳本如下
其中需要有一個@TargetInstance Throwable參數
@BTrace
public class BtraceThrowScript {
@OnMethod(
clazz = "me.zks.jvm.troubleshoot.btrace.BtraceThrow",
method = "divide",
location = @Location(Kind.ERROR)
)
public static void testThrow(@Self Object self, @TargetInstance Throwable err , int a){
BTraceUtils.print("a: ");
BTraceUtils.Threads.jstack(err);
}
}
監聽後輸出
DEBUG: received com.sun.btrace.comm.OkayCommand@3ecd23d9
DEBUG: received com.sun.btrace.comm.RetransformationStartNotification@569cfc36
DEBUG: received com.sun.btrace.comm.OkayCommand@43bd930a
DEBUG: received com.sun.btrace.comm.MessageCommand@553a3d88
a: DEBUG: received com.sun.btrace.comm.MessageCommand@7a30d1e6
java.lang.ArithmeticException: / by zero
DEBUG: received com.sun.btrace.comm.MessageCommand@5891e32e
me.zks.jvm.troubleshoot.btrace.BtraceThrow.divide(BtraceThrow.java:24)
me.zks.jvm.troubleshoot.btrace.BtraceThrow.main(BtraceThrow.java:10)
DEBUG: received com.sun.btrace.comm.MessageCommand@cb0ed20
a: DEBUG: received com.sun.btrace.comm.MessageCommand@8e24743
java.lang.ArithmeticException: / by zero
DEBUG: received com.sun.btrace.comm.MessageCommand@74a10858
me.zks.jvm.troubleshoot.btrace.BtraceThrow.divide(BtraceThrow.java:24)
me.zks.jvm.troubleshoot.btrace.BtraceThrow.main(BtraceThrow.java:10)
DEBUG: received com.sun.btrace.comm.MessageCommand@23fe1d71
a: DEBUG: received com.sun.btrace.comm.MessageCommand@28ac3dc3
java.lang.ArithmeticException: / by zero
DEBUG: received com.sun.btrace.comm.MessageCommand@32eebfca
me.zks.jvm.troubleshoot.btrace.BtraceThrow.divide(BtraceThrow.java:24)
me.zks.jvm.troubleshoot.btrace.BtraceThrow.main(BtraceThrow.java:10)
DEBUG: received com.sun.btrace.comm.MessageCommand@4e718207
a: DEBUG: received com.sun.btrace.comm.MessageCommand@1d371b2d
java.lang.ArithmeticException: / by zero
Please enter your option:
1. exit
2. send an event
3. send a named event
4. flush console output
DEBUG: received com.sun.btrace.comm.MessageCommand@543c6f6d
Kind.Cache Kind.Throw
catch感覺有bug了,Throw感覺用不上,一般用ERROR代替Throw即可,所以這裏不說了。具體讀者自己去嘗試。
未完待續。。。