Java-debug-tool

Java-debug-tool

微信公眾號:一字馬胡

Java-debug-tool解決什么問題

java-debug-tool

Java-debug-tool是為了解決日常問題排查的痛點而設(shè)計的,問題排查分成兩個主要階段,問題定位和問題修復(fù),問題定位是說找到問題的原因,問題修復(fù)是說將問題解決,使得系統(tǒng)恢復(fù)正常運行。
對于問題定位來說,我們的需求是:

  • 能知道方法入?yún)⒑头祷刂担蛘邟伋龅漠惓P畔?/li>
  • 當(dāng)方法有多個出口的時候,可以知道方法是從什么地方退出的,或者是從什么地方拋出異常的;
  • 一次方法調(diào)用的執(zhí)行路徑是怎么樣的,每一行代碼的耗時又是多少;
  • 獲取到方法執(zhí)行過程中的局部變量信息;

本質(zhì)上,問題定位的需求是實現(xiàn)單步調(diào)試,因為這樣是最容易發(fā)現(xiàn)問題出在什么地方的,但是對于java來說,單步調(diào)試技術(shù)會停頓整個JVM,所以只能在測試的時候使用這種技術(shù),對于生產(chǎn)環(huán)境來說就不能使用了,所以對于線上問題排查來說,基本可以不用考慮單步調(diào)試,但是如果集群有流量摘除等功能的話,倒是可以使用;java-debug-tool解決了這個問題,可以模擬單步調(diào)試的同時不會停頓正在運,使用行的JVM,下面會介紹Java-debug-tool到底實現(xiàn)了一些什么功能。

找到了問題出現(xiàn)的原因,接著就是問題修復(fù),問題修復(fù)最大的痛點其實是恢復(fù)生產(chǎn),對于java來說,恢復(fù)生產(chǎn)意味著需要重啟JVM,這樣就會造成問題修復(fù)時間變長,Java-debug-tool為此提供了技術(shù)支持Java Instrumentation技術(shù),可以在運行時的JVM中替換類的字節(jié)碼,實現(xiàn)熱修復(fù)。


Java-debug-tool不能解決什么問題

  • (1)如果需要做性能優(yōu)化分析,Java-debug-tool可能支持的力度很小,雖然可以通過Java-debug-tool觀察到每一行代碼的執(zhí)行耗時,但是也僅僅是觀察,所以性能問題還是需要其他專業(yè)的工具來進行;
  • (2)非JVM自身問題,比如機器CPU、磁盤I/O等問題,Java-debug-tool就無能為力了,Java-debug-tool專注于解決JVM自身的問題;
  • (3)Java-debug-tool僅支持方法級別的觀察,無法觀察到整體的調(diào)用鏈路,后續(xù)可能會支持多級方法鏈路的觀察,但可能性不大,因為要支持這種方法間調(diào)用鏈路追蹤,就得增強多個方法,而增強方法是對運行時有一定損耗的,如果一個方法調(diào)用鏈路特別長(對于java來說一般調(diào)用鏈路都很長),那么就悲劇了;
  • (4)Java-debug-tool不支持遞歸方法的觀察,這個功能實現(xiàn)起來也是非常麻煩,而且極其不可控,所以千萬不要用Java-debug-tool去觀察一個遞歸方法,切記;

如何使用

使用

首先需要下載安裝腳本:

wget https://github.com/pandening/storm-ml/releases/download/6.0/javadebug-tool-install.sh

之后執(zhí)行:

sh javadebug-tool-install.sh 

如果看到屏幕輸出:

welcome to use java-debug-tool

就說明安裝成功了,可以使用工具了!

開發(fā)

Java-debug-tool使用Java開發(fā),下面介紹如何使用Java-debug-tool進行問題排查;

  • (1)下載Java-debug-tool代碼;
  • (2)進入script目錄,執(zhí)行javadebug-pack.sh腳本執(zhí)行編譯打包,要求JDK 1.8 +,并且一定要執(zhí)行javadebug-pack.sh腳本之后再使用;
  • (3)如果是Spring項目,則只需要將下面的bean配置到項目中即可實現(xiàn)JVM啟動之后Java-debug-tool Agent自動attach到目標(biāo)JVM上的功能,如果不是Spring項目,請看(4)
    <!-- dynamic debug bean -->
    <bean id = "javaDebugInitializer" class="io.javadebug.spring.JavaDebugInitializer" factory-method="initializer" destroy-method="destroy" lazy-init="false"/>
  • (4)Java-debug-tool不要求在目標(biāo)JVM啟動的時候就必須attach到目標(biāo)JVM上,可以動態(tài)attach,在目錄 /bin下有多個可用的腳本,方便用于動態(tài)attach到目標(biāo)JVM上,無論如何,你都需要首先知道目標(biāo)JVM的進程id,然后執(zhí)行一個腳本就可以動態(tài)attach到目標(biāo)JVM上:
