Java線上問題排查神器Arthas快速上手與原理淺談

後端技術漫談發表於2020-08-25

前言

當你興沖沖地開始執行自己的Java專案時,你是否遇到過如下問題:

  • 程式在穩定執行了,可是實現的功能點了沒反應。
  • 為了修復Bug而上線的新版本,上線後發現Bug依然在,卻想不通哪裡有問題?
  • 想到可能出現問題的地方,卻發現那裡沒打日誌,沒法在執行中看到問題,只能加了日誌輸出重新打包——部署——上線
  • 程式功能正常了,可是為啥響應時間這麼慢,在哪裡出現了問題?
  • 程式不但穩定執行,而且功能完美,但跑了幾天或者幾周過後,發現響應速度變慢了,是不是記憶體洩漏了?

以前,你碰到這些問題,解決的辦法大多是,修改程式碼,重新上線。但是在大公司裡,上線的流程是非常繁瑣的,如果為了多加一行日誌而重新發布版本,無疑是非常折騰人的。

現在,我們有了更為優雅的線上除錯方法,來自阿里巴巴開源的Arthas

下圖是Arthas文件中對於為什麼要使用它的描述,我進行了精簡:

好了,前言已經超過字數了,哈哈,在本篇文章裡,你能夠了解:

  • Arthas使用例項:幫助你快速讓你上手,拯救你的低效率Debug
  • 使用Arthas解決具體問題:看一下Arthas幫我拯救了多少時間
  • 相似工具:看看線上Debug還有沒有別的工具可以使用
  • 原理淺談:莫在浮沙築高閣!你需要大概瞭解下Arthas的原理

相信我,Arhas覺得是你提升效率的利器,適合各種階段的開發者,尤其適合我這種剛入門的新人(天天上班寫Bug的人)。你不應該有這種東西是高階程式設計師才應該去使用的思想,放心大膽的去用吧

