1. 程式人生 > 程式設計 >java應用監測(7)-線上動態診斷神器BTrace

java應用監測(7)-線上動態診斷神器BTrace

tags: java,troubleshooting,monitor,btrace


一句話概括:BTrace是一個是強大的java線上應用檢測工具(動態追蹤工具),可以在不修改應用程式碼,不停應用服務的前提下檢測程式碼執行情況,進而診斷問題,是生產環境下必備神器,本文將對它的使用進行講解。

1 引言

BTrace是一款開源軟體github地址為:https://github.com/btraceio/btrace,官網的介紹是BTrace is a safe,dynamic tracing tool for the Java platform.,它是安全的動態追蹤java應用的工具,即可以動態地向目標應用的位元組碼注入追蹤程式碼。何為動態?我們都知道,即在java應用啟動的時候會把class

檔案載入到JVM執行,此時class程式碼功能是確定、靜態的(無法變更),要想修改,只能是修改程式碼,重新編譯、部署、啟動。

而在處理線上應用時,我們經常需要檢視程式碼執行情況,引數值、返回值檢視,或者新增自己需要除錯的日誌等,在開發階段,新增日誌,重新啟動沒有問題,但在生產環境就不適用了(生產環境一般不輕易關停服務,而且即使可以重啟,可能發生問題的現場就破壞了,無法重現問題),那麼是否有方法在java應用執行期間,不重啟程式的情況,動態加入自己想要監測(追蹤)的內容?Btrace就是這樣一個動態追蹤神器,可以在不用重啟的情況下監控應用執行情況,可以獲取程式執行時的資料資訊,如方法引數、返回值、全域性變數和堆疊資訊等。本文就是對BTrace

進行執行原理和使用進行描述。

2 BTrace執行原理

2.1 class檔案的動態修改替換

