簡(jiǎn)明的 Arthas 配置及基礎(chǔ)運(yùn)維教程
Arthas是一款強(qiáng)大的開(kāi)源Java診斷程序,它可以非常方便的啟動(dòng)并以界面式的方式和Java程序進(jìn)行交互,支持監(jiān)控程序的內(nèi)存使用情況、線程信息、gc情況、甚至可以反編譯并修改現(xiàn)上代碼等。
一、簡(jiǎn)述Arthas的運(yùn)行原理(理解)
arthas的運(yùn)行原理大致是以下幾個(gè)步驟:
- 啟動(dòng)arthas選擇目標(biāo)Java程序后,artahs會(huì)向目標(biāo)程序注入一個(gè)代理。
- 代理會(huì)創(chuàng)建一個(gè)集HTTP和Telnet的服務(wù)器與客戶端建立連接。
- 客戶端與服務(wù)端建立連接。
- 后續(xù)客戶端需要監(jiān)控或者調(diào)整程序都可以通過(guò)服務(wù)端Java Instrumentation機(jī)制和應(yīng)用程序產(chǎn)生交互。
二、詳解Arthas基礎(chǔ)使用
1.下載安裝
在介紹幾個(gè)典型的案例之前,我們需要先下載安裝一下Arthas,Arthas的官方地址如下:
Arthas的官方地址:https://arthas.aliyun.com/
考慮到方便筆者一般是使用命令行的方式下載:
curl -O https://arthas.aliyun.com/arthas-boot.jar
完成后我們通過(guò)下面這個(gè)命令就可以將Arthas啟動(dòng)了。
java -jar arthas-boot.jar
此時(shí)我們就可以看到對(duì)應(yīng)的進(jìn)程序號(hào)和進(jìn)程的pid,以筆者為例,開(kāi)啟arthas之后就會(huì)看到一個(gè)序號(hào)為1的9121的Java進(jìn)程,我們可以直接點(diǎn)擊1并輸入回車對(duì)此進(jìn)程進(jìn)行監(jiān)控管理:
[INFO] JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9121 arthasExample.jar
隨后arthas初次會(huì)進(jìn)行相關(guān)依賴下載,然后我們就可以正式的使用arthas管理當(dāng)前進(jìn)程了:
2.離線用戶的使用姿勢(shì)(可選閱讀)
考慮到內(nèi)網(wǎng)用戶無(wú)法聯(lián)網(wǎng)進(jìn)行arthas初始化,所以arthas也人性化的提供了全量包的下載方式,有需要的讀者可移步到下載頁(yè)面選擇全量包下載即可獲取全量的arthas包:
完成后下載并解壓之后,我們可以直接通過(guò)下面這個(gè)腳本指令快速啟動(dòng)arthas:
./as.sh
3.常見(jiàn)指令介紹
步入arthas我們就可以進(jìn)行一些比較基礎(chǔ)的操作,以下是筆者日常用的比較多的指令,和Linux差不多,讀者可自行參閱了解
- cat:打印文件內(nèi)容。
- cls:清空當(dāng)前屏幕區(qū)域內(nèi)容。
- grep:匹配查找。
- history:打印歷史命令。
- pwd:輸入當(dāng)前Java進(jìn)程所在的位置。
- quit:退出當(dāng)前arthas客戶端。
- stop:關(guān)閉arthas服務(wù)端,所有arthas客戶端都會(huì)退出。
這里筆者就簡(jiǎn)單的演示一下,可以看到pwd輸出的就是筆者所監(jiān)控的Java進(jìn)程所處的文件目錄:
[arthas@9121]$ pwd
# 當(dāng)前監(jiān)控的進(jìn)程在服務(wù)器上的目錄
/home/sharkchili/arthasExample
[arthas@9121]$
又比如筆者通過(guò)memory查看堆內(nèi)存使用情況,如果只想看老年代的數(shù)據(jù),就可以使用grep:
memory |grep ps_old_gen
點(diǎn)擊quit會(huì)直接退出當(dāng)前進(jìn)程的客戶端,stop同理只不過(guò)多是連著服務(wù)端和其他客戶端一并殺掉,這里就不多做演示了:
[arthas@9121]$ quit
# 直接返回到服務(wù)器的目錄
sharkchili@DESKTOP-7IPKPVJ:~/arthas$
4.快捷啟動(dòng)配置
為了快捷啟動(dòng)arthas,筆者也給出個(gè)人的配置方式,首先vim一個(gè)名為as.sh的腳本,其內(nèi)容為arthas-boot.jar的啟動(dòng)指令:
java -jar /home/sharkchili/arthas/arthas-boot.jar
完成腳本編寫(xiě)確認(rèn)啟動(dòng)無(wú)誤之后,我們將這個(gè)腳本通過(guò)alias重命名的方式追加到/etc/profile下,內(nèi)容如下即sh指令加上上述腳本的全路徑:
alias as="sh /home/sharkchili/arthas/as.sh"
然后通過(guò)source指令使其生效:
可以看到完成這樣一段配直接后,我們可以直接通過(guò)as指令完成arthas的快捷啟動(dòng):
# 鍵入as
sharkchili@DESKTOP-xxx:~$ as
# 直接快速啟動(dòng)arthas
[INFO] JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9121 arthasExample.jar
三、Arthas中比較常用的運(yùn)維指令
1.查看實(shí)時(shí)數(shù)據(jù)面板
日常開(kāi)發(fā)維護(hù)過(guò)程中對(duì)于項(xiàng)目的巡檢還是蠻重要的,通過(guò)Arthas的dashboard可以非常直觀的查看當(dāng)前系統(tǒng)中進(jìn)程的運(yùn)行情況。
在arthas的控制面板輸入dashboard,默認(rèn)情況下5s進(jìn)行一次刷新:
這里我們來(lái)簡(jiǎn)單介紹一下第一板塊線程中的字段的含義:
- ID:java級(jí)別的線程id號(hào),注意與jstack中的native id的區(qū)別。
- NAME:線程名稱。
- GROUP:線程所在線程組名。
- PRIORITY:線程優(yōu)先級(jí),值越大優(yōu)先級(jí)越高。
- STATE:線程運(yùn)行狀態(tài)。
- CPU%:線程CPU使用率。
- DELTA_TIME:上次采樣之后,線程運(yùn)行的CPU時(shí)間,單位為秒。
- TIME:線程運(yùn)行的總CPU時(shí)長(zhǎng),數(shù)據(jù)格式為分:秒。
- INTERRUPTED:線程當(dāng)前的中斷位狀態(tài)。
- DAEMON :是否為守護(hù)線程。
第二板塊就是內(nèi)存使用版塊,記錄各個(gè)堆區(qū)、元空間的內(nèi)存使用情況以及GC情況。而第三板塊則是服務(wù)器運(yùn)行參數(shù)版塊,這一版塊記錄著程序當(dāng)前運(yùn)行服務(wù)器的內(nèi)核版本信息、jdk版本等。
需要了解的是arthas中的操作指令可以通過(guò)--help了解查閱,我們以dashboard為例,其使用說(shuō)明如下,可以看到我們可以通過(guò)-i決定面板刷新間隔(單位是毫秒),用-n決定面板刷新次數(shù):
所以如果我們希望每1s刷新1次,刷新5次,那么對(duì)應(yīng)的命令就是:
dashboard -i 1000 -n 5
2.查看JVM信息
arthas也可能非常直觀的查看jvm信息,對(duì)應(yīng)的指令也就是jvm。
同樣的這個(gè)指令也會(huì)輸出多個(gè)板塊的內(nèi)容,我們先來(lái)看看第一個(gè)板塊,可以可以看到該指令可以非常直觀的看到機(jī)器名稱、jvm啟動(dòng)時(shí)間、jdk版本以及我們配置jvm參數(shù)信息:
由于板塊比較多,這里筆者就說(shuō)幾個(gè)筆者比較常用的板塊,分別是線程板塊和文件描述符板塊,通過(guò)這兩個(gè)板塊筆者可以日常巡檢了解是否發(fā)生線程死鎖或者程序中是否出現(xiàn)資源未能及時(shí)關(guān)閉的情況:
- THREAD:它記錄當(dāng)前活躍線程數(shù)、活躍的守護(hù)線程數(shù)、從JVM啟動(dòng)開(kāi)啟曾經(jīng)活著的最大線程數(shù)、總共啟動(dòng)線程數(shù)以及發(fā)生死鎖的線程數(shù)。
- FILE-DESCRIPTOR:這個(gè)板塊記錄JVM可以打開(kāi)的最大文件描述符和當(dāng)前已經(jīng)打開(kāi)的文件描述符數(shù)。
3.查看和修改日志
logger指令也算是筆者比較喜歡的指令,它可以非常直觀的查看我們對(duì)于日志的配置,如下圖,以筆者當(dāng)前運(yùn)行的程序?yàn)槔梢钥吹饺缦聨讉€(gè)信息:
- 日志級(jí)別為INFO。
- 存儲(chǔ)錯(cuò)誤日志的ERROR_FILE的相對(duì)路徑。
- 存儲(chǔ)普通日志INFO_FILE的相對(duì)路徑。
當(dāng)然我們也可以查看指定名字的日志信息,例如我們想查看com.example.arthasExample.TestController的日志信息,就可以直接鍵入logger -n com.example.arthasExample.TestController指令進(jìn)行查看:
logger指令還有一個(gè)比較實(shí)用的功能,即直接修改日志級(jí)別,例如我們希望修改ROOT這個(gè)名稱的日志級(jí)別,就可以基于如下步驟完成修改:
- 獲取classLoader的哈希碼。
- 基于哈希碼通過(guò)logger指令修改日志級(jí)別。
我們程序中有這樣一段代碼,此時(shí)我們請(qǐng)求下面這個(gè)接口只會(huì)輸出info級(jí)別的日志:
@GetMapping(value = {"/user/logger"})
public String loggerPrint() {
log.info("info logger");
log.debug("debug logger");
return "success";
}
對(duì)應(yīng)的輸出結(jié)果為:
2024-08-19 23:53:29.454 INFO c.e.a.TestController :138 http-nio-8080-exec-1 info logger
接下來(lái)我們就直接通過(guò)arthas修改日志級(jí)別,首先我們需要獲取當(dāng)前classloader的哈希碼:
sc -d com.example.arthasExample.TestController
然后我們直接通過(guò)這個(gè)哈希碼,執(zhí)行如下指令將日志設(shè)置為debug
logger -c 306a30c7 --name ROOT --level debug
于是debug日志就出現(xiàn)了:
2024-08-20 00:13:31.438 INFO c.e.a.TestController :138 http-nio-8080-exec-6 info logger
2024-08-20 00:13:31.439 DEBUG c.e.a.TestController :139 http-nio-8080-exec-6 debug logger
4.查看JVM內(nèi)存信息
接下來(lái)就是memory指令,這也是筆者比較常用的指令之一,通過(guò)memory我們可以監(jiān)控到當(dāng)前內(nèi)存的使用情況: 如下圖所示,鍵入memory指令后我們就可以看到這些區(qū)域的內(nèi)存已用、總大小、最大值以及使用率等信息:
對(duì)應(yīng)的我們也給出上文中memory各行代表的含義:
- heap:堆區(qū)內(nèi)存。
- ps_eden_space:堆內(nèi)存中新生代Eden區(qū)。
- ps_survivor_space:堆內(nèi)存中新生代survivor區(qū)。
- ps_old_gen:堆內(nèi)存老年代區(qū)。
- nonheap:非堆內(nèi)存,即堆內(nèi)存之外的內(nèi)存。
- code_cache:因?yàn)镴ava執(zhí)行是將字節(jié)碼編譯為機(jī)器碼,而這個(gè)區(qū)域就是用于緩存這部分代碼。
- metaspace:元空間,以jdk1.8為例該空間是用于存儲(chǔ)Java類和方法的元數(shù)據(jù)信息、常量池等。
- compressed_class_space:存放類文件信息的區(qū)域。
對(duì)應(yīng)的我們?cè)谶@里也簡(jiǎn)單的復(fù)習(xí)一下JVM內(nèi)存區(qū)域的分布,建議讀者可參考下圖了解memory指令中各個(gè)字段的含義:
5.查看JVM的環(huán)境變量
arthas的sysenv指令常用于獲取系統(tǒng)環(huán)境變量信息,鍵入這條指令我們可以看到當(dāng)前java程序所使用的系統(tǒng)大部分環(huán)境變量信息,如下圖,可以看到大部分的當(dāng)前系統(tǒng)用戶名稱、編碼格式、當(dāng)前程序路徑以及客戶端ip和端口號(hào)等信息:
根據(jù)help的提示,這條指令同樣也支持查詢單個(gè)環(huán)境變量,不過(guò)意義不大,畢竟不是每個(gè)都知道環(huán)境變量叫什么,只有查看了才知道(笑):
[arthas@23543]$ sysenv PWD
KEY VALUE
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
PWD /home/sharkchili/arthasExample
6.查看和修改JVM系統(tǒng)屬性
sysprop查看的jvm的系統(tǒng)屬性,基本上通過(guò)這個(gè)指令我們可以看到大部分的JVM參數(shù)配置信息,如下輸出結(jié)果,我們大體可以看到JDK版本、程序名稱以及日志編碼格式和當(dāng)前系統(tǒng)用戶名稱等:
7.查看當(dāng)前JVM線程堆棧信息
arthas提供thread指令用于查看線程情況,如下所示,它基本打印了線程計(jì)數(shù)信息和幾個(gè)活躍的線程的實(shí)時(shí)情況,默認(rèn)情況下,它是按照CPU增量時(shí)間降序進(jìn)行排序:
按照help的提示,我們也可以通過(guò)-n打印前幾個(gè)忙碌的線程調(diào)用堆棧信息,如下所示,筆者希望打印出前2條忙碌的線程,鍵入的指令為thread -n 2:
同時(shí)arthas也支持按照時(shí)間間隔進(jìn)行輸出打印,比如我們希望列出5s內(nèi)最忙的3個(gè)線程,那么對(duì)應(yīng)的指令就是:
thread -n 3 -i 5000
當(dāng)我們的Java程序有大量的線程時(shí)候,我們希望篩選中某種狀態(tài)的線程,我們可以通過(guò)--state指定,例如我們希望打印處于RUNNABLE狀態(tài)的線程,那么我們就可以鍵入thread --state RUNNABLE來(lái)獲得輸出結(jié)果:
對(duì)于死鎖問(wèn)題,我們也可以通過(guò)-b指令來(lái)定位查看當(dāng)前程序是否存在阻塞其他線程的線程,如下圖所示,以筆者為例當(dāng)前程序就不存在死鎖的情況:
此時(shí)我們給出一個(gè)觸發(fā)死鎖的接口并調(diào)用:
@RequestMapping("dead-lock")
public void deadLock() {
//線程1先取得鎖1,休眠后取鎖2
new Thread(() -> {
synchronized (lock1) {
try {
log.info("t1 successfully acquired the lock1......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
log.info("t1 successfully acquired the lock1......");
}
}
}, "t1").start();
//線程2先取得鎖2,休眠后取鎖1
new Thread(() -> {
synchronized (lock2) {
try {
log.info("t2 successfully acquired the lock2......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
log.info("t2 successfully acquired the lock1......");
}
}
}, "t2").start();
}
此時(shí),鍵入-b指令就可以定位阻塞其他線程的線程以及所在代碼段:
8.vmtool對(duì)于JVM的調(diào)控
vmtool算是筆者用的比較多的一個(gè)工具指令,可用于查詢對(duì)象或者強(qiáng)制GC等功能,這些功能讀者可自行參考官網(wǎng)查閱:
vmtool:https://arthas.aliyun.com/doc/vmtool.html
而筆者這里想要介紹的是一個(gè)強(qiáng)制打斷線程的功能,這個(gè)指令對(duì)于特定場(chǎng)景下應(yīng)急處理還是蠻實(shí)用的。
我們的程序調(diào)用下面的接口被系統(tǒng)監(jiān)控到CPU100%,此時(shí)我們就可以通過(guò)arthas進(jìn)行特定場(chǎng)景下的應(yīng)急處理:
@RequestMapping("cpu-100")
public static void cpu() {
//無(wú)限循環(huán)輸出打印
new Thread(() -> {
while (true) {
log.info("cpu working");
}
}, "thread-1").start();
}
我們通過(guò)thread指令看到thread-1基本將單個(gè)CPU跑滿了,并且我們通過(guò)控制臺(tái)定位到對(duì)應(yīng)的id為48:
此時(shí)我們可以通過(guò)vmtool的action指令將線程打斷:
vmtool --action interruptThread -t 48
完成操作之后即可看到這個(gè)線程被我們成功打斷了:
同時(shí)vmTool也支持觀測(cè)變量的詳情,以下面這個(gè)實(shí)例變量dateTimeStr 為例,每次接口請(qǐng)求都會(huì)實(shí)時(shí)刷新:
private String dateTimeStr = DateUtil.formatDateTime(new Date());
@RequestMapping(value = "/getVal")
public String getVal() {
dateTimeStr = DateUtil.formatDateTime(new Date());
log.info("dateTimeStr: {}", dateTimeStr);
return "success";
}
如果我們希望查看此刻dateTimeStr 的值,我們就可以通過(guò)vmtool的action指定為getInstances ,然后指定類的全路徑(以筆者這段代碼為例則是com.example.arthasExample.TestController),最后鍵入表達(dá)式instances[0].dateTimeStr意為獲取當(dāng)前實(shí)例的dateTimeStr:
vmtool --action getInstances --className com.example.arthasExample.TestController --express 'instances[0].dateTimeStr'
此時(shí)我們就可以非常直觀的監(jiān)控到這個(gè)變量的信息了:
常見(jiàn)面試題:arthas統(tǒng)計(jì)方法耗時(shí)的原理是什么
watch指令示例如下:
watch com.example.MyService sayHello "{params, result, throwExp} -> { return 'Exception: ' + throwExp; }" -E
我們從指令即可看出他可以獲取到類的全路徑,對(duì)此我們不拿猜測(cè)它就是基于這個(gè)類的全路徑進(jìn)行一個(gè)字節(jié)碼樁技術(shù)對(duì)類進(jìn)行增強(qiáng),插入一段代碼進(jìn)行在方法執(zhí)行前后插入時(shí)間,實(shí)現(xiàn)耗時(shí)統(tǒng)計(jì)。