Btrace

Btrace入門到熟練小工徹底指南

09月 14, 2016 | Filed under 技術html

BTrace是神器,每個須要天天解決線上問題,但徹底不用BTrace的Java工程師,都是可疑的。java

BTrace的最大好處,是能夠經過本身編寫的腳本,獲取應用的一切調用信息。而不須要不斷地修改代碼,加入System.out.println(), 而後重啓,而後重啓,而後重啓應用!!!git

同時,特別嚴格的約束,保證本身的消耗特別小,只要定義腳本時不做大死,直接在生產環境打開也沒影響。github

在網上搜索BTrace出來的文章都有點舊了,並且不夠詳細,因而決定,從新寫一份。正則表達式

碼這麼多的字好辛苦,請保留原文連接:http://calvin1978.blogcn.com/articles/btrace1.htmlapi

 

1. 概述

1.1 快速開始

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 典型的場景

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後選擇發送事件)。

相關文章
相關標籤/搜索