1. 程式人生 > >認識一下java神器Btrace

認識一下java神器Btrace

 

轉載:

http://calvin1978.blogcn.com/articles/btrace1.html

 

BTrace是神器,每一個需要每天解決線上問題,但完全不用BTrace的Java工程師,都是可疑的。

BTrace的最大好處,是可以通過自己編寫的指令碼,獲取應用的一切呼叫資訊。而不需要不斷地修改程式碼,加入System.out.println(), 然後重啟,然後重啟,然後重啟應用!!!

同時,特別嚴格的約束,保證自己的消耗特別小,只要定義指令碼時不作大死,直接在生產環境開啟也沒影響。

在網上搜索BTrace出來的文章都有點舊了,而且不夠詳細,於是決定,重新寫一份。

1. 概述

1.1 快速開始

BTrace搬家了!! 已經搬離了Sun,搬到了http://github.com/btraceio/btrace,目前的版本已經是1.38。

在Release頁面裡下載最新Zip版,解壓就能用,UserGuide和Samples也在裡面。

先抄一個UserGuide裡的例子:

import com.sun.btrace.annotations.*;

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



@BTrace

public class HelloWorld {

    @OnMethod(clazz="java.lang.Thread", method="start")

    public static void onThreadStart() {

        println("thread start!");

    }

}

  

然後ps找出要監控的java應用的pid, ./btrace $pid HelloWorld.java 就跑起來了。

是不是很簡單??基本上不用任何BTrace的知識,都能猜出HelloWorld會幹啥。通過JVM Attach API,btrace把自己綁進了被監控的程序,按HelloWorld.java裡的定義,進行AOP式的程式碼植入。

最開心就是這裡,如果還想監控其他內容,直接修改HelloWorld.java,再執行一次btrace就可以了,不需要重啟應用!! 重啟應用!!

 

1.2 典型的場景

1. 服務慢,能找出慢在哪一步,哪個函式裡麼?

2. 誰呼叫了System.gc(),呼叫棧如何?

3. 誰構造了一個超大的ArrayList?

4. 什麼樣的入參或物件屬性,導致丟擲了這個異常?或進入了這個處理分支?

 

1.3 一些重要的事

為了避免Btrace指令碼的消耗過大影響真正業務,所以定義了一系列不允許的事情:比如不允許呼叫任何類的任何方法,只能呼叫BTraceUtils 裡的一系列方法和腳本里定義的static方法。 比如不允許建立物件,比如不允許For 迴圈等等,更多規定看User Guide。

當然,可以用-u 執行在unsafe mode來規避限制,但不推薦。

在以前的例子裡,甚至還不能字串相加,必須用strcat:

println(strcat(strcat(probeClass, "."), probeMethod));

好在新版裡已經可以寫回:

println(probeClass + '.' + probeMethod);

另外,BTrace植入過的程式碼,會一直在,直到應用重啟為止。所以即使Btrace推出了,業務函式每次執行時都會多出一次Btrace是否Attach狀態的判斷。

最後,記得用Eclipse,而不是寫字板來寫指令碼。

 

1.4 其他命令列選項

1.4.1 定義classpath

如果在HelloWorld.java裡使用了JDK外的其他類,比如Netty的:

./btrace -cp .:netty-all-4.0.41.Final.jar $pid HelloWorld.java

但上面定義的classpath只在編譯指令碼時使用,而腳本里需要顯式使用非JDK類的機會其實很少(後面真正用到的時候會提起)。
而在執行時,因為已經綁到目標應用的JVM裡,用的是目標JVM的classpath。

1.4.2 結果輸出到檔案

./btrace -o mylog $pid HelloWorld.java

很坑新人的引數,首先,這個mylog會生成在應用的啟動目錄,而不是btrace的啟動目錄。其次,執行過一次-o之後,再執行btrace不加-o 也不會再輸出回console,直到應用重啟為止。

所以有時也直接用轉向了事:
./btrace $pid HelloWorld.java > mylog

 

1.4.3.預編譯指令碼

雖然btrace可以實時編譯Java原始檔,但如果你的指令碼是要給運維同學執行的,線上執行時才發現寫錯了就尷尬了。此時可以用btracec命令預編譯一下:

./btracec HelloWorld.java

 

2. 攔截方法定義

2.1 精準定位

就是HelloWorld的例子,精確定義要監控的類與方法。

2.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 引數,告訴指令碼實際匹配到的類和方法名。

另一個例子,監控Statement的executeUpdate(), executeQuery() 和 executeBatch() 三個方法,見JdbcQueries.java

 

2.3 按介面,父類,Annotation定位

比如我想匹配所有的Filter類,在介面或基類的名稱前面,加個+ 就行
@OnMethod(clazz="+com.vip.demo.Filter", method="doFilter")

也可以按類或方法上的annotaiton匹配,前面加上@就行
@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")

 

2.4 其他

1. 建構函式的名字是 <init>
@OnMethod(clazz="java.net.ServerSocket", method="<init>")

2. 靜態內部類的寫法,是在類與內部類之間加上"$"

@OnMethod(clazz="com.vip.MyServer$MyInnerClass", method="hello")

3. 如果有多個同名的函式,想區分開來,可以在攔截函式上定義不同的引數列表(見4.1)。

 

3. 攔截時機

可以為同一個函式的不同的Location,分別定義多個攔截函式。

3.1 Kind.Entry與Kind.Return

@OnMethod( clazz="java.net.ServerSocket", method="bind" )
不寫Location,預設就是剛進入函式的時候(Kind.ENTRY)。

但如果你想獲得函式的返回結果或執行時間,則必須把切入點定在返回(Kind.RETURN)時。

OnMethod(clazz = "java.net.ServerSocket", method = "getLocalPort", location = @Location(Kind.RETURN))

public static void onGetPort(@Return int port, @Duration long duration)

  

duration的單位是納秒,要除以 1,000,000 才是毫秒。
 

3.2 Kind.Error, Kind.Throw和 Kind.Catch

異常丟擲(Throw),異常被捕獲(Catch),異常沒被捕獲被丟擲函式之外(Error),主要用於對某些異常情況的跟蹤。

在攔截函式的引數定義裡注入一個Throwable的引數,代表異常。

@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(Kind.ERROR))

public static void onBind(Throwable exception, @Duration long duration)

  

3.3 Kind.Call與Kind.Line

下例定義監控bind()函式裡呼叫的所有其他函式:

@OnMethod(clazz = "java.net.ServerSocket", method = "bind", 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)

所呼叫的類及方法名所注入到@TargetInstance與 @TargetMethodOrField中。

​靜態函式中,instance的值為空。如果想獲得執行時間,必須把Where定義成AFTER。
如果想獲得執行時間,必須 把Where定義成AFTER。

注意這裡,一定不要像下面這樣大範圍的匹配,否則這效能是神仙也沒法救了

@OnMethod(clazz = "/javax\\.swing\\..*/", method = "/.*/", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))

  

下例監控程式碼是否到達了Socket類的第363行。

 

@OnMethod(clazz = "java.net.ServerSocket", location = @Location(value = Kind.LINE, line = 363))

public static void onBind4() {

   println("socket bind reach line:363");

}

  

line還可以為-1,然後每行都會打印出來,加引數int line 獲得的當前行數。此時會顯示函式裡完整的執行路徑,但肯定又非常慢。

4. 列印this,引數 與 返回值

4.1 定義注入

import com.sun.btrace.AnyType;

@OnMethod(clazz = "java.io.File", method = "createTempFile", location = @Location(value = Kind.RETURN))

public static void o(@Self Object self, String prefix, String suffix, @Return AnyType result)

  

如果想列印它們,首先按順序定義用@Self 註釋的this, 完整的引數列表,以及用@Return 註釋的返回值。

需要列印哪個就定義哪個,不需要的就不要定義。但定義一定要按順序,比如引數列表不能跑到返回值的後面。