(打廣告時間,更多精彩文章,請關注公眾號:後端技術漫談

線上Debug神器Arthas

Arthas使用例項

命令的詳細文件請參考:

https://alibaba.github.io/arthas/commands.html

快速啟動

快速啟動它,你只需要兩行命令:

wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

隨後,在介面出現的程式中,選擇你的程式序號,比如1

這樣你就進入了arthas的控制檯

基本使用

Arthas有如下功能:

1. 首先是我認為的“上帝視角”指令:Dashboard

當前系統的實時資料皮膚,按 ctrl+c 退出。

當執行在Ali-tomcat時,會顯示當前tomcat的實時資訊,如HTTP請求的qps, rt, 錯誤數, 執行緒池資訊等等。

通過這些,你可以對於整個程式程式有個直觀的資料監控。

2. 類載入問題相關指令

SC:檢視JVM已載入的類資訊

通過SC我們可以看到我們這個類的詳細資訊,包括是從哪個jar包讀取的,他是不是介面/列舉類等,甚至包括他是從哪個類載入器載入的。

上圖中程式碼:

[arthas@37]$ sc -d *MathGame
 class-info        demo.MathGame
 code-source       /home/scrapbook/tutorial/arthas-demo.jar
 name              demo.MathGame
 isInterface       false
 isAnnotation      false
 isEnum            false
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       MathGame
 modifier          public
 annotation
 interfaces
 super-class       +-java.lang.Object
 class-loader      +-sun.misc.Launcher$AppClassLoader@70dea4e
                     +-sun.misc.Launcher$ExtClassLoader@69260973
 classLoaderHash   70dea4e

SC也可以檢視已載入的類,幫助你看是否有沒有納入進來的類,尤其是在Spring中,可以判斷的你的依賴有沒有正確的進來。

上圖中程式碼:

# 檢視JVM已載入的類資訊
[arthas@37]$ sc javax.servlet.Filter
com.example.demo.arthas.AdminFilterConfig$AdminFilter
javax.servlet.Filter
org.apache.tomcat.websocket.server.WsFilter
org.springframework.boot.web.filter.OrderedCharacterEncodingFilter
org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter
org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter
org.springframework.boot.web.filter.OrderedRequestContextFilter
org.springframework.web.filter.CharacterEncodingFilter
org.springframework.web.filter.GenericFilterBean
org.springframework.web.filter.HiddenHttpMethodFilter
org.springframework.web.filter.HttpPutFormContentFilter
org.springframework.web.filter.OncePerRequestFilter
org.springframework.web.filter.RequestContextFilter
org.springframework.web.servlet.resource.ResourceUrlEncodingFilter
Affect(row-cnt:14) cost in 11 ms.
 
# 檢視已載入類的方法資訊
[arthas@37]$ sm java.math.RoundingMode
java.math.RoundingMode <init>(Ljava/lang/String;II)V
java.math.RoundingMode values()[Ljava/math/RoundingMode;
java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode;
java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode;
Affect(row-cnt:4) cost in 6 ms.

jad:反編譯某個類,或者反編譯某個類的某個方法

上圖中程式碼:

# 反編譯只顯示原始碼
jad --source-only com.Arthas
# 反編譯某個類的某個方法
jad --source-only com.Arthas mysql


[arthas@37]$ jad demo.MathGame

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@70dea4e
  +-sun.misc.Launcher$ExtClassLoader@69260973

Location:
/home/scrapbook/tutorial/arthas-demo.jar

/*
 * Decompiled with CFR.
 */
package demo;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class MathGame {
    private static Random random = new Random();
    public int illegalArgumentCount = 0;

    public List<Integer> primeFactors(int number) {
        if (number < 2) {
            ++this.illegalArgumentCount;
            throw new IllegalArgumentException("number is: " + number + ", need >= 2");
        }
        ArrayList<Integer> result = new ArrayList<Integer>();
        int i = 2;
        while (i <= number) {
            if (number % i == 0) {
                result.add(i);
                number /= i;
                i = 2;
                continue;
            }
            ++i;
        }
        return result;
    }

    public static void main(String[] args) throws InterruptedException {
        MathGame game = new MathGame();
        do {
            game.run();
            TimeUnit.SECONDS.sleep(1L);
        } while (true);
    }

    public void run() throws InterruptedException {
        try {
            int number = random.nextInt() / 10000;
            List<Integer> primeFactors = this.primeFactors(number);
            MathGame.print(number, primeFactors);
        }
        catch (Exception e) {
            System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
        }
    }

    public static void print(int number, List<Integer> primeFactors) {
        StringBuffer sb = new StringBuffer(number + "=");
        for (int factor : primeFactors) {
            sb.append(factor).append('*');
        }
        if (sb.charAt(sb.length() - 1) == '*') {
            sb.deleteCharAt(sb.length() - 1);
        }
        System.out.println(sb);
    }
}

Affect(row-cnt:1) cost in 760 ms.

3. 方法執行相關指令

watch:方法執行的資料觀測

你可以通過watch指令,來監控某個類,監控後,執行下你的功能,復現下場景,arthas會提供給你具體的出參和入參,幫助你排查故障

trace:輸出方法呼叫路徑,並輸出耗時

這個指令對於優化程式碼非常的有用,可以看出具體每個方法執行的時間,如果是for迴圈等重複語句,還能看出n次迴圈中的最大耗時,最小耗時,和平均耗時,完美!

tt:官方名為時空隧道

這是我除錯用的最多的指令,在你對某方法開啟tt後,會記錄下每一次的呼叫(你需要設定最大監控次數),然後你可以在任何時候會看這裡面的呼叫,包括出參,入參,執行耗時,是否異常等。非常強大。

4. 執行緒除錯相關指令

thread相關命令:

thread -n:排列出 CPU 使用率 Top N 的執行緒。

thread -b:排查阻塞的執行緒

我們程式碼有時候設計的不好,會引發死鎖的問題,卡住整個執行緒執行,使用這個指令可以輕鬆的找到問題執行緒,以及問題的執行語句。

  1. 強大的ognl表示式

眾所周知,一般來說,表示式都是除錯工具裡最強的指令,哈哈。

在Arthas中你可以利用ognl表示式語言做很多事,比如執行某個方法,獲取某個資訊,甚至進行修改。

[arthas@19856]$ ognl '@com.Arthas@hashSet'
@HashSet[
    @String[count1],
    @String[count2],
    @String[count29],
    @String[count28],
    @String[count0],
    @String[count27],
    @String[count5],
    @String[count26],
    @String[count6],
    @String[count25],
    @String[count3],
    @String[count24],
    
[arthas@19856]$ ognl  '@com.Arthas@hashSet.add("test")'
@Boolean[true]
[arthas@19856]$
# 檢視新增的字元
[arthas@19856]$ ognl  '@com.Arthas@hashSet' | grep test
    @String[test],
[arthas@19856]$

甚至你可以動態更換日誌輸出級別

$ ognl '@com.lz.test@LOGGER.logger.privateConfig'
@PrivateConfig[
    loggerConfig=@LoggerConfig[root],
    loggerConfigLevel=@Level[INFO],
    intLevel=@Integer[400],
]
$ ognl '@com.lz.test@LOGGER.logger.setLevel(@org.apache.logging.log4j.Level@ERROR)'
null
$ ognl '@com.lz.test@LOGGER.logger.privateConfig'
@PrivateConfig[
    loggerConfig=@LoggerConfig[root],
    loggerConfigLevel=@Level[ERROR],
    intLevel=@Integer[200],
  
]

使用Arthas解決具體問題

1. 響應時間異常問題

工作中遇到一個優化問題,系統中一個匯出表格的功能,響應時間長達2分鐘,雖然給內部使用,但也不能這麼誇張,用trace跟蹤下方法,發現是其中的手機號加解密函式佔用了非常大的時間,幾千個手機號,進行了解密後加密的精彩操作,最終導致了兩分鐘的返回時間。

2. 某功能Bug導致伺服器返回500

首先通過trace看異常報錯的方法,之後通過tt排查方法,發現入參進來後,居然走錯了方法(因為多型),走到了返回null的方法中,所以導致了NPE空指標錯誤。

3. tt新增表示式條件過濾請求

在一次使用tt -t時,由於線上請求太多,需要抓取一次導致錯誤的請求,必須開啟表示式過濾,這時候可以直接在tt後面新增表示式:

tt -t com.xxxxx.domain.xx.xxx.impl.LoupanAdvShowService compositeClickUrl -n 2 'params[1]==377142'

上面的句子可以將第二個引數值位377142的請求,過濾出來,注意:請使用==號,而非賦值的=號,我傻乎乎的蒙了好久才發現少寫了等於號。

4. watch新增表示式條件過濾請求

同理,watch也可以實現同樣的效果,watch com.xinfang.apps.chat.controller.ChatMsgController uploadImg "{params[1].request.getParameterMap()}" -x 3 -b -n 1

5. 補充

Arthas還支援Web Console,詳見:

https://alibaba.github.io/arthas/web-console.html

相似工具

BTrace一是個歷史比較久的工具,觀察下來Arthas其實和他的理念蠻相似的,相信Arthas也參考過Btrace,作為一個學習樣例來開發Arthas。詳細的優劣勢看圖:

其他的相似工具,還有jvm-sandbox,有興趣的朋友可以去看看。

Arthas原理淺談

分為三個部分:

  • 啟動
  • arthas服務端程式碼分析
  • arthas客戶端程式碼分析

啟動

使用了阿里開源的元件cli,對引數進行了解析

com.taobao.arthas.boot.Bootstrap

在傳入引數中沒有pid,則會呼叫本地jps命令,列出java程式

進入主邏輯,會在使用者目錄下建立.arthas目錄,同時下載arthas-core和arthas-agent等lib檔案,最後啟動客戶端和服務端

通過反射的方式來啟動字元客戶端

服務端——前置準備

看服務端啟動命令可以知道 從 arthas-core.jar開始啟動,arthas-core的pom.xml檔案裡面指定了mainClass為com.taobao.arthas.core.Arthas,使得程式啟動的時候從該類的main方法開始執行。

  • 首先解析入參,生成com.taobao.arthas.core.config.Configure類,包含了相關配置資訊
  • 使用jdk-tools裡面的VirtualMachine.loadAgent,其中第一個引數為agent路徑, 第二個引數向jar包中的agentmain()方法傳遞引數(此處為agent-core.jar包路徑和config序列化之後的字串),載入arthas-agent.jar包
  • 執行arthas-agent.jar包,指定了Agent-Class為com.taobao.arthas.agent.AgentBootstrap

上圖中程式碼:

public class Arthas {

    private Arthas(String[] args) throws Exception {
        attachAgent(parse(args));
    }

    private Configure parse(String[] args) {
        // 省略非關鍵程式碼,解析啟動引數作為配置,並填充到configure物件裡面
        return configure;
    }

    private void attachAgent(Configure configure) throws Exception {
           // 省略非關鍵程式碼,attach到目標程式
          virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
          virtualMachine.loadAgent(configure.getArthasAgent(),
                            configure.getArthasCore() + ";" + configure.toString());
    }


    public static void main(String[] args) {
            new Arthas(args);
    }
}

服務端——監聽客戶端請求

  • 如果是exit,logout,quit,jobs,fg,bg,kill等直接執行。
  • 如果是其他的命令,則建立Job,並執行。
  • 建立Job時,會根據具體客戶端傳遞的命令,找到對應的Command,幷包裝成Process, Process再被包裝成Job。
  • 執行Job時,反向先呼叫Process,再找到對應的Command,最終呼叫Command的process處理請求。

服務端——Command處理流程

  1. 不需要使用位元組碼增強的命令

其中JVM相關的使用 java.lang.management 提供的管理介面,來檢視具體的執行時資料。比較簡單,就不介紹了。

  1. 需要使用位元組碼增強的命令

位元組碼增加的命令統一繼承EnhancerCommand類,process方法裡面呼叫enhance方法進行增強。呼叫Enhancer類enhance方法,該方法內部呼叫inst.addTransformer方法新增自定義的ClassFileTransformer,這邊是Enhancer類。

Enhancer類使用AdviceWeaver(繼承ClassVisitor),用來修改類的位元組碼。重寫了visitMethod方法,在該方法裡面修改類指定的方法。visitMethod方法裡面使用了AdviceAdapter(繼承了MethodVisitor類),在onMethodEnter方法, onMethodExit方法中,把Spy類對應的方法(ON_BEFORE_METHOD, ON_RETURN_METHOD, ON_THROWS_METHOD等)編織到目標類的方法對應的位置。

在前面Spy初始化的時候可以看到,這幾個方法其實指向的是AdviceWeaver類的methodOnBegin, methodOnReturnEnd等。在這些方法裡面都會根據adviceId查詢對應的AdviceListener,並呼叫AdviceListener的對應的方法,比如before,afterReturning, afterThrowing。

客戶端

客戶端程式碼在arthas-client模組裡面,入口類是com.taobao.arthas.client.TelnetConsole。

主要使用apache commons-net jar進行telnet連線,關鍵的程式碼有下面幾步:

  1. 構造TelnetClient物件,並初始化
  2. 構造ConsoleReader物件,並初始化
  3. 呼叫IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput())處理各個流,一共有四個流:
    • telnet.getInputStream()
    • telnet.getOutputStream()
    • System.in
    • consoleReader.getOutput()

請求時:從本地System.in讀取,傳送到 telnet.getOutputStream(),即傳送給遠端服務端。
響應時:從telnet.getInputStream()讀取遠端服務端傳送過來的響應,並傳遞給 consoleReader.getOutput(),即在本地控制檯輸出。

關於原始碼,深入下去還有很多東西需要生啃,我也沒有消化得很好,大家可以繼續閱讀詳細資料。

總結

Arthas是一個線上Debug神器,小白也可以輕鬆上手。

碼字不易,希望大家捧個人場,謝謝諸位。

參考文獻

開源地址:

https://github.com/alibaba/arthas

官方文件:

https://alibaba.github.io/arthas

其他參考:

  1. Hollis:Arthas - Java 線上問題定位處理的終極利器
  2. https://www.cnblogs.com/LittleHann/p/4783581.html
  3. https://juejin.im/post/6844903765145813006#heading-29
  4. http://tech.dianwoda.com/2018/12/20/arthasyuan-ma-fen-xi/
  5. https://www.jianshu.com/p/0771646f3f25
  6. https://github.com/alibaba/arthas/blob/master/README_CN.md
  7. https://blog.csdn.net/qq_27376871/article/details/51613066
  8. https://github.com/alibaba/arthas/issues/222

關注我

我是一名後端開發工程師。主要關注後端開發,資料安全,網路爬蟲,物聯網,邊緣計算等方向,歡迎交流。

各大平臺都可以找到我

原創部落格主要內容

  • Java知識點複習全手冊
  • Leetcode演算法題解析
  • 劍指offer演算法題解析
  • SpringBoot菜鳥入門實戰系列
  • SpringCloud菜鳥入門實戰系列
  • 爬蟲相關技術文章
  • 後端開發相關技術文章
  • 逸聞趣事/好書分享/個人興趣

個人公眾號:後端技術漫談

如果文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

相關文章