./javadebug-agent-launch.sh PID

這樣就可以在目標(biāo)JVM上啟動一個tcp服務(wù),默認(rèn)地址為:127.0.0.1:11234,如果你想要指定其他的地址,可以使用下面的命令:

./javadebug-agent-launch.sh PID@IP:PORT

之后就可以在IP:PORT啟動tcpServer,attach到目標(biāo)JVM上之后,就可以連接目標(biāo)JVM進行動態(tài)調(diào)試了,連接到目標(biāo)JVM只需要執(zhí)行下面的命令即可:

 ./javadebug-client-launch.sh

默認(rèn)就是連接 127.0.0.1:11234,如果attach目標(biāo)JVM的時候指定的地址不是這個,需要顯示指定地址:

 ./javadebug-client-launch.sh IP:PORT


命令詳解

Java-debug-tool目前支持的命令不多,下面分別介紹一下當(dāng)前支持的核心命令。首先介紹一下命令輸出界面信息介紹:

---------------------------------------------------------------------------------------------
命令              :mt
命令執(zhí)行Round       :1
客戶端ID           :10000
客戶端類型           :client:1
協(xié)議版本            :version:1
命令耗時            :179 (ms)
STW時間           :45 (ms)
---------------------------------------------------------------------------------------------
[ReturnTest.getIntVal] with params
[1]
[0 ms] (37)
[0 ms] (43) [startTime = 1559358148073]
[0 ms] (44) [strTag = the return/throw line test tag]
[0 ms] (45)
[0 ms] (47)
[0 ms] (51)
[3 ms] (52) [paramModel = 1.1]
[0 ms] (53)
return value:[101]  at line:53 with cost:5 ms

---------------------------------------------------------------------------------------------

每個輸出字段都介紹一下:

字段 含義
命令 本次輸出執(zhí)行的命令是什么 就是你輸入的命令名稱
命令執(zhí)行Round 這個調(diào)試客戶端和目標(biāo)JVM交互了幾次 交互次數(shù)
客戶端ID 每個客戶端首次連接服務(wù)端都會被分配一個ContextId,后續(xù)的交互都需要將這個ID帶上 唯一ID
客戶端類型 這是一個保留字段,Java-debug-tool認(rèn)為第一個連接到目標(biāo)JVM的調(diào)試客戶端應(yīng)該是一個Master Client,權(quán)限最高
協(xié)議版本 防偽,只有是從服務(wù)端拿到的協(xié)議才能繼續(xù)交互
命令耗時 命令的執(zhí)行耗時,從命令輸入處理開始計算,到命令結(jié)果展示出來結(jié)束,所以是客戶端耗時 + 服務(wù)端耗時
STW時間 動態(tài)增強字節(jié)碼涉及到JVM字節(jié)碼替換,會造成STW,這個時間就記錄到底STW了多長時間,這個時間會比實際STW的時間長,只是一個粗略的時間 如果一個方法被一個client增強過了,后續(xù)的client就不能增強了,除非增強該方法的client退出,其他client才能繼續(xù)增強;同時,一個client增強過的方法,其他client可以共享

接著就是具體方法的執(zhí)行路徑信息,比如上面這個例子,說明本次觀察的方法執(zhí)行是 "ReturnTest.getIntVal",方法入?yún)⑹?,方法執(zhí)行路徑是37-43-44-45-47-51-52-53,最終從53行退出,其中第52行耗時3ms,其他行耗時小于1ms,所以無法收集到,最終方法的執(zhí)行結(jié)果是101,本次方法耗時5ms,并且可以看到43、44、52行都有變量賦值信息,格式為 varName = varVal.toString(),需要注意的是,varName可能是錯誤的,但是varVal是正確的,如果有多個,按照賦值順序展示;這是方法正常返回的結(jié)果展示,下面看一個方法拋出異常的結(jié)果展示:

---------------------------------------------------------------------------------------------
命令              :mt
命令執(zhí)行Round       :1
客戶端ID           :10001
客戶端類型           :client:0
協(xié)議版本            :version:1
命令耗時            :75 (ms)
STW時間           :0 (ms)
---------------------------------------------------------------------------------------------
[ReturnTest.getIntVal] with params
[7]
[0 ms] (37)
[0 ms] (43) [startTime = 1559358921527]
[0 ms] (44) [strTag = the return/throw line test tag]
[0 ms] (45)
[0 ms] (47)
[0 ms] (51)
[0 ms] (54)
[0 ms] (59)
[0 ms] (73) [paramModel = ParamModel{intVal=0, doubleVal='0.0'}]
[0 ms] (74)
[0 ms] (75)
[0 ms] (76) [subVal = 200]
[0 ms] (78)
[0 ms] (82)
throw exception:[java.lang.IllegalStateException: error occ with in:7]  at line:82 with cost:0 ms

---------------------------------------------------------------------------------------------

可以看到本次方法執(zhí)行路徑,參數(shù)為7,在82行拋出了異常,其他信息和正常返回時類似,就不做過多解釋了。

methodTrace命令

就像命令名稱一樣,這個命令是用于觀察方法執(zhí)行路徑的,可以使用mt來替代命令,該命令參數(shù)較多,但是大部分都是可選的,下面先介紹每一個參數(shù)的含義,然后再介紹如何實現(xiàn)具體的功能。

命令基本格式:
mt -c <class> -m <method>

可選參數(shù):

  • -d :如果目標(biāo)類中的目標(biāo)方法是重載方法,那么你需要提供這個參數(shù),比如int a(int a) => desc = "(I)I";

  • -t:選擇具體的功能類型,可選項為:

    • return:當(dāng)方法正常退出的時候,獲取到一次方法鏈路信息;
    • throw:方方法拋出異常的時候,獲取到一次方法鏈路信息;
    • record:記錄方法調(diào)用信息,用于回放流量;
    • custom:用于實現(xiàn)用戶自己輸入?yún)?shù)觀察,或者回放record的流量進行觀察,當(dāng)然,如果只是想發(fā)生一次請求也是可以的;
    • watch:等待特定的參數(shù),使用Spring表達式進行參數(shù)匹配,當(dāng)匹配到目標(biāo)參數(shù)之后,會返回方法鏈路信息,如果Spring表達式有誤,那么會直接在第一次方法調(diào)用之后返回;
  • -i:用于接收用戶的參數(shù)輸入,比如當(dāng)t=custom的時候,i參數(shù)就是用戶指定的參數(shù),這個參數(shù)是通過特殊處理的json字符串,java-debug-tool將提供工具接口來生成這個字符串,當(dāng)t=watch的時候,i參數(shù)就是用于匹配參數(shù)的Spring表達式。

  • -n:當(dāng)t=record的時候,n參數(shù)的含義就是需要錄制的流量數(shù)量,當(dāng)前僅允許錄制10個以內(nèi);

  • -time:當(dāng)t=record的時候,該參數(shù)的含義是錄制的時間限制,超出則停止錄制;

  • -u:當(dāng)t=custom的時候,如果提供了u參數(shù),那么i參數(shù)將被忽略,u代表record的流量下標(biāo),從0開始,如果u參數(shù)獲取到了具體的流量,那么本次custom輸入的參數(shù)就會從u參數(shù)取出來的流量中拿到參數(shù),如果t=record,并且u參數(shù)合法,那么就不會進行錄制,而是會從錄制好的流量中取出代表u下標(biāo)的流量,用戶可以查看具體的流量信息(包括該流量的方法鏈路);

  • -e:如果t=throw,那么如果-e內(nèi)容合法,那么該參數(shù)就代表需要等待的目標(biāo)異常,如果參數(shù)不合法,只要遇到一個異常,本次觀察就會結(jié)束;當(dāng)t=custom的時候,該參數(shù)用于匹配自定義輸入,也就是說,如果你希望觀察自定義輸入的執(zhí)行路徑,你需要在custom類型下指定-e參數(shù),內(nèi)容是用于匹配輸入的Spring表達式;

  • -s:有些情況下,你可能只需要看方法調(diào)用的路徑,不需要耗時信息,或者不需要變量信息,那么這個參數(shù)有很有用,因為可能有些變量很長,展示出來很難看,而有些時候你只需要看看方法到底是從哪里退出來的,這個參數(shù)有很有幫助??梢允?line"/"cost"中的一個,前者表示只需要給我方法鏈路信息,后者其實是"line" + "cost";

  • -l:這個參數(shù)很有用,當(dāng)某個方法很長,那么鏈路追蹤信息打印出來會很難看,你可能只關(guān)心某一行的相關(guān)信息,比如就想看看某一行的代碼執(zhí)行耗時,以及這一行相關(guān)的變量信息,那么這個參數(shù)就可以派上用場,值就是具體的行號(對照源碼);