Self:

如果是靜態函式, self為空。

前面提到,如果上述使用了非JDK的類,命令列裡要指定classpath。不過,如前所述,因為BTrace裡不允許呼叫類的方法,所以定義具體類很多時候也沒意思,所以self定義為Object就夠了。

引數:

引數數列表要麼不要定義,要定義就要定義完整,否則BTrace無法處理不同引數的同名函式。

如果有些引數你實在不想引入非JDK類,又不會造成同名函式不可區分,可以用AnyType來定義(不能用Object)。

如果攔截點用正則表示式中匹配了多個函式,函式之間的引數個數不一樣,你又還是想把引數打印出來時,可以用AnyType[] args來定義。

但不知道是不是當前版本的bug,AnyType[] args 不能和 location=Kind.RETURN 同用,否則會進入一種奇怪的靜默狀態,只要有一個函式定義錯了,整個Btrace就什麼都打印不出來。

結果:

同理,結果也可以用AnyType來定義,特別是用正則表示式匹配多個函式的時候,連void都可以表示。

 

4.2 列印

再次強調,為了保證效能不受影響,Btrace不允許呼叫任何例項方法。
比如不能呼叫getter方法(怕在getter裡有複雜的計算),只會通過直接反射來讀取屬性名。
又比如,除了JDK類,其他類toString時只會列印其類名+System.IdentityHashCode。
println, printArray,都按上面的規律進行,所以只能打打基本型別。

如果想列印一個Object的屬性,用printFields()來反射。

如果只想反射某個屬性,參照下面列印Port屬性的寫法。從效能考慮,應把field用靜態變數快取起來。

注意JDK類與非JDK類的區別:

import java.lang.reflect.Field;

//JDK的類這樣寫就行

private static Field fdFiled = field("java.io,FileInputStream", "fd");



//非JDK的類,要給出ClassLoader,否則ClassNotFound

private static Field portField = field(classForName("com.vip.demo.MyObject", contextClassLoader()), "port");



public static void onChannelRead(@Self Object self) {

    println("port:" + getInt(portField, self));

}

4.3.TLS,攔截函式間的通訊機制

如果要多個攔截函式之間要通訊,可以使用@TLS定義 ThreadLocal的變數來共享

@TLS

private static int port = -1;



@OnMethod(clazz = "java.net.ServerSocket", method = "<init>")

public static void onServerSocket(int p){

    port = p;

}

@OnMethod(clazz = "java.net.ServerSocket", method = "bind")

public static void onBind(){

  println("server socket at " + port);

}

  

5. 典型場景

5.1 列印慢呼叫

下例列印所有用時超過1毫秒的filter。

@OnMethod(clazz = "+com.vip.demo.Filter", method = "doFilter", location = @Location(Kind.RETURN))

public static void onDoFilter2(@ProbeClassName String pcn,  @Duration long duration) {

    if (duration > 1000000) {

        println(pcn + ",duration:" + (duration / 100000));

    }

}

  

最好能抽取了列印耗時的函式,減少程式碼重複度。

定位到某一個Filter慢了之後,可以直接用Location(Kind.CALL),進一步找出它裡面的哪一步慢了。

5.2 誰呼叫了這個函式

比如,誰呼叫了System.gc() ?

 

@OnMethod(clazz = "java.lang.System", method = "gc")

public static void onSystemGC() {

    println("entered System.gc()");

    jstack();

}

  

5.3 捕捉異常,或進入了某個特定程式碼行時,this物件及引數的值

按之前的提示,自己組合一下即可。

 

5.4 列印函式的呼叫/慢呼叫的統計資訊

如果你已經看到了這裡,那基本也不用我再囉嗦了,自己看Samples的Histogram.javaHistoOnEvent.java

可以用AtomicInteger構造計數器,然後定時(@OnTimer),或根據事件(@OnEvent)輸出結果(ctrl+c後選擇傳送事件)。

 

Btrace一個簡單的案例:http://www.it610.com/article/2158134.htm