BTrace學習總結


一、簡介:

在生產環境中經常遇到格式各樣的問題,如OOM或者莫名其妙的進程死掉。一般情況下是通過修改程序,添加打印日志;然后重新發布程序來完成。然而,這不僅麻煩,而且帶來很多不可控的因素。有沒有一種方式,在不修改原有運行程序的情況下獲取運行時的數據信息呢?如方法參數、返回值、全局變量、堆棧信息等。Btrace就是這樣一個工具,它可以在不修改原有代碼的情況下動態地追蹤java運行程序,通過hotswap技術,動態將跟蹤字節碼注入到運行類中,對運行代碼侵入較小,對性能上的影響可以忽略不計。

在下列情況時可以使用BTrace進行分析:

1、接口性能變慢,分析每個方法的耗時情況;

2、當在Map中插入大量數據,分析其擴容情況;

3、分析哪個方法調用了System.gc(),調用棧如何;

4、執行某個方法拋出異常時,分析運行時參數;

5、..................

二、安裝:

1、安裝JDK;

2、下載BTrace的壓縮包,這里使用的是BTrace 1.3.11版本,可以到下面地址下載:

http://www.voidcn.com/link?url=https://github.com/btraceio/btrace/releases/tag/v1.3.11

3、將BTrace包解壓,在系統的環境變量上添加變量BTRACE_HOME,並設置其路徑為BTrace的路徑,同時在PATH變量中添加上路徑%BTRACE_HOME%\bin;

4、編輯%BTRACE_HOME%\bin\btrace.bat文件,將其中的-Dcom.sun.btrace.unsafe=false改為-Dcom.sun.btrace.unsafe=true

5、btrace命令的語法說明:

btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]

1)沒有這個表明跳過預編譯;

2)include-path:指定用來編譯腳本的頭文件路徑(關於預編譯可參考例子ThreadBean.java)

3)port:btrace agent端口,默認是2020

4)classpath:編譯所需類路徑,一般是指btrace-client.jar等類所在路徑;

5)pid:java進程id

6)btrace-script:btrace腳本可以是.java文件,也可以是.class文件;

7)args:傳遞給btrace腳本的參數, 在腳本中可以通過$(), $length()來獲取這些參數(定義在BTraceUtils)

三、Demo

(一)JavaSE應用Demo

1、編寫測試功能實現類:

public class Calculator {

 

