前言
當(dāng)你興沖沖地開始運(yùn)行自己的Java項(xiàng)目時(shí),你是否遇到過如下問題:
- 程序在穩(wěn)定運(yùn)行了,可是實(shí)現(xiàn)的功能點(diǎn)了沒反應(yīng)。
- 為了修復(fù)Bug而上線的新版本,上線后發(fā)現(xiàn)Bug依然在,卻想不通哪里有問題?
- 想到可能出現(xiàn)問題的地方,卻發(fā)現(xiàn)那里沒打日志,沒法在運(yùn)行中看到問題,只能加了日志輸出重新打包——部署——上線
- 程序功能正常了,可是為啥響應(yīng)時(shí)間這么慢,在哪里出現(xiàn)了問題?
- 程序不但穩(wěn)定運(yùn)行,而且功能完美,但跑了幾天或者幾周過后,發(fā)現(xiàn)響應(yīng)速度變慢了,是不是內(nèi)存泄漏了?
以前,你碰到這些問題,解決的辦法大多是,修改代碼,重新上線。但是在大公司里,上線的流程是非常繁瑣的,如果為了多加一行日志而重新發(fā)布版本,無疑是非常折騰人的。
現(xiàn)在,我們有了更為優(yōu)雅的線上調(diào)試方法,來自阿里巴巴開源的Arthas
下圖是Arthas文檔中對于為什么要使用它的描述,我進(jìn)行了精簡:
好了,前言已經(jīng)超過字?jǐn)?shù)了,哈哈,在本篇文章里,你能夠了解:
- Arthas使用實(shí)例:幫助你快速讓你上手,拯救你的低效率Debug
- 使用Arthas解決具體問題:看一下Arthas幫我拯救了多少時(shí)間
- 相似工具:看看線上Debug還有沒有別的工具可以使用
- 原理淺談:莫在浮沙筑高閣!你需要大概了解下Arthas的原理
相信我,Arhas覺得是你提升效率的利器,適合各種階段的開發(fā)者,尤其適合我這種剛?cè)腴T的新人(天天上班寫B(tài)ug的人)。你不應(yīng)該有這種東西是高階程序員才應(yīng)該去使用的思想,放心大膽的去用吧
線上Debug神器Arthas
Arthas使用實(shí)例
命令的詳細(xì)文檔請參考:
https://alibaba.github.io/arthas/commands.html
快速啟動
快速啟動它,你只需要兩行命令:
wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
隨后,在界面出現(xiàn)的進(jìn)程中,選擇你的程序序號,比如1
這樣你就進(jìn)入了arthas的控制臺
基本使用
Arthas有如下功能:
1. 首先是我認(rèn)為的“上帝視角”指令:Dashboard
當(dāng)前系統(tǒng)的實(shí)時(shí)數(shù)據(jù)面板,按 ctrl+c 退出。
當(dāng)運(yùn)行在Ali-tomcat時(shí),會顯示當(dāng)前tomcat的實(shí)時(shí)信息,如HTTP請求的qps, rt, 錯誤數(shù), 線程池信息等等。
通過這些,你可以對于整個程序進(jìn)程有個直觀的數(shù)據(jù)監(jiān)控。
2. 類加載問題相關(guān)指令
SC:查看JVM已加載的類信息
通過SC我們可以看到我們這個類的詳細(xì)信息,包括是從哪個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也可以查看已加載的類,幫助你看是否有沒有納入進(jìn)來的類,尤其是在Spring中,可以判斷的你的依賴有沒有正確的進(jìn)來。
上圖中代碼:
# 查看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. 方法運(yùn)行相關(guān)指令
watch:方法執(zhí)行的數(shù)據(jù)觀測
你可以通過watch指令,來監(jiān)控某個類,監(jiān)控后,運(yùn)行下你的功能,復(fù)現(xiàn)下場景,arthas會提供給你具體的出參和入?yún)?,幫助你排查故?/p>
trace:輸出方法調(diào)用路徑,并輸出耗時(shí)
這個指令對于優(yōu)化代碼非常的有用,可以看出具體每個方法執(zhí)行的時(shí)間,如果是for循環(huán)等重復(fù)語句,還能看出n次循環(huán)中的最大耗時(shí),最小耗時(shí),和平均耗時(shí),完美!
tt:官方名為時(shí)空隧道
這是我調(diào)試用的最多的指令,在你對某方法開啟tt后,會記錄下每一次的調(diào)用(你需要設(shè)置最大監(jiān)控次數(shù)),然后你可以在任何時(shí)候會看這里面的調(diào)用,包括出參,入?yún)?,運(yùn)行耗時(shí),是否異常等。非常強(qiáng)大。
4. 線程調(diào)試相關(guān)指令
thread相關(guān)命令:
thread -n:排列出 CPU 使用率 Top N 的線程。
thread -b:排查阻塞的線程
我們代碼有時(shí)候設(shè)計(jì)的不好,會引發(fā)死鎖的問題,卡住整個線程執(zhí)行,使用這個指令可以輕松的找到問題線程,以及問題的執(zhí)行語句。
- 強(qiáng)大的ognl表達(dá)式
眾所周知,一般來說,表達(dá)式都是調(diào)試工具里最強(qiáng)的指令,哈哈。
在Arthas中你可以利用ognl表達(dá)式語言做很多事,比如執(zhí)行某個方法,獲取某個信息,甚至進(jìn)行修改。
[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]$
甚至你可以動態(tài)更換日志輸出級別
$ 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. 響應(yīng)時(shí)間異常問題
工作中遇到一個優(yōu)化問題,系統(tǒng)中一個導(dǎo)出表格的功能,響應(yīng)時(shí)間長達(dá)2分鐘,雖然給內(nèi)部使用,但也不能這么夸張,用trace跟蹤下方法,發(fā)現(xiàn)是其中的手機(jī)號加解密函數(shù)占用了非常大的時(shí)間,幾千個手機(jī)號,進(jìn)行了解密后加密的精彩操作,最終導(dǎo)致了兩分鐘的返回時(shí)間。
2. 某功能Bug導(dǎo)致服務(wù)器返回500
首先通過trace看異常報(bào)錯的方法,之后通過tt排查方法,發(fā)現(xiàn)入?yún)⑦M(jìn)來后,居然走錯了方法(因?yàn)槎鄳B(tài)),走到了返回null的方法中,所以導(dǎo)致了NPE空指針錯誤。
補(bǔ)充
Arthas還支持Web Console,詳見:
https://alibaba.github.io/arthas/web-console.html
相似工具
BTrace一是個歷史比較久的工具,觀察下來Arthas其實(shí)和他的理念蠻相似的,相信Arthas也參考過Btrace,作為一個學(xué)習(xí)樣例來開發(fā)Arthas。詳細(xì)的優(yōu)劣勢看圖:
其他的相似工具,還有jvm-sandbox,有興趣的朋友可以去看看。
原理淺談
分為三個部分:
- 啟動
- arthas服務(wù)端代碼分析
- arthas客戶端代碼分析
啟動
使用了阿里開源的組件cli,對參數(shù)進(jìn)行了解析
com.taobao.arthas.boot.Bootstrap
在傳入?yún)?shù)中沒有pid,則會調(diào)用本地jps命令,列出java進(jìn)程
進(jìn)入主邏輯,會在用戶目錄下建立.arthas目錄,同時(shí)下載arthas-core和arthas-agent等lib文件,最后啟動客戶端和服務(wù)端
通過反射的方式來啟動字符客戶端
服務(wù)端——前置準(zhǔn)備
看服務(wù)端啟動命令可以知道 從 arthas-core.jar開始啟動,arthas-core的pom.xml文件里面指定了mainClass為com.taobao.arthas.core.Arthas,使得程序啟動的時(shí)候從該類的main方法開始運(yùn)行。
- 首先解析入?yún)ⅲ蒫om.taobao.arthas.core.config.Configure類,包含了相關(guān)配置信息
- 使用jdk-tools里面的VirtualMachine.loadAgent,其中第一個參數(shù)為agent路徑, 第二個參數(shù)向jar包中的agentmain()方法傳遞參數(shù)(此處為agent-core.jar包路徑和config序列化之后的字符串),加載arthas-agent.jar包
- 運(yùn)行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) {
// 省略非關(guān)鍵代碼,解析啟動參數(shù)作為配置,并填充到configure對象里面
return configure;
}
private void attachAgent(Configure configure) throws Exception {
// 省略非關(guān)鍵代碼,attach到目標(biāo)進(jìn)程
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
virtualMachine.loadAgent(configure.getArthasAgent(),
configure.getArthasCore() + ";" + configure.toString());
}
public static void main(String[] args) {
new Arthas(args);
}
}
服務(wù)端——監(jiān)聽客戶端請求
- 如果是exit,logout,quit,jobs,fg,bg,kill等直接執(zhí)行。
- 如果是其他的命令,則創(chuàng)建Job,并運(yùn)行。
- 創(chuàng)建Job時(shí),會根據(jù)具體客戶端傳遞的命令,找到對應(yīng)的Command,并包裝成Process, Process再被包裝成Job。
- 運(yùn)行Job時(shí),反向先調(diào)用Process,再找到對應(yīng)的Command,最終調(diào)用Command的process處理請求。
服務(wù)端——Command處理流程
- 不需要使用字節(jié)碼增強(qiáng)的命令
其中JVM相關(guān)的使用 java.lang.management 提供的管理接口,來查看具體的運(yùn)行時(shí)數(shù)據(jù)。比較簡單,就不介紹了。
- 需要使用字節(jié)碼增強(qiáng)的命令
字節(jié)碼增加的命令統(tǒng)一繼承EnhancerCommand類,process方法里面調(diào)用enhance方法進(jìn)行增強(qiáng)。調(diào)用Enhancer類enhance方法,該方法內(nèi)部調(diào)用inst.addTransformer方法添加自定義的ClassFileTransformer,這邊是Enhancer類。
Enhancer類使用AdviceWeaver(繼承ClassVisitor),用來修改類的字節(jié)碼。重寫了visitMethod方法,在該方法里面修改類指定的方法。visitMethod方法里面使用了AdviceAdapter(繼承了MethodVisitor類),在onMethodEnter方法, onMethodExit方法中,把Spy類對應(yīng)的方法(ON_BEFORE_METHOD, ON_RETURN_METHOD, ON_THROWS_METHOD等)編織到目標(biāo)類的方法對應(yīng)的位置。
在前面Spy初始化的時(shí)候可以看到,這幾個方法其實(shí)指向的是AdviceWeaver類的methodOnBegin, methodOnReturnEnd等。在這些方法里面都會根據(jù)adviceId查找對應(yīng)的AdviceListener,并調(diào)用AdviceListener的對應(yīng)的方法,比如before,afterReturning, afterThrowing。
客戶端
客戶端代碼在arthas-client模塊里面,入口類是com.taobao.arthas.client.TelnetConsole。
主要使用apache commons-net jar進(jìn)行telnet連接,關(guān)鍵的代碼有下面幾步:
- 構(gòu)造TelnetClient對象,并初始化
- 構(gòu)造ConsoleReader對象,并初始化
- 調(diào)用IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput())處理各個流,一共有四個流:
- telnet.getInputStream()
- telnet.getOutputStream()
- System.in
- consoleReader.getOutput()
請求時(shí):從本地System.in讀取,發(fā)送到 telnet.getOutputStream(),即發(fā)送給遠(yuǎn)程服務(wù)端。
響應(yīng)時(shí):從telnet.getInputStream()讀取遠(yuǎn)程服務(wù)端發(fā)送過來的響應(yīng),并傳遞給 consoleReader.getOutput(),即在本地控制臺輸出。
關(guān)于源碼,深入下去還有很多東西需要生啃,我也沒有消化得很好,大家可以繼續(xù)閱讀詳細(xì)資料。
總結(jié)
Arthas是一個線上Debug神器,小白也可以輕松上手。
碼字不易,希望大家捧個人場,謝謝諸位。
參考文獻(xiàn)
開源地址:
https://github.com/alibaba/arthas
官方文檔:
https://alibaba.github.io/arthas
其他參考:
- Hollis:Arthas - Java 線上問題定位處理的終極利器
- https://www.cnblogs.com/LittleHann/p/4783581.html
- https://juejin.im/post/5c466d7bf265da61511509ab#heading-29
- http://tech.dianwoda.com/2018/12/20/arthasyuan-ma-fen-xi/
- http://www.itdecent.cn/p/0771646f3f25
- https://github.com/alibaba/arthas/blob/master/README_CN.md
- https://blog.csdn.net/qq_27376871/article/details/51613066
- https://github.com/alibaba/arthas/issues/222