java線上程序排錯經驗4 -Btrace瞭解一下

簡介

在生產環境中經常遇到格式各樣的問題,如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即可,所以這裏不說了。具體讀者自己去嘗試。

未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章