下面根據(jù)上面的參數(shù)來實現(xiàn)不同的觀察功能,首先是用于測試的Java類:

public class ReturnTest {

    private TestClass testClass = new TestClass();

    public static void say(int a) {
        int b = a * 10;
        System.out.println("hello:" + b);
        //return b;
    }

    public int getIntVal(int in) {
//        if (in < 7) {
//            System.out.println("in < 7, return");
//            throw new UnsupportedOperationException("test");
//        }

        if (in == 5) {
            String msg = null;
            // produce npe
            in += msg.length();
        }

        long startTime = System.currentTimeMillis() + fibonacci(2);
        String strTag = "the return/throw line test tag";
        if (in < 0) {
            return strTag.charAt(0);
        } else if (in == 0) {
            return 1000;
        }
        // > 0
        if (in < 2) {
            double dbVal = 1.1;
            return (int) (dbVal + 100);
        } else if (in == 2) {
            float fVal = 1.2f;
            return (int) (fVal + 200);
        }
        // > 2
        if (in % 2 == 0) {
            Random random = new Random();
            int rdm = random.nextInt(100);
            if (rdm >= 50) {
                throw new IllegalArgumentException("npe test");
            } else if (rdm <= 20) {
                throw new NullPointerException("< 20");
            }
            // end time
            long end = System.currentTimeMillis();
            long cost = startTime - end;
            int ret = testClass.test(in);
            return (int) (rdm * 10 + ret + (cost / 1000));
        } else {
            ParamModel paramModel = new ParamModel();
            paramModel.setIntVal(in);
            paramModel.setDoubleVal(1.0 * in);
            int subVal = getSubIntVal(paramModel);

            if (subVal == 100) {
                throw new IllegalArgumentException("err occ with in:" + subVal);
            }

            throw new IllegalStateException("error occ with in:" + in);
        }
    }

    /**
     *  不支持遞歸函數(shù)
     *
     * @param n
     * @return
     */
    public int fibonacci(int n) {
        if (n < 0) {
            return -1;
        }
        if (n == 0) {
            return 0;
        }
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    public int getSubIntVal(ParamModel paramModel) {
        if (paramModel == null) {
            return -1;
        }
        if (paramModel.getIntVal() <= 0) {
            return (int) paramModel.getDoubleVal();
        } else if (paramModel.getIntVal() <= 5) {
            return 100;
        } else if (paramModel.getIntVal() <= 8) {
            return 200;
        } else {
            throw new RuntimeException("ill");
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            private Random random = new Random();
            private ReturnTest returnTest = new ReturnTest();

            @Override
            public void run() {
                while (true) {
                    try {
                        System.err.println(returnTest.getIntVal(random.nextInt(10)));
                        TimeUnit.MILLISECONDS.sleep(5);
                    } catch (Exception e) {
                        //e.printStackTrace();
                        //System.out.println("error:" + e.getMessage());
                    }
                }
            }
        }).start();
    }
}

public class TestClass {

    Aa aa = new Aa();

    public int test(int in) {

        if (in == 5) {
            return 100;
        }
        String tag = "the in:" + in;
        if (in < 5) {
            in += 2;
        } else {
            in -= 1;
        }

        if (in > 5) {
            throw new IllegalArgumentException("must <= 5");
        }
        if (in <= 3) {
            throw new NullPointerException("must >= 3");
        }

        return in * 100;
    }

}
  • (1)觀察一次方法調(diào)用的執(zhí)行路徑
觀察一次方法調(diào)用執(zhí)行路徑

