BTrace:Java 線上問(wèn)題排查神器

BTrace 是什么

BTrace 是檢查和解決線上的問(wèn)題的殺器贷帮,BTrace 可以通過(guò)編寫(xiě)腳本的方式,獲取程序執(zhí)行過(guò)程中的一切信息柒瓣,并且儒搭,注意了吭狡,不用重啟服務(wù)玫氢,是的逆趣,不用重啟服務(wù)汉操。寫(xiě)好腳本昧谊,直接用命令執(zhí)行即可创倔,不用動(dòng)原程序的代碼涯冠。

原理

總體來(lái)說(shuō)磁滚,BTrace 是基于動(dòng)態(tài)字節(jié)碼修改技術(shù)(Hotswap)來(lái)實(shí)現(xiàn)運(yùn)行時(shí) java 程序的跟蹤和替換拣挪。大體的原理可以用下面的公式描述:
Client(Java compile api + attach api) + Agent(腳本解析引擎 + ASM + JDK6 Instumentation) + Socket
其實(shí) BTrace 就是使用了 java attach api 附加 agent.jar 擦酌,然后使用腳本解析引擎+asm來(lái)重寫(xiě)指定類(lèi)的字節(jié)碼,再使用 instrument 實(shí)現(xiàn)對(duì)原有類(lèi)的替換菠劝。

安裝和配置

本次安裝和配置在 Linux Ubuntu 14.04 下進(jìn)行赊舶。目前 BTrace 的最新版本為 1.3.9,代碼托管在 [github] 上赶诊。
第一步笼平,在github 上下載 releases 版 btrace-bin-1.3.9.tgz,zip 版的沒(méi)有 build 目錄舔痪。
第二步寓调,解壓 btrace-bin-1.3.9.tgz 到一個(gè)目錄即可,例如 /home/fengzheng/soft/btrace , 到這一步其實(shí)就可以用了锄码,只是執(zhí)行腳本的時(shí)候需要在 btrace 命令前加上絕對(duì)路徑夺英,如果想在任意目錄可執(zhí)行晌涕,進(jìn)行下一步
第三步,配置環(huán)境變量秋麸,配置的環(huán)境變量包括 JAVA_HOMEBTRACE_HOME 渐排,例如我的配置如下:

export JAVA_HOME=/home/fengzheng/soft/jdk1.8.0_111
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib  
export PATH=${JAVA_HOME}/bin:$PATH
export BTRACE_HOME=/home/fengzheng/soft/btrace
export PATH=$PATH:$BTRACE_HOME/bin

之后執(zhí)行命令 source /etc/profile ,使環(huán)境變量立即生效灸蟆。接下來(lái)在任意目錄執(zhí)行 btrace命令驯耻,都可以執(zhí)行成功了。

簡(jiǎn)單測(cè)試用例

btrace 最簡(jiǎn)單的語(yǔ)法是 btrace $pid script.java炒考,所以需要知道要探測(cè)的 Java程序的進(jìn)程id可缚,然后編寫(xiě)一個(gè)探測(cè)腳本即可。

  1. 寫(xiě)一個(gè)常駐內(nèi)存的 Java 程序斋枢,這里寫(xiě)了一個(gè)無(wú)限循環(huán)帘靡,每隔5秒鐘輸出一組計(jì)算結(jié)果,內(nèi)容如下:
package kite.lab.utils;

/**
 * NumberUtil
 *
 * @author fengzheng
 * @date 2017/2/15
 */
public class NumberUtil {

    public int sum(){
        int result = 0;
        for(int i = 0; i< 100; i++){
            result += i * i;
        }
        return result;
    }

    public static void main(String[] args){
        while (true) {
            Thread.currentThread().setName("計(jì)算");
            NumberUtil util = new NumberUtil();
            int result = util.sum();
            System.out.println(result);
            try {
                Thread.sleep(5000);
            }catch (InterruptedException e){

            }
        }
    }
}

順便說(shuō)一下命令行編譯和運(yùn)行 Java 的過(guò)程:
編譯javac -d . NumberUtil.java瓤帚,定位到 NumberUtil.java 所在目錄描姚,然后執(zhí)行此命令行,將會(huì)在當(dāng)前目錄(.表示當(dāng)前目錄)生成包名所示的目錄結(jié)構(gòu)戈次,kite/lab/utils/NumberUtil.class
執(zhí)行java kite.lab.utils.NumberUtil 即可

  1. 執(zhí)行上面的程序后轩勘,可用 jps 命令查看 pid(一般情況下用哪個(gè)賬號(hào)啟動(dòng)的程序,就要用哪個(gè)賬號(hào)執(zhí)行 jps 怯邪,root 賬號(hào)除外)绊寻,執(zhí)行 jps 命令看到如下結(jié)果:
