BTrace : Java 線上問題排查神器


BTrace 是什么

BTrace 是檢查和解決線上的問題的殺器,BTrace 可以通過編寫腳本的方式,獲取程序執行過程中的一切信息,並且,注意了,不用重啟服務,是的,不用重啟服務。寫好腳本,直接用命令執行即可,不用動原程序的代碼。

原理

總體來說,BTrace 是基於動態字節碼修改技術(Hotswap)來實現運行時 java 程序的跟蹤和替換。大體的原理可以用下面的公式描述:Client(Java compile api + attach api) + Agent(腳本解析引擎 + ASM + JDK6 Instumentation) + Socket其實 BTrace 就是使用了 java attach api 附加 agent.jar ,然后使用腳本解析引擎+asm來重寫指定類的字節碼,再使用 instrument 實現對原有類的替換。

安裝和配置

本次安裝和配置在 Linux Ubuntu 14.04 下進行。目前 BTrace 的最新版本為 1.3.9,代碼托管在 [github] 上。第一步,在github 上下載 releases 版 btrace-bin-1.3.9.tgz,zip 版的沒有 build 目錄。第二步,解壓 btrace-bin-1.3.9.tgz 到一個目錄即可,例如 /home/fengzheng/soft/btrace , 到這一步其實就可以用了,只是執行腳本的時候需要在 btrace 命令前加上絕對路徑,如果想在任意目錄可執行,進行下一步第三步,配置環境變量,配置的環境變量包括 JAVA_HOMEBTRACE_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

之后執行命令 source /etc/profile ,使環境變量立即生效。接下來在任意目錄執行 btrace命令,都可以執行成功了。

簡單測試用例  

btrace 最簡單的語法是 btrace $pid script.java,所以需要知道要探測的 Java程序的進程id,然后編寫一個探測腳本即可。

1. 寫一個常駐內存的 Java 程序,這里寫了一個無限循環,每隔5秒鍾輸出一組計算結果,內容如下:

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("計算");
            NumberUtil util = new NumberUtil();
            int result = util.sum();
            System.out.println(result);
            try {
                Thread.sleep(5000);
            }catch (InterruptedException e){

            }
        }
    }
}

順便說一下命令行編譯和運行 Java 的過程:

編譯javac -d . NumberUtil.java,定位到 NumberUtil.java 所在目錄,然后執行此命令行,將會在當前目錄(.表示當前目錄)生成包名所示的目錄結構,kite/lab/utils/NumberUtil.class

執行java kite.lab.utils.NumberUtil 即可  

2. 執行上面的程序后,可用 jps 命令查看 pid(一般情況下用哪個賬號啟動的程序,就要用哪個賬號執行 jps ,root 賬號除外),執行 jps 命令看到如下結果:

root@ubuntu:/home/fengzheng/codes/btrace# jps
10906 Jps
10860 NumberUtil

3. 可以看到剛剛執行的 java 進程為 10860  

4. 編寫 btrace 腳本,腳本內容簡單如下:

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();
    }
}

意思是在執行結束后(location=@Location(Kind.RETURN) 表示執行結束)輸出結果和堆棧信息  

5. 預編譯:執行之前可以用預編譯命令檢查腳本的正確性,預編譯命令為 btracec,它是一個 javac-like 命令,btracec NumberUtilBTrace.java

6. 調用命令行執行,btrace 10860 NumberUtilBTrace.java ,(如果要保存到本地文件中,可以使用轉向命令 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)

7. 按ctrl + c ,會給出退出提示,再按 1 退出

使用場景

BTrace 是一個事后工具,所謂事后工具就是在服務已經上線了,但是發現存在以下問題的時候,可以用 BTrace。

  1. 比如哪些方法執行太慢,例如監控執行時間超過1s的方法

  2. 查看哪些方法調用了 System.gc() ,調用棧是怎樣的

  3. 查看方法參數或對象屬性

  4. 哪些方法發生了異常

多說一點,為了更好解決問題,最好還要配合事前准備和進行中監控,事前准備就是埋點嘛,在一些可能出現問題的方法中進行日志輸出,進行中監控就是利用一些實時監控工具,例如 VisualVM 、jmc 這些帶界面的工具或者 jdk 提供的命令行工具等,再高級一點的就是利用 Graphite 這樣的Metrics 工具配合 web 界面展示出來。