上面的圖片展示了觀察一次 "ReturnTest.getIntVal"方法調(diào)用的執(zhí)行路徑,本次方法入?yún)⑹?,返回值是201,是從56行代碼退出的,耗時1ms;

  • (2)在(1)中只要方法被調(diào)用一次,那么觀察就會立刻結(jié)束,所以觀察結(jié)果可能是方法正常結(jié)束,也可能是拋出了異常,如果只是希望觀察方法正常退出,那么就可以指定-t參數(shù)為return,這樣只有當(dāng)?shù)谝淮畏椒ú粧伋霎惓M顺霾艜Y(jié)束觀察;

  • (3)和(2)相反的是,如果你希望監(jiān)控一個異常的執(zhí)行路徑,比如一個方法執(zhí)行偶爾會拋出某種異常,搞得你很摸不著頭腦,那你就可以指定-t參數(shù)為throw來觀察拋出異常的執(zhí)行路徑:

觀察方法異常退出執(zhí)行路徑

當(dāng)然,如果你想要觀察的是某種特定的異常,可以指定-e參數(shù):

觀察指定的異常
  • (4)自定義輸入?yún)?shù)進行方法調(diào)用,并進行方法執(zhí)行路徑觀察:需要注意的是,在執(zhí)行自定義參數(shù)調(diào)用之前,Java-debug-tool需要獲取到目標(biāo)對象,或者目標(biāo)方法是一個靜態(tài)方法,否則Java-debug-tool命令會一直等待獲取目標(biāo)對象(不會主動創(chuàng)建目標(biāo)對象)
觀察特定輸入
  • (5)記錄方法調(diào)用請求
錄制方法請求

錄制完成后可以回放請求:

觀察回放請求
  • (6)觀察特定輸入執(zhí)行路徑
觀察特定參數(shù)-1
觀察特定參數(shù)-2

redefineClass命令

redefineClass命令用于替換一個類的字節(jié)碼,可以簡寫成rdf,用于快速恢復(fù)生產(chǎn)環(huán)境,命令的參數(shù)沒有mt命令復(fù)雜,但是需要有幾點需要注意:

  • 一個client對一個類執(zhí)行rdf命令,就會鎖定這個類,其他client就不能對相同的類執(zhí)行rdf,直至client退出;

我們把getIntVal方法的開始部分的注釋去掉,也就是:

//        if (in < 7) {
//            System.out.println("in < 7, return");
//            throw new UnsupportedOperationException("test");
//        }

這一段內(nèi)容,去掉之后,只要輸入的參數(shù)小于7,那么就會拋出異常,我們使用mt命令配合custom來驗證我們的rdf結(jié)果:

執(zhí)行rdf命令

可以看到,此時輸入?yún)?shù)為5的時候拋出了那個期望的異常;

rollback命令

rollback命令用于將一個增強過的類恢復(fù)到初始狀態(tài),可以使用back簡寫,目前僅支持恢復(fù)到初始狀態(tài),后續(xù)會記錄增強stage,然后恢復(fù)到上一次增強過的字節(jié)碼:

回滾一個類

findClass命令

是不是曾經(jīng)出現(xiàn)過因為jar包沖突導(dǎo)致的類加載錯誤的情況呢?findClass命令用于快速判斷一個類是不是在目標(biāo)JVM加載了,如果加載了,是從哪個jar包中加載的(jar一般都會有版本號,可以看看是不是從期望的jar版本中加載的),是被什么類加載器加載的,還可以僅僅使用類名(不含包名)來匹配,甚至通過正則表達式來匹配,可以使用簡寫fc:

查找類信息

help命令

如果你不知道怎么使用一個命令,那么可以試試help命令:

help命令

如何重復(fù)發(fā)生上一次發(fā)送的命令

有時候需要重復(fù)上一次輸入的命令,但是上一次命令輸入內(nèi)容很多,如何快速實現(xiàn)上一次命令的復(fù)制呢?下面的一些字符可以快速幫你搞定這件事情:"p","r","s","go","last"

重復(fù)命令發(fā)送

后續(xù)將支持命令歷史記錄回放的功能,目前僅支持回放上一次輸入。

規(guī)劃中的功能

  • (1)線程相關(guān)功能,包括當(dāng)前線程總數(shù)、活動線程數(shù)等等信息,并能獲取到某個線程的調(diào)用堆棧等信息;
  • (2)GC相關(guān)信息;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容