佛山高端網(wǎng)站建設(shè)google搜索引擎入口下載
功能概述
首先,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)行觀測。
- 熱啟動(類似JRebel)
redefine 命令:「熱替換」線上的代碼,注意應(yīng)用重啟之后會失效,這在某些緊急情況下會有奇效。
比如說我們修改一下方法體里面的代碼,加了一行日志打印
- 看程序運(yùn)行時(shí)的整體情況
dashboard命令:可以查看當(dāng)前系統(tǒng)的實(shí)時(shí)數(shù)據(jù)面板,當(dāng)運(yùn)行在Ali-tomcat時(shí),會顯示當(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會導(dǎo)致找不到子服務(wù)的注解.
本地調(diào)試
開發(fā)環(huán)境運(yùn)行后bug復(fù)現(xiàn)了, 看了Dispatcher分發(fā)服務(wù)的源碼, 原理是系統(tǒng)啟動時(shí)掃描所有繼承了MumbleBaseService的類, 然后遍歷實(shí)現(xiàn)類以及父類里的方法是否帶有@MumbleMessageService, 如果有就放在緩存里, 請求過來時(shí)就從緩存里取出對應(yīng)的服務(wù).
在掃描結(jié)束的位置加了斷點(diǎn), 可以看到出問題的那個(gè)類由于有個(gè)方法用了@Async, 類名帶有$Proxy, 是個(gè)JDK動態(tài)代理類. 而JDK動態(tài)代理類和它的父類java.lang.reflect.Proxy 方法上都沒有@MumbleMessageService, 所以不會被Dispatcher放進(jìn)緩存, 子服務(wù)自然識別不到了.
那么測試環(huán)境的類是什么樣的呢?為什么注解能識別到呢? 使用神器Arthas試試.
使用Arthas
1. 首先使用sc命令查看jvm里加載的類信息
發(fā)現(xiàn)有個(gè)類名帶有 $EnhancerBySpringCGLIBEnhancerBySpringCGLIBEnhancerBySpringCGLIB, 是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. 也就會出現(xiàn)找不到子服務(wù)的問題了.
所以這個(gè)bug的根本原因是不同類型的動態(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é)
- 以前只了解過兩種動態(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)更清晰整潔.