root@ubuntu:/home/fengzheng/codes/btrace# jps
10906 Jps
10860 NumberUtil
  1. 可以看到剛剛執(zhí)行的 java 進(jìn)程為 10860

  2. 編寫(xiě) btrace 腳本,腳本內(nèi)容簡(jiǎn)單如下:

package kite;

import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.Strings.strcat;
import static com.sun.btrace.BTraceUtils.jstack;
import static com.sun.btrace.BTraceUtils.println;
import static com.sun.btrace.BTraceUtils.str;

/**
 * NumberUtilBTrace
 *
 * @author fengzheng
 * @date 2017/6/20
 */
@BTrace
public class NumberUtilBTrace {

    @OnMethod(
            clazz="kite.lab.utils.NumberUtil",
            method="sum",
            location=@Location(Kind.RETURN)
    )
    public static void func(@Return int result) {
        println("trace: =======================");
        println(strcat("result:", str(result)));
        jstack();
    }
}

意思是在執(zhí)行結(jié)束后(location=@Location(Kind.RETURN) 表示執(zhí)行結(jié)束)輸出結(jié)果和堆棧信息

  1. 預(yù)編譯:執(zhí)行之前可以用預(yù)編譯命令檢查腳本的正確性悬秉,預(yù)編譯命令為 btracec澄步,它是一個(gè) javac-like 命令,btracec NumberUtilBTrace.java

  2. 調(diào)用命令行執(zhí)行和泌,btrace 10860 NumberUtilBTrace.java 村缸,(如果要保存到本地文件中,可以使用轉(zhuǎn)向命令 btrace 10860 NumberUtilBTrace.java > mylog.log )打印的信息如下:

trace: =======================
result:328350
kite.lab.utils.NumberUtil.sum(NumberUtil.java:16)
kite.lab.utils.NumberUtil.main(NumberUtil.java:27)
  1. 按ctrl + c 允跑,會(huì)給出退出提示王凑,再按 1 退出

使用場(chǎng)景

BTrace 是一個(gè)事后工具,所謂事后工具就是在服務(wù)已經(jīng)上線了聋丝,但是發(fā)現(xiàn)存在以下問(wèn)題的時(shí)候,可以用 BTrace工碾。

  1. 比如哪些方法執(zhí)行太慢弱睦,例如監(jiān)控執(zhí)行時(shí)間超過(guò)1s的方法
  2. 查看哪些方法調(diào)用了 System.gc() ,調(diào)用棧是怎樣的
  3. 查看方法參數(shù)或?qū)ο髮傩?/li>
  4. 哪些方法發(fā)生了異常

多說(shuō)一點(diǎn)渊额,為了更好解決問(wèn)題况木,最好還要配合事前準(zhǔn)備和進(jìn)行中監(jiān)控垒拢,事前準(zhǔn)備就是埋點(diǎn)嘛,在一些可能出現(xiàn)問(wèn)題的方法中進(jìn)行日志輸出火惊,進(jìn)行中監(jiān)控就是利用一些實(shí)時(shí)監(jiān)控工具求类,例如 VisualVM 、jmc 這些帶界面的工具或者 jdk 提供的命令行工具等屹耐,再高級(jí)一點(diǎn)的就是利用 Graphite 這樣的Metrics 工具配合 web 界面展示出來(lái)尸疆。

使用限制

為了保證trace語(yǔ)句只讀,最小化對(duì)被檢測(cè)程序造成影響, BTrace對(duì)trace腳本有一些限制(比如不能改變被trace代碼中的狀態(tài))

  • BTrace class不能新建類(lèi), 新建數(shù)組, 拋異常, 捕獲異常,
  • 不能調(diào)用實(shí)例方法以及靜態(tài)方法(com.sun.btrace.BTraceUtils除外)
  • 不能將目標(biāo)程序和對(duì)象賦值給BTrace的實(shí)例和靜態(tài)field
  • 不能定義外部, 內(nèi)部, 匿名, 本地類(lèi)
  • 不能有同步塊和方法
  • 不能有循環(huán)
  • 不能實(shí)現(xiàn)接口, 不能擴(kuò)展類(lèi)
  • 不能使用assert語(yǔ)句, 不能使用class字面值

攔截方法定義

@OnMethod 可以指定 clazz 惶岭、method寿弱、location。由此組成了在什么時(shí)機(jī)(location 決定)監(jiān)控某個(gè)類(lèi)/某些類(lèi)(clazz 決定)下的某個(gè)方法/某些方法(method 決定)按灶。

如何定位

  1. 精準(zhǔn)定位

    直接定位到一個(gè)類(lèi)下的一個(gè)方法症革,上面測(cè)試用的例子就是

  2. 正則表達(dá)式定位

    正則表達(dá)式在兩個(gè)"/" 之間,例如下面的例子,監(jiān)控 javax.swing 包下的所有方法鸯旁,注意正式環(huán)境中噪矛,范圍盡可能小一點(diǎn),太大了性能會(huì)有影響铺罢。

    @OnMethod(clazz="/javax\\.swing\\..*/", method="/.*/")
    public static void swingMethods( @ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
       print("entered " + probeClass + "."  + probeMethod);
    }
    

    通過(guò)在攔截函數(shù)的定義里注入@ProbeClassName String probeClass, @ProbeMethodName String probeMethod 參數(shù)艇挨,告訴腳本實(shí)際匹配到的類(lèi)和方法名。

  3. 按接口或繼承類(lèi)定位

    例如要匹配繼承或?qū)崿F(xiàn)了 com.kite.base 的接口或基類(lèi)的畏铆,只要在類(lèi)前加上 + 號(hào)就可以了雷袋,例如

    @OnMethod(clazz="+com.kite.base", method="doSome")

  4. 按注解定位

    在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")

攔截時(shí)機(jī)

攔截時(shí)機(jī)由 location 決定辞居,當(dāng)然也可為同一個(gè)定位加入多個(gè)攔截時(shí)機(jī)楷怒,即可以在進(jìn)入方法時(shí)攔截、方法返回時(shí)攔截瓦灶、拋出異常時(shí)攔截

  1. Kind.Entry與Kind.Return

    分別表示函數(shù)的開(kāi)始和返回鸠删,不寫(xiě) location 的情況下,默認(rèn)為 Kind.Entry,僅獲取參數(shù)值贼陶,可以用 Kind.Entry 刃泡,要獲取返回值或執(zhí)行時(shí)間就要用 Kind.Return

  2. Kind.Error, Kind.Throw和 Kind.Catch

    表示異常被 throw 、異常被捕獲還有異常發(fā)生但是沒(méi)有被捕獲的情況碉怔,在攔截函數(shù)的參數(shù)定義里注入一個(gè)Throwable的參數(shù)烘贴,代表異常

    @OnMethod(clazz = "java.net.ServerSocket", method = "bind", location =@Location(Kind.ERROR))
    
    public static void onBind(Throwable exception, @Duration long duration){
      
    }
    
  3. Kind.Call 和 Kind.Line

    Kind.Call 表示被監(jiān)控的方法調(diào)用了哪些其他方法,例如:

    @OnMethod(clazz = "com.kite",
                method = "login",
                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){
            println(strcat("self: ", str(self)));
            println(strcat("instance: ", str(instance)));
            println(strcat("method: ", str(method)));
            println(strcat("duration(ms): ", str(duration / 1000000)));
        }
    

    @Self 表示當(dāng)前監(jiān)控的函數(shù)所在類(lèi)撮胧,如果是靜態(tài)類(lèi)則為空桨踪,@TargetInstance 表示函數(shù)中調(diào)用的方法或?qū)傩运诘念?lèi),如果是靜態(tài)方法則為空芹啥,@TargetMethodOrField 表示調(diào)用的方法或?qū)傩远屠耄绻@取執(zhí)行時(shí)間铺峭,那么 where 必須設(shè)置為 Where.AFTER

    Kind.Line 監(jiān)測(cè)類(lèi)是否執(zhí)行到了設(shè)置的行數(shù),例如:

    @OnMethod(clazz = "com.kite.demo", location = @Location(value = Kind.LINE, line = 20))
    public static void onBind() {
    
       println("執(zhí)行到第20行");
    
    }
    

幾個(gè)例子

查看誰(shuí)調(diào)用了GC

@OnMethod(clazz = "java.lang.System", method = "gc")
   public static void onSystemGC() {
       println("entered System.gc()");
       jstack();
   }

打印耗時(shí)超過(guò)100ms的方法

 @OnMethod(clazz = "/com\\.kite\\.controller\\..*/",method = "/.*/",location = @Location(Kind.RETURN))
    public static void slowQuery(@ProbeClassName String pcn,@ProbeMethodName String probeMethod, @Duration long duration){
        if(duration > 1000000 * 100){
            println(strcat("類(lèi):", pcn));
            println(strcat("方法:", probeMethod));
            println(strcat("時(shí)長(zhǎng):", str(duration / 1000000)));
        }
    }