    public int add(int a, int b) {

        try {

            Thread.sleep(1000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        return a + b;

    }

}

 

2、編寫調用代碼:

public class App

{

    public static void main( String[] args )

    {

        Calculator calc = new Calculator();

        Random random = new Random();

        while (true) {

            int a = random.nextInt(10);

            int b = random.nextInt(20);

            int c = calc.add(a, b);

            System.out.println(String.format("%d + %d = %d", a, b, c));

        }

    }

}

上面的代碼無限循環調用Calculator .add方法並輸出調用結果;

 

3、運行程序,可以看到屏幕上不停的輸出各種加法運算的表達式;

4、編寫btrace腳本:

@BTrace(unsafe = true)

public class BTraceTest {

    @OnMethod(clazz = "com.ucar.test.Calculator", method = "add", location = @Location(Kind.RETURN))

    public static void traceTest(int a, int b, @Return int sum) {

        println(String.format("%d + %d = %d", a, b, sum));

    }

}

@BTrace注解中要加上unsafe=true,否則運行btrace腳本時會因為安全機制導致報錯而無法執行腳本;

@OnMethod注解中的clazz表示要跟蹤的類名,method表示要跟蹤的方法名稱,location表示在什么時候進行攔截;

 

5、運行btrace腳本,可以看到前面輸出的加法運算表達式也能在這個窗口上輸出;

運行btrace腳本的命令為:

btrace 3856 BTraceTest.java

其中3856為剛才運行的java程序的進程ID;

(二)web應用Demo

1、新建SpringMVC的web應用程序(參考https://www.cnblogs.com/laoxia/p/9311442.html);

2、實現Controller:

@RestController

@RequestMapping("/btrace")

public class BTraceController {

 

    @RequestMapping("/arg1")

    public String arg1(@RequestParam("name") String name) {

        return "hello: " + name;

    }

}

 

3、生成war包並放到tomcat的webapp目錄下,啟動tomcat,瀏覽器中打開URL地址:http://localhost:8080/test/btrace/arg1?name=aaaaa,頁面上應該能正常打印出“hello: aaaaa”;

4、編寫BTrace腳本:

@BTrace(unsafe = true)

public class PrintArgSimple {

 

    @OnMethod(clazz = "com.ucar.test.controller.BTraceController", method = "arg1", location = @Location(Kind.RETURN))

    public static void anyRead(@ProbeClassName String pcn, // 被攔截的類名

                               @ProbeMethodName String pmn,  //被攔截的方法名

                               AnyType[] args  //被攔截的方法的參數值) {

        BTraceUtils.printArray(args);

        BTraceUtils.println("className: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println();

    }

}

注意:需要在maven的POM文件中引入btrace-client.jar, btrace-boot.jar和btrace_agent.jar三個文件或者直接引入這三個jar包;

 

5、運行腳本,然后在瀏覽器中請求第三步的URL地址,這時候就能看到屏幕上打印出運行過程中的相關信息;

運行BTrace的命令為:

btrace 1256 PrintArgSimple.java

其中1256為這個web應用進程的進程ID

6、注意:需要將web應用打包成war放到tomcat下運行,如果直接在idea下運行會報錯;

四、攔截時機:

1、Kind.ENTRY:入口攔截,默認值;

2、Kind.RETURN:攔截返回值,只有把攔截位置定義為Kind.RETURN,才能獲取方法的返回結果@Return和執行時間@Duration

3、Kind.THROW:發生異常時攔截;

4、Kind.LINE:攔截某一行,可以監控代碼是否執行到指定的位置;

5、Kind.CALL:分析方法中調用其它方法的執行情況,比如在execute方法中,想獲取add方法的執行耗時,必須把where設置成Where.AFTER

 

五、技巧:

1、攔截構造函數:

指定method = "<init>"即可攔截指定類的構造函數;

 

2、攔截同名函數:攔截同名重載方法,只需要在BTrace腳本的方法中聲明與之對應的參數即可。

比如有如下兩個同名方法:

@RequestMapping("/same1")

public String same(@RequestParam("name") String name) {

    return "hello: " + name;

}

 

@RequestMapping("/same2")

public User same(@RequestParam("id") int id,

                 @RequestParam("name") String name) {

    return new User(id, name);

}

 

編寫如下的btrace腳本即可攔截:

@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) {

    BTraceUtils.println("ClassName: " + pcn);

    BTraceUtils.println("MethodName: " + pmn);

    BTraceUtils.println("name: " + name);

    BTraceUtils.println();

}

 

@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) {

    BTraceUtils.println("ClassName: " + pcn);

    BTraceUtils.println("MethodName: " + pmn);

    BTraceUtils.println("id: " + id);

    BTraceUtils.println("name: " + name);

    BTraceUtils.println();

}

 

3、攔截返回值:

指定location=@Location(Kind.RETURN),並且在方法的參數里面加上@Return AnyType result即可接收返回值;

 

4、攔截異常:

@BTrace

public class PrintOnThrow {    

    @TLS

    static Throwable currentException;

 

    @OnMethod(

        clazz="java.lang.Throwable",

        method="<init>"

    )

    public static void onthrow(@Self Throwable self) {  // @Self其實就是攔截了this

        currentException = self;

    }

 

    @OnMethod(

        clazz="java.lang.Throwable",

        method="<init>"

    )

    public static void onthrow1(@Self Throwable self, String s) {        

        currentException = self;

    }

 

    @OnMethod(

        clazz="java.lang.Throwable",

        method="<init>"

    )

    public static void onthrow1(@Self Throwable self, String s, Throwable cause) {        

        currentException = self;

    }

 

    @OnMethod(

        clazz="java.lang.Throwable",

        method="<init>"

    )

    public static void onthrow2(@Self Throwable self, Throwable cause) {

        currentException = self;

    }

 

    @OnMethod(

        clazz="java.lang.Throwable",

        method="<init>",

        location=@Location(Kind.RETURN)

    )

    public static void onthrowreturn() {

        if (currentException != null) {

            // 打印異常堆棧

            BTraceUtils.Threads.jstack(currentException);

            BTraceUtils.println("=====================");

            // 打印完之后就置空

            currentException = null;

        }

    }

}

在命令行里運行該腳本,訪問相應的接口后,即可輸出異常堆棧;即使異常被try catch給隱藏起來了,這個腳本也一樣能揪出來。

 

5、攔截指定行:

@BTrace

public class PrintLine {

    @OnMethod(

            clazz="org.zero01.monitor_tuning.controller.BTraceController",

            method="exception",

            location=@Location(value=Kind.LINE, line=43)  // 攔截第43

    )

    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) {

        BTraceUtils.println("ClassName: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println("line: " + line);

        BTraceUtils.println();

    }

}

如果沒有任何輸出的話,就代表那一行沒有被執行到,所以沒被攔截。這種攔截某一行的方式,不適用於判斷是否有異常,只能單純用於判斷某一行是否被執行了。

 

6、攔截復雜參數:

比如要攔截下面方法的復雜參數類型User:

@RequestMapping("/arg2")

public User arg2(User user) {

    return user;

}

 

可以使用下面的btrace腳本攔截:

@BTrace

public class PrintArgComplex {

    @OnMethod(

            clazz = "org.zero01.monitor_tuning.controller.BTraceController",

            method = "arg2",

            location = @Location(Kind.ENTRY)

    )

    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) {

        //print all fields

        BTraceUtils.print("print all fields: ");

        BTraceUtils.printFields(user);

 

        //print one field

        Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name");

        BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user));

 