使用限制

 

為了保證trace語句只讀,最小化對被檢測程序造成影響, BTrace對trace腳本有一些限制(比如不能改變被trace代碼中的狀態)

  • BTrace class不能新建類, 新建數組, 拋異常, 捕獲異常,

  • 不能調用實例方法以及靜態方法(com.sun.btrace.BTraceUtils除外)

  • 不能將目標程序和對象賦值給BTrace的實例和靜態field

  • 不能定義外部, 內部, 匿名, 本地類

  • 不能有同步塊和方法

  • 不能有循環

  • 不能實現接口, 不能擴展類

  • 不能使用assert語句, 不能使用class字面值

攔截方法定義

@OnMethod 可以指定 clazz 、method、location。由此組成了在什么時機(location 決定)監控某個類/某些類(clazz 決定)下的某個方法/某些方法(method 決定)。

如何定位

 

1. 精准定位

直接定位到一個類下的一個方法,上面測試用的例子就是

 

 

2. 正則表達式定位

正則表達式在兩個"/" 之間,例如下面的例子,監控 javax.swing 包下的所有方法,注意正式環境中,范圍盡可能小一點,太大了性能會有影響。

 

@OnMethod(clazz="/javax\\.swing\\..*/", method="/.*/")
public static void swingMethods( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
   print("entered " + probeClass + "."  + probeMethod);
}

通過在攔截函數的定義里注入@ProbeClassName String probeClass, @ProbeMethodName String probeMethod 參數,告訴腳本實際匹配到的類和方法名。

3. 按接口或繼承類定位

例如要匹配繼承或實現了 com.kite.base 的接口或基類的,只要在類前加上 + 號就可以了,例如

@OnMethod(clazz="+com.kite.base", method="doSome")

4. 按注解定位

在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")  

攔截時機

攔截時機由 location 決定,當然也可為同一個定位加入多個攔截時機,即可以在進入方法時攔截、方法返回時攔截、拋出異常時攔截

1. Kind.Entry與Kind.Return

分別表示函數的開始和返回,不寫 location 的情況下,默認為 Kind.Entry,僅獲取參數值,可以用 Kind.Entry ,要獲取返回值或執行時間就要用 Kind.Return

2. Kind.Error, Kind.Throw和 Kind.Catch

表示異常被 throw 、異常被捕獲還有異常發生但是沒有被捕獲的情況,在攔截函數的參數定義里注入一個Throwable的參數,代表異常

@OnMethod(clazz = "com.kite.demo", location = @Location(value = Kind.LINE, line = 20))
public static void onBind() {

   println("執行到第20行");

}

  

@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location =@Location(Kind.ERROR)) public static void onBind(Throwable exception, @Duration long duration){ }

3. Kind.Call 和 Kind.Line  

Kind.Call 表示被監控的方法調用了哪些其他方法,例如:

@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 表示當前監控的函數所在類,如果是靜態類則為空,@TargetInstance 表示函數中調用的方法或屬性所在的類,如果是靜態方法則為空,@TargetMethodOrField 表示調用的方法或屬性,如果要獲取執行時間,那么 where 必須設置為 Where.AFTER

Kind.Line 監測類是否執行到了設置的行數,例如:

@OnMethod(clazz = "com.kite.demo", location = @Location(value = Kind.LINE, line = 20))
public static void onBind() {

   println("執行到第20行");

}

幾個例子

查看誰調用了GC

@OnMethod(clazz = "java.lang.System", method = "gc")
    public static void onSystemGC() {
        println("entered System.gc()");
        jstack();
    }

打印耗時超過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("類:", pcn));
            println(strcat("方法:", probeMethod));
            println(strcat("時長:", str(duration / 1000000)));
        }
    }

BTrace 提供了一系列的 sample, 可到 github 上查看。

注意問題

如果出現 Unable to open socket file: target process not responding or HotSpot VM not loaded 這個問題,可能的原因是執行 BTrace 腳本的用戶和 Java 進程運行的用戶不是同一個,使用 ps -aux | grep $pid查看一下 Java 進程的執行用戶,保證和 BTrace 腳本執行用戶相同即可  

 


 

古時的風箏 【微信公眾號】gushidefengzheng  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM