09月 14, 2016 | Filed under 技術html
BTrace是神器,每個須要天天解決線上問題,但徹底不用BTrace的Java工程師,都是可疑的。java
BTrace的最大好處,是能夠經過本身編寫的腳本,獲取應用的一切調用信息。而不須要不斷地修改代碼,加入System.out.println(), 而後重啓,而後重啓,而後重啓應用!!!git
同時,特別嚴格的約束,保證本身的消耗特別小,只要定義腳本時不做大死,直接在生產環境打開也沒影響。github
在網上搜索BTrace出來的文章都有點舊了,並且不夠詳細,因而決定,從新寫一份。正則表達式
碼這麼多的字好辛苦,請保留原文連接:http://calvin1978.blogcn.com/articles/btrace1.htmlapi
BTrace搬家了!! 已經搬離了Sun,搬到了http://github.com/btraceio/btrace,目前的版本已是1.38。緩存
在Release頁面裏下載最新Zip版,解壓就能用,UserGuide和Samples也在裏面。運維
先抄一個UserGuide裏的例子:socket
import com.sun.btrace.annotations.*;ide
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. 誰調用了System.gc(),調用棧如何?
3. 誰構造了一個超大的ArrayList?
4. 什麼樣的入參或對象屬性,致使拋出了這個異常?或進入了這個處理分支?
爲了不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 定義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
就是HelloWorld的例子,精肯定義要監控的類與方法。
能夠用表達式,批量定義須要監控的類與方法。正則表達式須要寫在兩個 "/" 中間。
下例監控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
好比我想匹配全部的Filter類,在接口或基類的名稱前面,加個+ 就行
@OnMethod(clazz="+com.vip.demo.Filter", method="doFilter")
也能夠按類或方法上的annotaiton匹配,前面加上@就行
@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")
1. 構造函數的名字是 <init>
@OnMethod(clazz="java.net.ServerSocket", method="<init>")
2. 靜態內部類的寫法,是在類與內部類之間加上"$"
@OnMethod(clazz="com.vip.MyServer$MyInnerClass", method="hello")
3. 若是有多個同名的函數,想區分開來,能夠在攔截函數上定義不一樣的參數列表(見4.1)。
能夠爲同一個函數的不一樣的Location,分別定義多個攔截函數。
@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 纔是毫秒。
異常拋出(Throw),異常被捕獲(Catch),異常沒被捕獲被拋出函數以外(Error),主要用於對某些異常狀況的跟蹤。
在攔截函數的參數定義裏注入一個Throwable的參數,表明異常。
@OnMethod(clazz = "java.net.ServerSocket", method = "bind", location = @Location(Kind.ERROR))
public static void onBind(Throwable exception, @Duration long duration)
下例定義監控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 得到的當前行數。此時會顯示函數裏完整的執行路徑,但確定又很是慢。
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均可以表示。
再次強調,爲了保證性能不受影響,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));
}
若是要多個攔截函數之間要通訊,可使用@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);
}
下例打印全部用時超過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),進一步找出它裏面的哪一步慢了。
好比,誰調用了System.gc() ?
@OnMethod(clazz = "java.lang.System", method = "gc")
public static void onSystemGC() {
println("entered System.gc()");
jstack();
}
按以前的提示,本身組合一下便可。
若是你已經看到了這裏,那基本也不用我再囉嗦了,本身看Samples的Histogram.java,HistoOnEvent.java
能夠用AtomicInteger構造計數器,而後定時(@OnTimer),或根據事件(@OnEvent)輸出結果(ctrl+c後選擇發送事件)。