BTrace 提供了一系列的 sample, 可到 github 上查看汽纠。

注意問(wèn)題

如果出現(xiàn) Unable to open socket file: target process not responding or HotSpot VM not loaded 這個(gè)問(wèn)題卫键,可能的原因是執(zhí)行 BTrace 腳本的用戶(hù)和 Java 進(jìn)程運(yùn)行的用戶(hù)不是同一個(gè),使用 ps -aux | grep $pid查看一下 Java 進(jìn)程的執(zhí)行用戶(hù)虱朵,保證和 BTrace 腳本執(zhí)行用戶(hù)相同即可莉炉。

歡迎關(guān)注微信公眾號(hào):gushidefengzheng

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市卧秘,隨后出現(xiàn)的幾起案子呢袱,更是在濱河造成了極大的恐慌,老刑警劉巖翅敌,帶你破解...
    沈念sama閱讀 212,686評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件羞福,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蚯涮,警方通過(guò)查閱死者的電腦和手機(jī)治专,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)遭顶,“玉大人张峰,你說(shuō)我怎么就攤上這事“羝欤” “怎么了喘批?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,160評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)铣揉。 經(jīng)常有香客問(wèn)我饶深,道長(zhǎng),這世上最難降的妖魔是什么逛拱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,736評(píng)論 1 284
  • 正文 為了忘掉前任敌厘,我火速辦了婚禮,結(jié)果婚禮上朽合,老公的妹妹穿的比我還像新娘俱两。我一直安慰自己,他們只是感情好曹步,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,847評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布宪彩。 她就那樣靜靜地躺著,像睡著了一般讲婚。 火紅的嫁衣襯著肌膚如雪毯焕。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,043評(píng)論 1 291
  • 那天磺樱,我揣著相機(jī)與錄音纳猫,去河邊找鬼。 笑死竹捉,一個(gè)胖子當(dāng)著我的面吹牛芜辕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播块差,決...
    沈念sama閱讀 39,129評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼侵续,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了憨闰?” 一聲冷哼從身側(cè)響起状蜗,我...
    開(kāi)封第一講書(shū)人閱讀 37,872評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鹉动,沒(méi)想到半個(gè)月后轧坎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,318評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泽示,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,645評(píng)論 2 327
  • 正文 我和宋清朗相戀三年缸血,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片械筛。...
    茶點(diǎn)故事閱讀 38,777評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捎泻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出埋哟,到底是詐尸還是另有隱情笆豁,我是刑警寧澤,帶...
    沈念sama閱讀 34,470評(píng)論 4 333
  • 正文 年R本政府宣布赤赊,位于F島的核電站闯狱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏砍鸠。R本人自食惡果不足惜扩氢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望爷辱。 院中可真熱鬧录豺,春花似錦、人聲如沸饭弓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,861評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弟断。三九已至咏花,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昏翰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,095評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工苍匆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棚菊。 一個(gè)月前我還...
    沈念sama閱讀 46,589評(píng)論 2 362
  • 正文 我出身青樓浸踩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親统求。 傳聞我的和親對(duì)象是個(gè)殘疾皇子检碗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,687評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)码邻,斷路器折剃,智...
    卡卡羅2017閱讀 134,637評(píng)論 18 139
  • 1.使用背景生產(chǎn)環(huán)境系統(tǒng)發(fā)生問(wèn)題時(shí),定位問(wèn)題需要獲取系統(tǒng)運(yùn)行時(shí)的相關(guān)數(shù)據(jù)像屋,如方法參數(shù)怕犁、返回值、全局變量开睡、堆棧信息等...
    JavaQ閱讀 16,355評(píng)論 3 13
  • 文|平之 郭哥今天結(jié)婚了篇恒。 從十二三歲知道同年級(jí)有這么個(gè)人扶檐,到高中一個(gè)班的郭班頭,再到11年夏天交大對(duì)面小炮樓十平...
    常平之閱讀 250評(píng)論 1 1
  • 殘荷凋前夏日盛胁艰,美顏無(wú)需弄脂粉款筑。 可曾蜻蜓愛(ài)初荷,露似淚水相思晨腾么。 不悔隆冬無(wú)人問(wèn)奈梳,伴與霜雪棄紅塵。 古往今來(lái)引墨...
    羽扇閑人閱讀 343評(píng)論 2 2