認識一下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.java, HistoOnEvent.java
可以用AtomicInteger構造計數器,然後定時(@OnTimer),或根據事件(@OnEvent)輸出結果(ctrl+c後選擇傳送事件)。
Btrace一個簡單的案例:http://www.it610.com/article/2158134.htm