最近我參與的業務系統在生産環境運作出了點問題,沒有得到自己預期的結果,排查了好長一段時間也沒有結果。正當我犯愁之際,突然想起了我曾使用過的Arthas工具。于是聯系運維在業務系統所在的伺服器部署了該工具,借助該工具的排查,沒過多久就順利找到了問題點,簡直不要太愉快!鑒于Arthas優秀的功能,我覺得有必要分享一下該工具,還沒使用過它的小夥伴,不妨先了解一下它,指不定哪天就能派上大用場。
1、簡介
Arthas 是一款線上監控診斷産品,通過全局視角實時檢視java應用 load、記憶體、gc、線程的狀态資訊,并能在不修改應用代碼的情況下,對業務問題進行診斷,包括檢視方法調用的出入參、異常,監測方法執行耗時,類加載資訊等,大大提升線上問題排查效率。
2、使用場景
正如官方所說,如果我們遇到了如下問題,Arthas都能幫我們解決。
(1)這個類從哪個 jar 包加載的?為什麼會報各種類相關的 Exception?
(2)我改的代碼為什麼沒有執行到?難道是我沒 commit?分支搞錯了?
(3)遇到問題無法線上上 debug,難道隻能通過加日志再重新釋出嗎?
(4)線上遇到某個使用者的資料處理有問題,但線上同樣無法 debug,線下無法重制!
(5)是否有一個全局視角來檢視系統的運作狀況?
(6)有什麼辦法可以監控到 JVM 的實時運作狀态?
(7)怎麼快速定位應用的熱點,生成火焰圖?
(8)怎樣直接從 JVM 内查找某個類的執行個體?
拿我來說,我最近遇到的問題就是:我的系統要把使用者資料推送到别的業務系統,在傳輸給别的系統之前打了即将推送的使用者資料的日志,但日志裡面顯示使用者資料是空的,而系統又沒别的報錯日志。如果我要加日志的話,那系統就得重新測試,走上線流程,就太麻煩了。好在Arthas提供了監控方法調用的功能,能讓我直覺的看到程式的執行情況,定位到資料缺失的原因。
3、安裝Arthas
3.1、安裝Arthas(公網)
當我們的電腦可以通路外網時,可以參考本部分完成arthas的安裝。
3.1.1、下載下傳安裝包
首先執行下列指令,下載下傳Arthas工具
curl -O https://arthas.aliyun.com/arthas-boot.jar
3.1.2、啟動Arthas
針對上一步下載下傳的jar包,通過執行java -jar啟動Arthas
java -jar arthas-boot.jar
可以得到以下結果:
D:\>java -jar arthas-boot.jar
[INFO] JAVA_HOME: D:\developer\Java\jdk1.8\jre
[INFO] arthas-boot version: 3.6.8
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 20048 org.jetbrains.idea.maven.server.RemoteMavenServer36
[2]: 21184 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 15412
[4]: 324 org.jetbrains.jps.cmdline.Launcher
[5]: 3568 com.xk.mybatis.springboot.App
輸出的結果可以看到目前arthas的版本是3.6.8,以及作業系統運作的java程序。編号1對應程序id是20048,編号5對應的程序id是3568。由于現在指令行是處于互動模式,我們可以通過輸入編号來讓arthas綁定指定的程序,友善進行後續的操作。
此處我們輸入編号5,監控本地啟動的業務系統,得出以下輸出:
* [1]: 20048 org.jetbrains.idea.maven.server.RemoteMavenServer36
[2]: 21184 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 15412
[4]: 324 org.jetbrains.jps.cmdline.Launcher
[5]: 3568 com.xk.mybatis.springboot.App
[6]: 31740 C:\Users\scau_\Desktop\jd-gui.exe
5
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/3.6.8?mirror=aliyun
[INFO] File size: 12.97 MB, downloaded size: 554.62 KB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 1.21 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 2.00 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 2.88 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 3.76 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 4.85 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 6.19 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 7.77 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 9.47 MB, downloading ...
[INFO] File size: 12.97 MB, downloaded size: 11.56 MB, downloading ...
[INFO] Download arthas success.
[INFO] arthas home: C:\Users\scau_\.arthas\lib\3.6.8\arthas
[INFO] Try to attach process 3568
[INFO] Attach process 3568 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.6.8
main_class
pid 3568
time 2023-05-05 17:53:25
[arthas@3568]$
arthas會聯網下載下傳一些必須的檔案,然後和目标程序進行綁定。至此,arthas就啟動成功了。我們可以通過arthas提供的指令來對我們的業務系統進行監控。
注意:如果我們已經知道了Java程序的ID(比如上面編号5對應的程序ID是3568),那麼可以直接執行下面指令,啟動Artthas,并将其和業務系統綁定。
java -jar arthas-boot.jar 3568
3.2、安裝Arthas(内網)
如果我們要在内網伺服器(或者個人辦公電腦)上安裝arthas,由于無法通路網際網路,沒辦法通過3.1中的指令線上下載下傳arthas元件,此時我們可以去外網下載下傳arthas完整的安裝包,然後再去内網部署。
3.2.1、下載下傳完整安裝包
通路下面的GitHub位址,去選擇合适的發行版來下載下傳
https://github.com/alibaba/arthas/releases
例:3.6.8版本的github下載下傳位址如下
https://github.com/alibaba/arthas/releases/download/arthas-all-3.6.8/arthas-bin.zip
或者直接通路下述位址,下載下傳最新版本的Arthas完整安裝包
https://arthas.aliyun.com/download/latest_version?mirror=aliyun
3.2.2、安裝Arthas
将上述的zip包的内容直接解壓到arthas目錄下,arthas目錄便是Arthas的安裝目錄,由此便完成了安裝。
3.2.3、啟動Arthas
首先確定我們的業務系統已啟動,通過jps指令找到程序ID,比如ID是3568
進入arthas目錄。
如果是Windows系統,執行下述指令啟動
as.bat 3568
如果是Linux系統,執行下述指令啟動
./as.sh 3568
或者直接通過下述指令啟動(不分作業系統)
#此處指令也可以不攜帶結尾的程序ID參數3568,可以根據提示在後面的操作中選擇Arthas查詢到的程序ID并完成啟動操作
java -jar arthas-boot.jar 3568
3.3、遠端連接配接Arthas
Arthas目前支援Web Console,我們可以通過浏覽器通路Arthas。Arthas綁定應用成功後,我們可以直接通路
http://127.0.0.1:8563,會打開如下界面:
浏覽器通路本地arthas
上圖的arthas已經綁定了應用系統,是以我們可以像在本地指令行界面操作那樣,輸入arthas相應的指令。當然,我們還可以輸入ip,通路其他的arthas服務。
注意:預設情況下,arthas 隻 listen 127.0.0.1,是以如果想從遠端連接配接(比如在本地浏覽器通路伺服器上的arthas),則可以在啟動伺服器上的arthas時使用 --target-ip參數指定 listen 的 IP,示例如下:
java -jar arthas-boot.jar 3568 --target-ip 192.168.83.206
然後就可以浏覽器通路http://192.168.83.206:8563/,檢視伺服器上的arthas服務。
浏覽器通路遠端arthas
3.4、退出Arthas
當我們處于以下和arthas互動的狀态時,可以選擇退出連接配接。
[arthas@3568]$
[arthas@3568]$
輸入quit或exit退出目前連接配接:
[arthas@3568]$
[arthas@3568]$ quit
D:\developer\arthas-packaging-3.6.8-bin>
但是arthas程序還在,arthas自身監聽所使用的端口也未關閉,下次啟動arthas時仍會連接配接到之前的應用。可以輸入stop,完全停止arthas程序,将其和目标應用解綁:
[arthas@3568]$
[arthas@3568]$ stop
Resetting all enhanced classes ...
Affect(class count: 1 , method count: 0) cost in 15 ms, listenerId: 0
Arthas Server is going to shutdown...
[arthas@3568]$ session (9785d839-da6a-4c13-a7c9-e13bdafafb18) is closed because server is going to shutdown.
D:\developer\arthas-packaging-3.6.8-bin>
4、常用指令
4.1、memory
該指令用于檢視JVM記憶體情況
arthas-memory
4.2、heapdump
該指令用于dump Java堆資訊
(1)指定存儲的路徑
[arthas@3568]$ heapdump arthas-output/dump.hprof
Dumping heap to arthas-output/dump.hprof ...
Heap dump file created
[arthas@3568]$
上述指令dump堆資訊到arthas-output目錄下的dump.hprof檔案中,注意arthas-output目錄是固定的,後面的檔案名可以自定義。
然後可以通過通路下述位址,将堆檔案下載下傳下來
#這塊和Arthas提供的web console功能有關,如果是個人電腦執行的指令,可以直接通路下述位址。否則需要在Arthas啟動的時候配置ip位址
http://localhost:8563/arthas-output/
arthas-dump
(2)不指定存儲的路徑
[arthas@3568]$ heapdump
Dumping heap to C:\Users\ADMINI~1\AppData\Local\Temp\heapdump2023-05-05-22-245366907463725074073.hprof ...
Heap dump file created
[arthas@3568]$
上述指令将dump檔案存儲到臨時檔案。
4.3、watch(超實用的指令)
該指令用于觀測指定函數的執行情況,非常實用。文法如下:
watch 類名的全路徑名 類的方法名 觀察表達式
其中,類的方法名不限制方法對應的通路權限,即使是私有方法也可以。
觀察表達式可以省略,它的預設值是:
{params,target,returnObj}
params代表方法的入參,target代表我們觀察的目标對象(目前類的執行個體),returnObj代表方法的傳回值,它們之間用英文逗号分隔。如果隻需要觀察方法的入參和傳回值,觀察表達式可以這麼寫:
{params,returnObj}
如果隻需要觀察傳回值,觀察表達式可以直接寫:
returnObj
在 watch 指令的結果裡,會列印出location資訊。location有三種可能值:AtEnter,AtExit和AtExceptionExit。分别對應函數入口,函數正常 return,函數抛出異常。
示例如下:
我們業務系統有如下代碼:
package com.xk.mybatis.springboot.service;
import com.xk.mybatis.springboot.entity.Order;
import com.xk.mybatis.springboot.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author xk
* @since 2023.04.24 14:26
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 根據訂單id擷取訂單
* @param id
* @return
*/
public Order findById(Long id){
return orderMapper.findById(id);
}
}
我們觀察其中的findById方法:
[arthas@3568]$ watch com.xk.mybatis.springboot.service.OrderService findById {params,returnObj}
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 29 ms, listenerId: 2
method=com.xk.mybatis.springboot.service.OrderService.findById location=AtExit
ts=2023-05-06 08:40:24; [cost=5.9093ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@Order[com.xk.mybatis.springboot.entity.Order@734346e3],
]
可以看到location=AtExit,說明函數正常傳回。而result=@ArrayList代表我們要觀察的資料。數組中有兩項,@Object[]代表我們的入參params,@Order就對應我們的傳回對象returnObj。findById會傳回一個Order對象,而示例中returnObj對應的值正是一個Order對象.
如果不寫觀察表達式,就會輸出如下的結果:
[arthas@3568]$
[arthas@3568]$ watch com.xk.mybatis.springboot.service.OrderService findById
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 28 ms, listenerId: 3
method=com.xk.mybatis.springboot.service.OrderService.findById location=AtExit
ts=2023-05-06 08:50:06; [cost=2.886499ms] result=@ArrayList[
@Object[][isEmpty=false;size=1],
@OrderService[com.xk.mybatis.springboot.service.OrderService@32acb99a],
@Order[com.xk.mybatis.springboot.entity.Order@36933b17],
]
4.4、jad(超實用的指令)
該指令用于反編譯.class檔案,可以将其反編譯成java源碼的形式,友善我們觀察類檔案的代碼。如果我們懷疑我們新增或修改的代碼沒有得到執行,就可以使用該指令來看看class檔案中是否有我們新增或修改的那部分代碼。
文法如下:
jad --source-only 類的全路徑名 類中的方法名 --lineNumber true(或false)
參數說明如下:
參數說明是否必填--source-only代表隻輸出源碼資訊,不包括classloader資訊否類的全路徑名代表類的全路徑名是類中的方法名代表類中的方法名否--lineNumber是否列印行号,如果不寫,預設為true否
實際有下列的用法:
(1)jad --source-only 類的全路徑名
等價于jad --source-only 類的全路徑名 --lineNumber true
(2)jad 類的全路徑名 類中的方法名
等價于jad 類的全路徑名 類中的方法名 --lineNumber true
(3)jad --source-only 類的全路徑名 類中的方法名 --lineNumber false
(4)jad --source-only 類的全路徑名 類中的方法名 --lineNumber false
示例如下:
反編譯我們的OrderService位元組碼(class)檔案,得出如下結果:
[arthas@3568]$ jad com.xk.mybatis.springboot.service.OrderService
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@5cbc508c
Location:
/D:/programs/ideaProject/study/mybatis/springboot-mybatis/target/classes/
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* com.xk.mybatis.springboot.entity.Order
* com.xk.mybatis.springboot.mapper.OrderMapper
*/
package com.xk.mybatis.springboot.service;
import com.xk.mybatis.springboot.entity.Order;
import com.xk.mybatis.springboot.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Order findById(Long id) {
/*24*/ return this.orderMapper.findById(id);
}
}
Affect(row-cnt:1) cost in 537 ms.
[arthas@3568]$
我們可以很直覺的看出class檔案的内容,如果是類檔案中沒有包括我們修改的代碼,可能就是我們的代碼沒送出成功,導緻打出的jar包沒包括我們的代碼。通過jad指令就能快速的定位出這種問題。
4.5、trace(超實用的指令
該指令用于監控方法内部調用路徑,并輸出方法路徑上的每個節點上耗時。如果我們發現生産環境某個接口響應速度慢,就可以通過該指令觀察主要耗時時間在哪個環節。
文法如下:
trace 類的全路徑名 類的方法名
示例如下:
我們監控下根據訂單id查詢訂單詳情的http接口的調用鍊路及耗時情況,輸出結果如下:
[arthas@3568]$ trace com.xk.mybatis.springboot.controller.OrderController findById
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 72 ms, listenerId: 1
`---ts=2023-05-06 09:29:20;thread_name=http-nio-8080-exec-1;id=19;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@48b0e701
`---[6.1043ms] com.xk.mybatis.springboot.controller.OrderController:findById()
`---[95.87% 5.852ms ] com.xk.mybatis.springboot.service.OrderService:findById() #24
可以看到,controller接口執行耗時6.1043ms,而它内部調用的service層接口執行了5.852ms。如果我們的鍊路涉及到了很複雜的調用過程(比如調用了很多方法來處理業務),就可以通過trace指令來監控哪些方法比較耗時。
結束語
本文先分享到這裡,覺得有收獲的朋友,可以點選"關注",或者進行分享或收藏,有疑惑的也可以來私聊評論,我會及時進行回複~