BTrace是基於java的動態追蹤技術來實現的。對於java開發人員,都清楚java程式的開發流程是寫java程式碼,把它編譯為class檔案,然後在JVM中載入class執行。若此時想要在不停止應用的情況下對class進行修改來新增追蹤內容,如在某個方法(method)中新增輸出資訊,主要是兩件事情:

  • (1)修改已經載入到JVM中的class,新增自定義輸出
  • (2)替換執行在JVM`中的class

第一步,修改,由於JVM執行的都是class檔案,是不是可以直接修改位元組碼class檔案就行了(當然,位元組碼檔案的可讀性遠遠沒有Java程式碼高),但是已經有相應的框架可以做這件事,就是ASM

,利用這框架,可以直接編輯位元組碼的框架,它也提供介面可以讓我們方便地操作位元組碼檔案,進行注入修改類的方法,動態創造一個新的類等等。Spring就是使用這種技術來實現動態代理的。

第二步,替換,如果對它進行替換,則需要用到java提供的java.lang.instrument.Instrumentation,它有兩個介面redefineClassesretransformClassesredefineClasses是自己提供位元組碼檔案替換掉已存在的class檔案,retransformClasses是在已存在的位元組碼檔案上修改後再替換。不過需要注意的是instrument的使用有限制的(不能新增、修改、刪除已經有欄位和方法,不能改變方法簽名,改變繼承屬性等):

The redefinition must not add,remove or rename fields or methods,change the signatures of methods,or change inheritance
複製程式碼

2.2 BTrace的模組與執行流程

BTrace就是基於前面的技術來實現的,文章《Java動態追蹤技術探究》https://mp.weixin.qq.com/s/_hSaI5yMvPTWxvFgl-UItA)對動態追蹤技術進行了詳細說明,下面簡要說明一下。

2.2.1 主要模組

  • BTrace指令碼:利用BTrace定義的註解,我們可以很方便地根據需要進行指令碼的開發。

  • Compiler:將BTrace指令碼編譯成BTrace class檔案。

  • Client:將class檔案傳送到Agent。

  • Agent:基於Java的Attach API,Agent可以動態附著到一個執行的JVM上,然後開啟一個BTrace Server,接收client發過來的BTrace指令碼;解析指令碼,然後根據指令碼中的規則找到要修改的類;修改位元組碼後,呼叫Java Instrumentretransform介面,完成對物件行為的修改並使之生效。

2.2.2 執行流程

執行流程圖如下:

跟java原始碼一樣,先編寫Btrace指令碼(也是java檔案),編譯(compiler),通過client傳送給agentagent通過attach api新增到JVM並啟動agent server來接收client傳送過來的內容,然後底層是使用ASM修改位元組碼檔案,之後使用Java Instrumentretransform介面替換修改後的class檔案,執行後的輸出再通過agent傳送到client進行顯示。

3 BTrace安裝

知道了BTrace的執行原理,現在可以安裝實踐一下。本文用的示例還是java-monitor-exampleBTrace的安裝很簡單,開箱即用。

  • 下載地址(當前最新版本是[v1.3.11.3]):https://github.com/btraceio/btrace/releases
  • 解壓到需要監測的java應用所在伺服器中
  • btrace的命令在bin目錄 下
  • 若需要在任意目錄可執行,需要把btrace設定到環境變數中(export)

4 BTrace適用場景

基本上,BTrace只適用於動態追蹤類的輸出資訊,不能新增屬性、刪除方法,修改繼承等,這跟前面提到的Instrument的限制是一致的。一般來說,使用Btrace進行線上應用監測,基於都屬於日誌輸出類,多數包括以下幾大場景:

  • 檢視某一個方法中入參和返回值
  • 檢視某一個方法的響應時間
  • 檢視某行程式碼是否有執行到
  • 列印系統引數或JVM啟動引數
  • 列印方法呼叫的執行緒堆疊
  • 出現異常時打印出現異常資訊

5 BTrace使用

Btrace作為一個獨立執行的工具,預設只能在本地執行,也就是說,想要監測哪個正在執行的java應用,就需要把它解壓到對應的伺服器。本示例中執行的是java-monitor-example作為需要監測的java應用,然後就是根據監測業務需求,寫指令碼,執行指令碼,檢視輸出了。

5.1 指令碼編寫

5.1.1 註解與BTraceUtils

Btrace的指令碼與編寫java程式碼無異,不過相對簡單很多,主要是使用Btrace提供的註解和BTraceUtils,註解用於告訴Btrace需要攔截的類、攔截時機、攔截位置等,BTraceUtils用於提供列印輸出種資訊的功能。如官網給出的示例如下:

package samples;

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;

/**
 * This script traces method entry into every method of 
 * every class in javax.swing package! Think before using 
 * this script -- this will slow down your app significantly!!
 */
@BTrace public class AllMethods {
    @OnMethod(
        clazz="/javax\\.swing\\..*/",method="/.*/"
    )
    public static void m(@ProbeClassName String probeClass,@ProbeMethodName String probeMethod) {
        print(Strings.strcat("entered ",probeClass));
        println(Strings.strcat(".",probeMethod));
    }
}
複製程式碼

以上程式碼,表示,會攔截所有呼叫以javax.swing開頭的方法,然後打印出類名和方法名。可以注意到註解有@BTrace@OnMethod@ProbeClassName@ProbeMethodName,而printprintlnBTraceUtils提供的靜態方法。BTraceUtils還提供了很多列印方法(後面示例會提到)。另外,還要注意的是跟蹤操作都需要在靜態方法體內指定,因此都需要static方法。

另外,關於BTrace提供的註解,詳細可以參考官方檔案https://github.com/btraceio/btrace/wiki/BTrace-Annotations)。主要包括以下:

/**Class Annotations*/
@com.sun.btrace.annotations.DTrace
@com.sun.btrace.annotations.DTraceRef
@com.sun.btrace.annotations.BTrace
/**Method Annotations*/
@com.sun.btrace.annotations.OnMethod
@com.sun.btrace.annotations.OnTimer
@com.sun.btrace.annotations.OnError
@com.sun.btrace.annotations.OnExit
@com.sun.btrace.annotations.OnEvent
@com.sun.btrace.annotations.OnLowMemory
@com.sun.btrace.annotations.OnProbe
/**Argument Annotations*/
@com.sun.btrace.annotations.Self
@com.sun.btrace.annotations.Return
@com.sun.btrace.annotations.CalledInstance
@com.sun.btrace.annotations.CalledMethod
/**Field Annotations*/
@com.sun.btrace.annotations.Export
@com.sun.btrace.annotations.Property
@com.sun.btrace.annotations.TLS

複製程式碼

其中,@OnMethod用得比較多,需要重點說明一下,它主要是三個屬性clazzmethodlocation

  • clazz:類的全路徑名,如me.mason.monitor.controller.UserController

  • method:要監測的方法名,如getUsers

  • location:攔截時機,使用@Location註解。

@Location又有以下幾種:

  • Kind.ENTRY:在進入方法時呼叫
  • Kind.RETURN:方法執行完時呼叫,只有把攔截位置定義為Kind.RETURN,才能獲取方法的返回結果@Return和執行時間@Duration
  • Kind.CALL:方法中呼叫其它方法時呼叫
  • Kind.LINE:通過設定line,可以監控程式碼是否執行到指定的位置
  • Kind.ERROR,Kind.THROW,Kind.CATCH:異常情況的跟蹤

5.1.2 關於編寫

建議還是使用java的maven專案的開發環境進行編寫,可以使用程式碼提示功能。寫好後再放到對應需要監測的伺服器中。不過編輯時需要引用對應的jar包(btrace-agent,btrace-boot,btrace-client),對應的jar在下載的安裝下的build目錄下。通過pom.xml引入即可使用。如下所示:

<!-- BTrace -->
<dependency>
    <groupId>com.sun.btrace</groupId>
    <artifactId>btrace-agent</artifactId>
    <version>1.3.11.3</version>
    <type>jar</type>
    <scope>system</scope>
    <systemPath>E:/btrace-bin-1.3.11.3/build/btrace-agent.jar</systemPath>
</dependency>
<dependency>
    <groupId>com.sun.btrace</groupId>
    <artifactId>btrace-boot</artifactId>
    <version>1.3.11.3</version>
    <type>jar</type>
    <scope>system</scope>
    <systemPath>E:/btrace-bin-1.3.11.3/build/btrace-boot.jar</systemPath>
</dependency>
<dependency>
    <groupId>com.sun.btrace</groupId>
    <artifactId>btrace-client</artifactId>
    <version>1.3.11.3</version>
    <type>jar</type>
    <scope>system</scope>
    <systemPath>E:/btrace-bin-1.3.11.3/build/btrace-client.jar</systemPath>
</dependency>
複製程式碼

5.2 指令碼執行

列印幫助資訊如下:

一般來說,在伺服器上,直接是btrace PID btraceFile.java,然後檢視輸出(也可以把內容輸出到檔案中再檢視,如btrace PID btraceFile.java > info.txt)。如果有使用到特定的jar包,則需要把引數cpclasspath加上。如下示例是把呼叫方法的返回值進行輸出:

5.3 指令碼示例

下面通過幾個常用的示例來說明一下BTrace指令碼的使用,指令碼在示例工程java-monitor-example中的btrace目錄下。java-monitor-example中,分別是一個controllerservice,有如下方法定義,下面會根據這些方法進行動態追蹤。

/**
  * UserController.java
  **/
@GetMapping("/user")
public ResponseResult<User> getUser() {
    User user = userService.getUser();
    return ResponseResult.ok(user);
}

@GetMapping("/users")
public ResponseResult<User> getUsers(int num) {
    List<User> users = userService.getUsers(num);
    return ResponseResult.ok(users);
}

/**
  * UserService.java
  * 根據ID獲取使用者
  *
  * @return
  */
public User getUser() {
    return mockUser();
}

/**
  * 獲取使用者陣列
  *
  * @return
  */
public List<User> getUsers(int num) {
    userList.clear();
    for(int i=0 ; i < num; i++){
        userList.add(mockUser());
    }
    return userList;
}
複製程式碼

5.3.1 列印方法相關資訊

  • 列印呼叫方法時的引數(呼叫UserControllergetUsers方法時列印)
@OnMethod(clazz = "me.mason.monitor.controller.UserController",method = "getUsers",location = @Location(Kind.ENTRY))
public static void readFunction(@ProbeClassName String className,@ProbeMethodName String methodName,AnyType[] args) {
    // 列印時間
    BTraceUtils.println(BTraceUtils.Time.timestamp("yyyy-MM-dd HH:mm:ss"));
    BTraceUtils.println("method controller");
    BTraceUtils.printArray(args);
    BTraceUtils.println(className + "," + methodName);
    BTraceUtils.println("==========================");
}
複製程式碼
  • 列印呼叫方法時的返回值
@OnMethod(clazz = "me.mason.monitor.service.UserService",location = @Location(Kind.RETURN))
public static void printReturnData1(@Return AnyType result){
    BTraceUtils.println(BTraceUtils.Time.timestamp("yyyy-MM-dd HH:mm:ss"));
    BTraceUtils.printFields(result);
    BTraceUtils.println("==========================");
    BTraceUtils.println(BTraceUtils.str(result));
    BTraceUtils.println("==========================");
}
複製程式碼
  • 執行到的行數(檢視是否執行到UserService的39行)
@OnMethod(clazz = "me.mason.monitor.service.UserService",location = @Location(value = Kind.LINE,line = 39))
public static void printLineData(@ProbeClassName String className,int line){
    BTraceUtils.println(BTraceUtils.Time.timestamp("yyyy-MM-dd HH:mm:ss"));
    BTraceUtils.println(className + "," + methodName + ","+line);
    BTraceUtils.println("==========================");
 }
複製程式碼
  • 執行方法的用時(UserControllergetUsers方法用時多長)
@OnMethod(clazz = "me.mason.monitor.controller.UserController",location = @Location(Kind.RETURN))
public static void getUsersDuration(@Duration long duration){
    BTraceUtils.println(BTraceUtils.Time.timestamp("yyyy-MM-dd HH:mm:ss"));
    BTraceUtils.println("time(ns):" + duration);
    BTraceUtils.println("time(ms):" + BTraceUtils.str(duration / 1000000));
    BTraceUtils.println("time(s):" + BTraceUtils.str(duration / 1000000000));
    BTraceUtils.println("==========================");
}
複製程式碼

5.3.2 列印系統屬性及JVM屬性

類似JDK的命令列工具jinfo,另外jmapjstatck可查詢官方示例。

@BTrace
public class JInfo {
    static {
        println("System Properties:");
        printProperties();
        println("VM Flags:");
        printVmArguments();
        println("OS Enviroment:");
        printEnv();
        exit(0);
    }
}
複製程式碼

5.3.3 列印異常輸出

java開發人員應該都知道,java的異常分為ErrorException,而它們都是Throwable的子類,即java中所有異常的父類都Throwable,因此追蹤這個的建構函式,然後把堆疊打印出來即可。如下:

//區域性變數儲存異常
@TLS static Throwable currentException;
//異常建構函式開始
@OnMethod(
    clazz="java.lang.Throwable",method="<init>"
)
public static void onthrow(@Self Throwable self) {
    currentException = self;
}
//異常建構函式結束,輸出堆疊
@OnMethod(
    clazz="java.lang.Throwable",method="<init>",location=@Location(Kind.RETURN)
)
public static void onthrowreturn() {
    if (currentException != null) {
        Threads.jstack(currentException);
        println("=====================");
        currentException = null;
    }
}
複製程式碼

5.4 指令碼限制

BTrace對JVM來說是“只讀的”,BTrace要做的是,雖然修改了位元組碼,但是主要是輸出需要的資訊,對整個程式的正常執行並沒有影響。需要注意的是,由於是動態替換class檔案,被修改的位元組碼是不會自動還原的。官方檔案也有說明,BTrace指令碼會有以下限制:

  • 不允許建立物件

  • 不允許建立陣列

  • 不允許拋異常

  • 不允許catch異常

  • 不允許隨意呼叫其他物件或者類的方法,只允許呼叫com.sun.btrace.BTraceUtils中提供的靜態方法(一些資料處理和資訊輸出工具)

  • 不允許改變類的屬性

  • 不允許有成員變數和方法,只允許存在static public void方法

  • 不允許有內部類、巢狀類

  • 不允許有同步方法和同步塊

  • 不允許有迴圈

  • 不允許隨意繼承其他類(當然,java.lang.Object除外)

  • 不允許實現介面

  • 不允許使用assert

  • 不允許使用Class物件

6 一些經驗

  • 搭建使用java的maven專案的開發環境進行指令碼編寫,引入相應的jar,以提供程式碼提示功能。
  • 檢視官方提供的例子,在下載包中已提供例子,位置:btrace-bin-1.3.11.3\samples目錄
  • BTrace指令碼中追蹤的輸入引數,返回值型別是簡單型別直接使用(如int,float等),複雜型別可以使用AnyType,但如果是使用自定義包中的型別(如User),則需要執行指令碼時新增cpclasspath引數,指定自定義包。
  • 一般簡單型別或字串,直接使用printprintln,列印物件屬性可使用printFields,列印List,可以使用BTraceUtils.println(BTraceUtils.str(list))
  • 在探查方法的最後一行列印分隔,強烈建議。可能是由於輸出有緩衝區延遲,如果不輸出分隔,有可能會無法輸出或者輸出後內容沒有分隔。分隔可使用BTraceUtils.printlnBTraceUtils.println("============")

7 總結

對於線上的java應用,如果想不停服務進行日誌輸出來診斷問題,動態追蹤技術是必不可少的技術,而Btrace是使用此技術來實現動態追蹤的有力工具。本文從Btrace的執行原理、安裝、適用場景、指令碼編寫、執行等方面進行了詳細描述,希望可以幫助大家加深Btrace的瞭解,更方便、有效率地解決線上問題。

引數資料

  • BTrace官網:https://github.com/btraceio/btrace
  • BTrace註解:https://github.com/btraceio/btrace/wiki/BTrace-Annotations
  • 示例程式碼地址:https://github.com/mianshenglee/my-example/tree/master/java-monitor-example
  • Java動態追蹤技術探究:https://mp.weixin.qq.com/s/_hSaI5yMvPTWxvFgl-UItA
  • BTrace原理淺析:https://www.rowkey.me/blog/2016/09/20/btrace/

相關閱讀