使用Arthas定位問題及分析
功能概述
首先,Arthas的常用功能大概有以下幾個(gè):
解決依賴沖突
- sc命令:模糊查看當(dāng)前 JVM 中是否加載了包含關(guān)鍵字的類,以及獲取其完全名稱。 sc -d 關(guān)鍵字注意使用 sc -d 命令,獲取
- classLoaderHash命令:通過 classloader 查看 class 文件來自哪個(gè) jar 包 注意 classloader -c后面的值填上面獲取到的 classLoaderHash值 查看線上運(yùn)行的代碼源碼,是否是預(yù)期的(確認(rèn)代碼是否提交,分支是否正確)
- jad --source-only:可以查看源代碼。
- watch命令:查看方法調(diào)用情況。后面跟上完全類名和方法名,以及一個(gè) OGNL的表達(dá)式,-f 表示不論正常返回還是異常返回都進(jìn)行觀察,-x 表示輸出結(jié)果的屬性遍歷深度,默認(rèn)為 1,建議無腦寫 4就行,最大的遍歷深度,再大就不支持了
- tt命令:觀測方法調(diào)用情況,tt命令可以查看「多次調(diào)用」并選擇其中一個(gè)進(jìn)行觀測,但是如果輸出結(jié)果是多層嵌套就沒辦法看了,而 watch 可以查看「多層嵌套」的結(jié)果。
這兩個(gè)命令都是用來查看方法調(diào)用過程的,不同的是 watch 命令是調(diào)用一次打印一次方法的調(diào)用情況,而 tt 命令可以先生成一個(gè)不斷增加的調(diào)用列表,然后指定其中某一項(xiàng)進(jìn)行觀測。
- 熱啟動(dòng)(類似JRebel)
redefine 命令:「熱替換」線上的代碼,注意應(yīng)用重啟之后會(huì)失效,這在某些緊急情況下會(huì)有奇效。比如說我們修改一下方法體里面的代碼,加了一行日志打印
- 看程序運(yùn)行時(shí)的整體情況
dashboard命令:可以查看當(dāng)前系統(tǒng)的實(shí)時(shí)數(shù)據(jù)面板,當(dāng)運(yùn)行在Ali-tomcat時(shí),會(huì)顯示當(dāng)前tomcat的實(shí)時(shí)信息,如HTTP請求的qps,rt,錯(cuò)誤數(shù),線程池信息,內(nèi)存使用情況,系統(tǒng)參數(shù)等等。
- 查看程序運(yùn)行時(shí)的jvm狀態(tài)
jvm 命令:可以查看 JVM 的實(shí)時(shí)運(yùn)行狀態(tài)。
- 定位應(yīng)用運(yùn)行中的熱點(diǎn)分析系統(tǒng)瓶頸
profiler 命令:支持生成應(yīng)用熱點(diǎn)的火焰圖。本質(zhì)上是通過不斷的采樣,然后把收集到的采樣結(jié)果生成火焰圖。
應(yīng)用實(shí)例
背景
項(xiàng)目使用了MumbleSDK 2.x,rmb請求先到一個(gè)Dispatcher類,然后Dispatcher根據(jù)請求參數(shù)里的bizServiceId把請求分發(fā)到不同的子服務(wù)接口. 各個(gè)子服務(wù)接口上有個(gè)@MumbleMessageService標(biāo)注著自己對應(yīng)的bizServiceId.
上個(gè)月有個(gè)一次性的補(bǔ)數(shù)需求,圖方便我就直接在子服務(wù)的類里用@Async寫了個(gè)異步方法,分發(fā)服務(wù)Dispatcher就識別不到@MumbleMessageService注解找不到子服務(wù)了. 根據(jù)組內(nèi)其他小伙伴的經(jīng)驗(yàn),是因?yàn)檫@個(gè)類被spring代理了導(dǎo)致的. 后來把異步方法抽到單獨(dú)的類實(shí)現(xiàn),服務(wù)就正常了.
但這個(gè)bug在測試環(huán)境沒有復(fù)現(xiàn)過,如果是代理問題,那么在什么環(huán)境都應(yīng)該復(fù)現(xiàn)才對,這篇文章就是尋找測試環(huán)境沒復(fù)現(xiàn)的原因,以及從源碼層面上分析為什么@Async會(huì)導(dǎo)致找不到子服務(wù)的注解.
本地調(diào)試
開發(fā)環(huán)境運(yùn)行后bug復(fù)現(xiàn)了,看了Dispatcher分發(fā)服務(wù)的源碼,原理是系統(tǒng)啟動(dòng)時(shí)掃描所有繼承了MumbleBaseService的類,然后遍歷實(shí)現(xiàn)類以及父類里的方法是否帶有@MumbleMessageService,如果有就放在緩存里,請求過來時(shí)就從緩存里取出對應(yīng)的服務(wù).
在掃描結(jié)束的位置加了斷點(diǎn),可以看到出問題的那個(gè)類由于有個(gè)方法用了@Async,類名帶有$Proxy,是個(gè)JDK動(dòng)態(tài)代理類. 而JDK動(dòng)態(tài)代理類和它的父類java.lang.reflect.Proxy 方法上都沒有@MumbleMessageService,所以不會(huì)被Dispatcher放進(jìn)緩存,子服務(wù)自然識別不到了.
那么測試環(huán)境的類是什么樣的呢?為什么注解能識別到呢? 使用神器Arthas試試.
使用Arthas
1. 首先使用sc命令查看jvm里加載的類信息
發(fā)現(xiàn)有個(gè)類名帶有 $Enhancer By Spring CG LIB EnhancerBySpringCGLIB EnhancerBySpringCGLIB,是cglib代理類,而本地調(diào)試時(shí)類名帶有$Proxy,是JDK代理類,這個(gè)差異很可能就是造成測試環(huán)境bug沒復(fù)現(xiàn)的原因. 而且有好多個(gè)在開發(fā)環(huán)境正常的類測試環(huán)境也變成代理類了. 應(yīng)該是有個(gè)地方統(tǒng)一給這些類做了增強(qiáng). 于是現(xiàn)在問題就變成了 哪里使用了cglib代理了這些類,而且只在測試環(huán)境才使用了呢?
我自己項(xiàng)目里的代碼里是沒這樣用的,可能是在某個(gè)引用的包里. 繼續(xù)挖.
2. 這次使用trace命令查看方法的調(diào)用鏈,想看看調(diào)用鏈里有沒有發(fā)現(xiàn)
輸入命令后,發(fā)送一筆請求,發(fā)現(xiàn)只有各個(gè)節(jié)點(diǎn)的耗時(shí)時(shí)長,沒有別的信息了. 官方文檔這個(gè)命令的說明是方法內(nèi)部調(diào)用路徑,并輸出方法路徑上的每個(gè)節(jié)點(diǎn)上耗時(shí),看來只能看到方法內(nèi)部的調(diào)用鏈,方法外的看不到,而我要找的是哪里增強(qiáng)了這個(gè)方法.
3. 接下去嘗試使用stack命令查詢方法被調(diào)用的調(diào)用路徑
下圖是發(fā)送請求后stack命令打印出來的東西,出現(xiàn)了一個(gè)mumbleSDK里的類,名字看起來就是使用了AOP切面
找到這個(gè)類源碼,就是它了! MumbleSDK里的dao,rmb調(diào)用耗時(shí)監(jiān)控組件,給項(xiàng)目里service目錄下的類都做了cglib代理,而且只有測試環(huán)境滿足了@Conditional里的條件所以開啟了.
讓我們驗(yàn)證下,在項(xiàng)目的配置文件里加上 mumble.monitor.web.enabled=false 關(guān)閉這個(gè)監(jiān)控服務(wù). 部署到測試環(huán)境后bug終于重現(xiàn)了. 再次使用sc查看,之前的cglib代理類已經(jīng)變成JDK代理了
4. 用jad命令反編譯兩種不同的代理類
下圖是cglib的,可以看到繼承的父類是原來的類. 再復(fù)習(xí)下MumbleSDK Dispatcher識別服務(wù)的原理: 遍歷實(shí)現(xiàn)類以及父類的方法掃描@MumbleMessageService注解. 所以可以識別到方法上的@MumbleMessageService并把子服務(wù)加進(jìn)緩存. 這就是一開始測試環(huán)境能識別到子服務(wù)的原因.
下圖是jdk代理類,父類是Proxy,方法上沒有@MumbleMessageService. 也就會(huì)出現(xiàn)找不到子服務(wù)的問題了.
所以這個(gè)bug的根本原因是不同類型的動(dòng)態(tài)代理的實(shí)現(xiàn)差異導(dǎo)致的,而不是一開始認(rèn)為的單純是因?yàn)楸淮砹?
下圖是@EnableAsync里的代碼,默認(rèn)是jdk代理.
回到本地開發(fā)環(huán)境,把@EnableAsync改成 @EnableAsync(proxyTargetClass = true),強(qiáng)制使用cglib代理. 重啟服務(wù),開發(fā)環(huán)境的服務(wù)也正常了.
但是,為了能亂放@Async而去改spring的默認(rèn)代理配置是不合理的,還是要把@Async方法獨(dú)立出去.
Arthas Idea插件
命令或類名太長記不得可以安裝使用Aethas的idea插件,如下圖,在方法上右鍵選中相應(yīng)的命令,就可以把命令復(fù)制到剪貼板,直接去終端粘貼使用就行了. 比如下圖粘貼的結(jié)果是
stack cn.webank.pmbank.cp.ocr.service.impl.OcrCorePojoService DoCommonOcr -n 5
總結(jié)
以前只了解過兩種動(dòng)態(tài)代理的實(shí)現(xiàn)機(jī)制及區(qū)別,沒感受過這種區(qū)別對系統(tǒng)運(yùn)行造成的影響. 就這個(gè)bug來說,是代理類的父類不同造成的.以后如果遇到這類問題也多了個(gè)debug思路.
Arthas真香. 以前debug時(shí)用的笨方法都可以用它代替. 比如定位接口耗時(shí)長問題,不用在代碼里一段段打印耗時(shí)日志再重新部署了,一行trace命令就可以打印出各個(gè)鏈路的耗時(shí); 比如不確定部署的代碼是不是剛才更新的,可以使用jad反編譯查看變更的類.
帶有@Async @Schedule @Transation 等注解的方法最好分類放到單獨(dú)的類里,比如專門的異步任務(wù)類,定時(shí)任務(wù)類等.不僅能避免代理方面的問題,也能使代碼結(jié)構(gòu)更清晰整潔.
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用feign發(fā)送http請求解析報(bào)錯(cuò)的問題
這篇文章主要介紹了使用feign發(fā)送http請求解析報(bào)錯(cuò)的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Java基于享元模式實(shí)現(xiàn)五子棋游戲功能實(shí)例詳解
這篇文章主要介紹了Java基于享元模式實(shí)現(xiàn)五子棋游戲功能,較為詳細(xì)的分析了享元模式的概念、功能并結(jié)合實(shí)例形式詳細(xì)分析了Java使用享元模式實(shí)現(xiàn)五子棋游戲的具體操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2018-05-05Mybatis-plus與Mybatis依賴沖突問題解決方法
,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧這篇文章主要介紹了Mybatis-plus與Mybatis依賴沖突問題解決方法2021-04-04Mybatis內(nèi)置參數(shù)之_parameter和_databaseId的使用
這篇文章主要介紹了Mybatis內(nèi)置參數(shù)之_parameter和_databaseId的使用方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12Java實(shí)現(xiàn)文件批量重命名,移動(dòng)和刪除
這篇文章主要為大家介紹了如何利用Java語言實(shí)現(xiàn)批量重命名,批量移動(dòng)文件,批量刪除tmp文件等功能,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-08-08