BTrace是神器,每一個需要每天解決線上問題,但完全不用BTrace的Java工程師,都是可疑的。
BTrace的最大好處,是可以通過自己編寫的腳本,獲取應用的一切調用信息。而不需要不斷地修改代碼,加入System.out.println(), 然后重啟,然后重啟,然后重啟應用!!!
同時,特別嚴格的約束,保證自己的消耗特別小,只要定義腳本時不作大死,直接在生產環境打開也沒影響。
在網上搜索BTrace出來的文章都有點舊了,而且不夠詳細,於是決定,重新寫一份。
碼這么多的字好辛苦,請保留原文鏈接:http://calvin1978.blogcn.com/articles/btrace1.html
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 Stringmethod, @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后選擇發送事件)。
發現自己還是喜歡這些不漂亮的照片。


