一、簡介
Arthas是Alibaba開源的一款Java診斷工具,採用命令式交互模式,用來排查各種JVM的問題。
Arthas主要提供了一下幾種功能
1、實時監控JVM運行狀況
2、實時查看已加載的類型和類加載器信息
3、通過字節碼增強技術實現方法執行的監控和統計
二、Arthas的使用
2.1、Arthas安裝啓動
Arthas本質上也是一個Java程序,沒有安裝流程,只需要下載jar包,通過java命令直接啓動即可
下載arthas-boot.jar
,然後用java -jar
的方式啓動:
下載命令:
wget https://arthas.aliyun.com/arthas-boot.jar
啓動命令:
java -jar arthas-boot.jar
啓動之後會列出當前節點正在運行的所有Java進程,並且有對應的序號,可以輸入序號來選擇需要訪問的進程信息,如選擇第一個進程就直接輸入1即可,啓動效果如下圖示:
表示Attach上進程號爲21093的Java進程,並且此時就進入Arthas的命令交互模式,不可以再輸入操作系統的命令,只可以輸入Arthas相關的命令,可以輸入help命令查看所有Arthas支持的命令,如下:
2.2、Arthas命令概覽
Arthas命令主要分成幾個類型,分別是基本命令、JVM相關命令、class相關命令、監控相關命令
2.2.1、基本命令
命令 | 用途 |
help | 查看命令幫助信息 |
cat | 打印文件內容 |
echo | 打印參數 |
grep | 匹配查找 |
base64 | base64編碼轉換 |
tee | 複製標誌輸入到標準輸出和指定的文件 |
pwd | 返回當前工作目錄 |
cls | 清空屏幕內容 |
session | 查看當前會話信息 |
reset | 重置增強類,將被Arthas增強過的類全部還原,Arthas服務關閉時自動還原 |
version | 輸出當前目標Java進程加載的Arthas版本號 |
history | 打印命令歷史 |
quit | 退出當前Arthus客戶端 |
stop | 關閉Arthas服務端,所有Arthas客戶端全部退出 |
2.2.2、JVM相關命令
命令 | 用途 | 用法 |
dashboard | 打印當前JVM實時數據面板 | dashboard |
thread | 打印當前JVM線程堆棧信息 |
thread thread 線程ID thread --state WAITING |
jvm |
打印當前JVM實時運行信息 | jvm |
sysprop | 打印和修改當前JVM信息 | sysprop |
sysenv | 查看當前JVM環境變量 | sysenv |
vmoption | 查看和修改JVM裏診斷相關option | vmoption |
logger | 查看和修改logger信息 | logger |
getstatic | 獲取靜態變量的值 | getstatic [className] [staticField] |
ognl | 執行ognl表達式 | ognl 表達式 |
mbean | 打印MBean信息 | mbean |
heapdump | 打印堆棧信息 |
heapdump heapdump /test/dump/test.hprof heapdump --live /test/dump/test.hprof |
vmtool | 從jvm裏查詢對象,執行forceGc | vmtool --action getInstances --className [className] |
perfcounter | 查看當前JVM的 Perf Counter信息 | perfcounter |
2.2.3、class相關命令
命令 | 用途 | 用法 |
sc | 查詢JVM已加載的類信息 |
sc *[className]* sc -d *[className]* sc -d -f *[className]* |
sm | 查詢JVM已加載的方法信息 |
sm [className] sm -f [className] |
jad | 反編譯JVM已加載的類信息 | jad [className] |
mc | 內存編譯器,通過內存編譯.java文件 |
mc /com/test/test.java mc -c [類加載ID] /com/test/test.java mc --classLoadClass [classLoadClassName] /com/test/test.java mc -d /tmp/file /com/test/test.java |
retransform | 加載外部.class文件,retransform到JVM中 | retransform /com/test/test.class |
redefine | 加載外部.class文件,redefine到JVM中 | redefine /com/test/test/class |
dump | dump已加載類的字節碼到指定目錄 |
dump java.lang.String dump -d /tmp/file java.lang.String |
classloader | 查看類加載器繼承樹信息 |
classloader classloader -l classloader -t classloader -c [類加載ID] --load [className] |
2.2.4、監控相關命令
監控相關的命令是通過字節碼增強技術將增強的邏輯織入到目標類中,所以監控完成之後需求及時執行reset命令去除增強的邏輯
命令 | 用途 | 用法 |
watch | 方法執行數據觀測 | watch [className] [methodName] "{params,returnObj}" -x 2 |
monitor | 方法執行監控 | monitor -c 5 [className] [methodName] |
stack | 輸出當前方法被調用的路徑 | stack [className] [methodName] |
trace | 方法內部的調用路徑,並打印路徑耗時 | trace [className] [methodName] |
tt | 方法執行數據的時空隧道,記錄下指定方法每次調用的入參和返回信息,並能對這些不同的時間下調用進行觀測 | tt -t [className] [methodName] |
2.3、Arthas命令詳解
2.3.1、dashborad(實時看板)
語法:dashboard [i:] [n:]
dashboard ##默認每個5秒打印一次JVM實時數據 dashboard -i 2000 ##每隔2秒打印一次JVM實時數據 dashboard -n 10 ##總共打印10次JVM實時數據
dashboard -i 1000 -n 10 ##每隔1秒打印一次JVM實時數據,共打印10次
dashboard命令用於實時查看JVM運行信息,包括三個模塊分佈是線程運行情況、內存使用情況以及JVM運行環境信息等
線程模塊會打印當前JVM所有的線程運行狀況,包括線程ID、名稱、優先級、狀態、佔有CPU比率、佔有CPU時長等;
內存模塊會打印JVM當前堆內和堆外內存使用情況以及GC的次數和時間統計
運行環境模塊會打印當前操作系統和JDK的版本信息以及運行環境等信息
2.3.2、thread(線程信息)
通過thread命令可以打印當前所有運行的線程信息,並且可以通過thread 線程ID的方式查看指定線程的棧信息
語法
thread 查詢所有線程
thread <id> 查詢指定線程
thread -n <n> 查詢最忙的n個線程
thread -i <i>
: 統計最近指定毫秒內的線程CPU時間
thread -n <n> -i <i>
: 列出最近指定毫秒內最忙的N個線程棧
thread -b 查詢阻塞其他線程的線程
thread --state [RUNNABEL]|[WAITING]|[TIMED_WAITING]|[NEW]|[BLOCKED]|[TERMINATED] 查詢指定狀態的線程
thread ## 查看所有線程信息
thread <id> ## 查看指定線程ID的信息
thread -n 10 ## 查看最忙的10個線程
thread -b ## 查看阻塞其他線程的線程信息
thread --state WAITING ## 查看等待狀態的線程信息
通過thread查看所有線程信息,除了業務線程還會包含JVM內部線程,JVM內部線程ID爲-1,包括GC線程如GC task thread#2 (ParallelGC)、JIT編譯線程如C2 CompilerThread0、其他內部線程如VM Periodic Task Thread等
thread -b 命令可以找出當前阻塞了其他線程的線程,比如線程A通過Synchronized關鍵字拿到了鎖,線程B和線程C被阻塞了,那麼此時就可以通過thread -b命令查詢出來,目前也僅支持Synchronized獲取鎖阻塞的情況
2.3.3、jvm(查看JVM信息)
用法:jvm
jvm命令可以打印當前JVM的實時信息,包括運行環境、加載的類統計、內存使用情況、GC統計情況、線程統計情況、文件描述符統計信息等
THREAD相關
-
COUNT: JVM當前活躍的線程數
-
DAEMON-COUNT: JVM當前活躍的守護線程數
-
PEAK-COUNT: 從JVM啓動開始曾經活着的最大線程數
-
STARTED-COUNT: 從JVM啓動開始總共啓動過的線程次數
-
DEADLOCK-COUNT: JVM當前死鎖的線程數
文件描述符相關
-
MAX-FILE-DESCRIPTOR-COUNT:JVM進程最大可以打開的文件描述符數
-
OPEN-FILE-DESCRIPTOR-COUNT:JVM當前打開的文件描述符數
2.3.4、jad(反編譯)
語法
jad <類完整路徑> 反編譯指定類
jad <類完整路徑> <方法名> 反編譯指定方法
通過sc可以查看所有已加載的類信息,然後就可以通過jad命令可以將已加載的類.class文件進行反編譯,命令如下:
jad com.test.ArthasDemo
2.3.5、sc(查看已加載的類)
語法
sc [-d] [-f] *<className>*
[-d] 表示打印詳細信息;[-f]表示打印屬性信息,可以通過關鍵字模糊查詢
sc是search class的縮寫,這個命令能搜索出所有已經加載到 JVM 中的 Class 信息,這個命令支持的參數有 [d]
、[E]
、[f]
和 [x:],並且支持模糊查詢
如想搜索後綴爲Service的類,則可以輸入一下命令:
sc -d -f *Service
-d表示輸出當前類的詳細信息,包括這個類所加載的原始文件來源、類的聲明、加載的ClassLoader等詳細信息。如果一個類被多個ClassLoader所加載,則會出現多次
-f表示輸出當前類的成員變量信息(需要配合參數-d一起使用)
通過該命令可以查看已加載的類的詳細信息,包括code-source表示從哪個包中加載的,使用哪個class-loader類加載器加載的等,案例如下圖示:
2.3.6、sm(查看已加載類的方法)
用法
sm <類完整路徑>
sm -d <類完整路徑>
sm -d <類完整路徑> <方法名>
sm命令可以查看已加載的類的函數,比如查看java.math.RoundingMode類的所有方法,則命令如下:
sm -d java.math.RoundingMode
2.3.7、getstatic(查看靜態屬性)
用法
getstatic <類完整路徑> <靜態屬性名>
通過getstatic命令可以查看已加載的類的靜態屬性的值
2.3.8、watch(監控方法執行數據)
用法
watch <className> <method> 觀察指定類指定方法的執行情況,返回耗時,返回值等信息
watch <className> <method> "{params,returnObj}"-x <x> 觀察入參和出參,返回結果便利深度爲x,默認深度爲1
watch <className> <method> "{params,returnObj}"-b 觀測方法調用前的入參和返回值,調用方法前返回值肯定爲空
watch <className> <method> "{params,returnObj}" -b -s -n <n> 同時觀察方法執行前和方法執行後的入參和返回值,監控n次
watch <className> <method> "{params,throwExp}" -e 觀察方法在拋出異常時的入參和異常信息
watch <className> <method> "{params,target}" -b -s 觀察方法執行前和方法執行後入參和當前對象的信息
watch <className> <method> "{params,target.fieldName}" -b -s 觀察方法執行前和方法執行後入參和當前對象的fieldName屬性的值
watch <className> <method> "{params}" '#cost>100' -f 觀察方法執行後執行耗時大於100毫秒的入參信息
通過watch命令可以監控指定類的指定方法的參數、返回值和異常信息,watch 命令定義了4個觀察事件點,即 -b
方法調用前,-e
方法異常後,-s
方法返回後,-f
方法結束後,
4個觀察事件點 -b
、-e
、-s
默認關閉,-f
默認打開,當指定觀察點被打開後,在相應事件點會對觀察表達式進行求值並輸出
這裏要注意方法入參
和方法出參
的區別,有可能在中間被修改導致前後不一致,除了 -b
事件點 params
代表方法入參外,其餘事件都代表方法出參
當使用 -b
時,由於觀察事件點是在方法調用前,此時返回值或異常均不存在
2.3.9、monitor(方法執行監控)
用法
monitor -c <second> <className> <methodName> 定時second秒統計一次指定類指定方法的調用情況
monitor -c <second> -b <className> <methodName> 'params[1] > 1' 方法調用之前統計第二個參數大於1的調用情況
對匹配 class-pattern
/method-pattern
/condition-express
的類、方法的調用進行監控。
monitor
命令是一個非實時返回命令.
實時返回命令是輸入之後立即返回,而非實時返回的命令,則是不斷的等待目標 Java 進程返回信息,直到用戶輸入 Ctrl+C
爲止。
服務端是以任務的形式在後臺跑任務,植入的代碼隨着任務的中止而不會被執行,所以任務關閉後,不會對原有性能產生太大影響,而且原則上,任何Arthas命令不會引起原有業務邏輯的改變。
monitor和watch的區別是watch是實時監控,每次方法調用都會打印;monitor是統計之後定時打印,watch傾向於實時查看,monitor傾向於定時統計
monitor可以監控的維度包括:timestamp(時間戳)、class(類)、method(方法)、total(調用次數)、success(成功次數)、fail(失敗次數)、rt(平均RT)、fail-rate(失敗率)
monitor結果如下圖示:
2.3.10、trace(方法調用路徑)
用法
trace <className> <methodName> 實時打印指定類指定方法的調用鏈路
trace <className> <methodName> --skipJDKMethod false 打印JDK方法執行鏈路,默認會被過濾
trace <className> <method> '#cost > 100' 過濾耗時大於100毫秒的調用鏈路
trace <className> <method> '#cost > 100' -n <n> 限制實時監控的次數爲n次
trace可以查詢指定類的指定方法調用路徑,渲染和統計整個調用鏈路上的所有性能開銷和追蹤調用鏈路
打印信息中包含了整個鏈路的每一步,並且打印了每一個鏈路的代碼行數和耗時情況
2.3.11、stack(輸出當前方法被調用的路徑)
用法
stack <className> <methodName> 打印某個方法被調用的鏈路
stack <className> <methodName> 'params[0] > 1' 按入參條件過濾
stack <className> <methodName> '#cost > 100' 按耗時進行過濾
當一個方法可以被很多地方調用時,想要查詢當前方法是被哪個方法調用,此時就可以通過stack命令得知當前方法是從什麼地方被執行的,如下圖示
3、Arthas使用案例
案例一:排查方法執行異常
場景:線上某個接口偶爾會報500錯誤,此時就需要排查報500錯誤時的具體參數
可以通過watch命令監控接口報500錯誤時的參數和異常信息命令如下:
watch <類路徑,支持通配符> <方法名,支持通配符> "{params, throwExp}" -e 表示當發生異常時打印出參數和異常信息內容
案例二:熱更新代碼
場景:線上存在一個小BUG,需要修改部分代碼就可以修復,而發版成本較大時就可以通過熱更新的方式替換線上的代碼邏輯
熱更新涉及到幾個步驟,
1、首先需要將.class文件進行反編譯成源代碼文件
2、然後通過編輯vim編輯源代碼文件更新代碼邏輯
3、然後再將新源代碼文件編譯成.class文件
4、最後再將新的.class文件替換掉JVM中的已加載的.class文件
假設用一個UserController的getUserDetail代碼如下:
1 @RestController 2 @RequestMapping(value = "/testUser") 3 public class TestController { 4 5 @RequestMapping(value = "/detail", method = RequestMethod.GET) 6 public String getUserName(@RequestParam("userId") String userId){ 7 return "name is :" + Long.valueOf(userId); 8 } 9 }
由於參數userId定義的是String類型,因此可能會出現第7行的轉換異常,所以需要將userId類型改成Long類型,並通過熱更新的方式部署
第一步:通過jad命令反編譯UserController類,並將反編譯後的代碼寫入臨時文件夾
jad --source-only com.zjic.message.business.controller.TestController > /tmp/TestController.java
第二步:通過vim來編輯TestController.java文件,修改源代碼將入參的String類型改成Long類型
vim /tmp/TestController
第三步:編譯新的TestController.java文件
編譯UserController可以指定原先的類加載器編譯,那麼可以先通過sc命令找到原先的類加載
sc -d *TestController | grep 'classLoader'
查到classLoader的hash碼後就可以通過mc命令編譯新的TestController.java文件,並寫入到tmp目錄生成TestController.class文件
mc -c 17d99928 /tmp/TestController.java -d /tmp
第四步:通過redefine命令將新生成的TestController.class替換掉JVM中已加載的TestController
1 redefine /tmp/com/zjic/message/business/controller/TestController.class
結果如下:
提示刪除了一個方法所以替換失敗,因爲通過反編譯修改新的TestController.java文件時將方法的參數類型進行了修改,那麼就相當於重新定義了一個方法,所以想要通過redefine替換.class文件時,類中的方法個數、參數個數、參數類型以及返回值都不允許修改,只可以修改方法內部的實現邏輯。
重新修改TestController.java文件,不修改參數userId的類型在方法體內部添加try/catch捕獲異常,
@RestController @RequestMapping(value={"/testUser"}) public class TestController { @RequestMapping(value={"/detail"}, method={RequestMethod.GET}) public String getUserName(@RequestParam(value="userId") String userId) { Long id = null; try{ id = Long.parseLong(userId); }catch(Exception e){ } return "name is :" + id; } }
並重新編譯TestController.class文件,最後重新執行redefine命令,結果如下:
測試接口驗證通過,從而成功實現了熱更新功能
案例三:線上慢請求排查
場景:當訪問線上某個接口時,發現接口相應比較慢並且通過代碼又不太好排查出具體是哪一行代碼引起的慢請求
通過trace命令可以監控接口方法的調用鏈路和各個鏈路的耗時,從而可以分析接口最耗時的代碼塊在哪,案例如下:
trace com.zjic.message.business.controller.BusinessApplyController * '#cost > 1'
先監控指定Controller的所有方法,採用*通配符匹配,然後可以發現最耗時的代碼是Service層的方法,此時就可以再繼續使用trace命令繼續查看Service層方法的調用鏈路
最終定位到最耗時的是Mapper層的方法,所以該接口最耗時的就是SQL語句的執行,那麼就可以針對SQL優化來進行接口的優化