        BTraceUtils.println("ClassName: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println();

    }

}

 

7、攔截環境變量:

@BTrace

public class PrintJinfo {

    static {

        // 打印系統屬性

        BTraceUtils.println("System Properties:");

        BTraceUtils.printProperties();

 

        // 打印JVM參數

        BTraceUtils.println("VM Flags:");

        BTraceUtils.printVmArguments();

 

        // 打印環境變量

        BTraceUtils.println("OS Enviroment:");

        BTraceUtils.printEnv();

 

        // 退出腳本

        BTraceUtils.exit(0);

    }

}

 

8、使用正則表達式攔截:

@BTrace

public class PrintRegex {

    @OnMethod(

            // 類名也可以使用正則表達式進行匹配

            clazz = "org.zero01.monitor_tuning.controller.BTraceController",  

            // 正則表達式需要寫在兩個斜杠內

            method = "/.*/"  

    )

    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) {

        BTraceUtils.println("ClassName: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println();

    }

}

 

六、注意事項:

1、@ProbeClassName String clazz:此處String不能寫為java.lang.String

2、@OnMethod(clazz="com.alibaba.security.acl.support.PermissionFactory", method="createPermission", type="com.alibaba.security.acl.support.AbstractPermission(java.lang.String,java.lang.String,com.alibaba.security.acl.support.PermissionDefiner)")

此處得String必須寫成java.lang.String

3、BTrace腳本默認只能本地運行,也就是只能調試本地的Java進程。如果需要在本地調試遠程的Java進程的話,是需要自己去修改BTrace源碼的;

4、BTrace腳本在生產環境下可以使用,但是被修改的字節碼不會被還原。所以我們需要先在本地調試好BTrace腳本,然后才能放到生產環境下使用。並且需要注意BTrace腳本中不能含有影響性能或消耗資源較多的代碼,不然會導致線上的服務性能降低。

七、其他:

1、其他命令行工具說明:

(1) Btracec:用於預編譯BTrace腳本,用於在編譯時期驗證腳本正確性。

btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>

參數意義同btrace命令一致,directory表示編譯結果輸出目錄。

(2) Btracerbtracer命令同時啟動應用程序和BTrace腳本,即在應用程序啟動過程中使用BTrace腳本。而btrace命令針對已運行程序執行BTrace腳本。

btracer <pre-compiled-btrace.class> <application-main-class> <application-args>

參數說明:

pre-compiled-btrace.class表示經過btracec編譯后的BTrace腳本。

application-main-class表示應用程序代碼;

application-args表示應用程序參數。

2、方法上的注解:

(1) @ OnMethod用來指定trace的目標類和方法以及具體位置,被注解的方法在匹配的方法執行到指定的位置會被調用。

  •  "clazz"屬性用來指定目標類名,可以指定全限定類名,比如"java.awt.Component",也可以是正則表達式(表達式必須寫在"//",比如"/java\\.awt\\..+/")
  •  "method"屬性用來指定被trace的方法.表達式可以參考自帶的例子(NewComponent.javaClassload.java,關於方法的注解可以參考MultiClass.java)
  •  有時候被trace的類和方法可能也使用了注解.用法參考自帶例子WebServiceTracker.java
  •  針對注解也是可以使用正則表達式,比如像這個"@/com\\.acme\\..+/ ",也可以通過指定超類來匹配多個類,比如"+java.lang.Runnable"可以匹配所有實現了java.lang.Runnable接口的類.具體參考自帶例子SubtypeTracer.java

(2) @OnTimer定時觸發Trace,時間可以指定,單位為毫秒,具體參考自帶例子Histogram.java

(3) @OnErrortrace代碼拋異常或者錯誤時,該注解的方法會被執行.如果同一個trace腳本中其他方法拋異常,該注解方法也會被執行。

(4) @OnExittrace方法調用內置exit(int)方法(用來結束整個trace程序),該注解的方法會被執行.參考自帶例子ProbeExit.java

(5) @OnEvent用來截獲"外部"btrace client觸發的事件,比如按Ctrl-C中斷btrace執行時,並且選擇2,或者輸入事件名稱,將執行使用了該注解的方法,該注解的value值為具體事件名稱。具體參考例子HistoOnEvent.java

(6) @OnLowMemory當內存超過某個設定值將觸發該注解的方法,具體參考MemAlerter.java

(7) @OnProbe使用外部文件XML來定義trace方法以及具體的位置,具體參考示例SocketTracker1.javajava.net.socket.xml

3、參數上的注解:

  • @Self用來指定被trace方法的this,可參考例子AWTEventTracer.javaAllCalls1.java
  • @Return用來指定被trace方法的返回值,可參考例子Classload.java
  • @ProbeClassName (since 1.1)用來指定被trace的類名,可參考例子AllMethods.java
  • @ProbeMethodName (since 1.1)用來指定被trace的方法名,可參考例子WebServiceTracker.java
  • @TargetInstance (since 1.1)用來指定被trace方法內部被調用到的實例,可參考例子AllCalls2.java
  • @TargetMethodOrField (since 1.1)用來指定被trace方法內部被調用的方法名,可參考例子AllCalls1.javaAllCalls2.java

4、屬性上的注解:

  • @Export該注解的靜態屬性主要用來與jvmstat計數器做關聯, 使用該注解之后,btrace程序就可以向jvmstat客戶端(可以用來統計jvm堆中的內存使用量)暴露trace程序的執行次數, 具體可參考例子ThreadCounter.java
  • @Property使用了該注解的trace腳本將作為MBean的一個屬性,一旦使用該注解, trace腳本就會創建一個MBean並向MBean服務器注冊, 這樣JMX客戶端比如VisualVMjconsole就可以看到這些BTrace MBean, 如果這些被注解的屬性與被trace程序的屬性關聯, 那么就可以通過VisualVMjconsole來查看這些屬性了, 具體可參考例子ThreadCounterBean.javaHistogramBean.java
  • @TLS用來將一個腳本變量與一個ThreadLocal變量關聯, 因為ThreadLocal變量是跟線程相關的, 一般用來檢查在同一個線程調用中是否執行到了被trace的方法, 具體可參考例子OnThrow.javaWebServiceTracker.java

5、類上的注解:

  • @com.sun.btrace.annotations.DTrace用來指定btrace腳本與內置在其腳本中的D語言腳本關聯, 具體參考例子DTraceInline.java
  • @com.sun.btrace.annotations.DTraceRef用來指定btrace腳本與另一個D語言腳本文件關聯, 具體參考例子DTraceRefDemo.java
  • @com.sun.btrace.annotations.BTrace用來指定該java類為一個btrace腳本文件。

6、BTrace文件下的samples文件夾下包含了很多的示例,這些示例說明如下:

AWTEventTracer.java -演示了對EventQueue.dispatchEvent()事件進行trace的做法,可以通過instanceof來對事件進行過濾,比如這里只針對focus事件trace.

AllLines.java -演示了如何在被trace的程序到達probe指定的類和指定的行號時執行指定的操作(例子中指定的行號是-1表示任意行).

AllSync.java -演示了如何在進入/退出同步塊進行trace.

ArgArray.java -演示了打印java.io包下所有類的readXXX方法的輸入參數.

Classload.java -演示打印成功加載指定類以及堆棧信息.

CommandArg.java -演示如何獲取btrace命令行參數.

Deadlock.java -演示了@OnTimer注解和內置deadlock()方法的用法

DTraceInline.java -演示@DTrace注解的用法

DTraceDemoRef.java -演示@DTraceRef注解的用法.

FileTracker.java -演示了如何對File{Input/Output}Stream構造函數中初始化打開文件的讀寫文件操作進行trace.

FinalizeTracker.java -演示了如何打印一個類所有的屬性,這個在調試和故障分析中非常有用.這里的例子是打印FileInputStream類的close() /finalize()方法被調用時的信息.

Histogram.java -演示了統計javax.swing.JComponent在一個應用中被創建了多少次.

HistogramBean.java -同上例,只不過演示了如何與JMX集成,這里的map屬性通過使用@Property注解被暴露成一個MBean.

HistoOnEvent.java -同上例,只不過演示了如何在通過按ctrl+c中斷當前腳本時打印出創建次數,而不是定時打印.

JdbcQueries.java -演示了聚合(aggregation)功能.關於聚合功能可參考DTrace.

JInfo.java -演示了內置方法printVmArguments(), printProperties()printEnv()的用法

JMap.java -演示了內置方法dumpHeap()的用法.即將目標應用的堆信息以二進制的形式dump出來

JStack.java -演示了內置方法jstackAll()的用法,即打印所有線程的堆棧信息.

LogTracer.java -演示了如何深入實例方法(Logger.log)並調用內置方法(field() )打印私有屬性內容.

MemAlerter.java -演示了使用@OnLowMememory注解監控內存使用情況.即堆內存中的年老代達到指定值時打印出內存信息.

Memory.java -演示每隔4s打印一次內存統計信息.

MultiClass.java -演示了通過使用正則表達式對多個類的多個方法進行trace.

NewComponent.java -使用計數器每隔一段時間檢查當前應用中創建java.awt.Component的個數.

OnThrow.java -當拋出異常時,打印出異常堆棧信息.

ProbeExit.java -演示@OnExit注解和內置exit(int)方法的用法

Profiling.java -演示了對profile的支持. //我執行沒成功, BTrace內部有異常

Sizeof.java -演示了內置的sizeof方法的使用.

SocketTracker.java -演示了對socketcreation/bind方法的trace.

SocketTracker1.java -同上,只不過使用了@OnProbe.

SysProp.java -演示了使用內置方法獲取系統屬性,這里是對java.lang.SystemgetProperty方法進行trace.

SubtypeTracer.java -演示了如何對指定超類的所有子類的指定方法進行trace.

ThreadCounter.java -演示了在腳本中如何使用jvmstat計數器. (jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count需要這樣來從外部通過jstat來訪問)

ThreadCounterBean.java -同上,只不過使用了JMX.

ThreadBean.java -演示了對預編譯器的使用(並結合了JMX).

ThreadStart.java -演示了腳本中DTrace的用法.

Timers.java -演示了在一個腳本中同時使用多個@OnTimer

URLTracker.java -演示了在每次URL.openConnection成功返回時打印出url.這里也使用了D語言腳本.

WebServiceTracker.java -演示了如何根據注解進行trace.

 

7、參考文檔:

http://huanghaifeng1990.iteye.com/blog/2121419

http://agapple.iteye.com/blog/962119

http://agapple.iteye.com/blog/1005918

 


免責聲明!

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



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