近期查了一個(gè)Java性能的case,在此記錄下。場(chǎng)景是一個(gè)query,查詢db,然后聚合計(jì)算,返回結(jié)果給用戶,現(xiàn)象是大規(guī)模query超時(shí)。
統(tǒng)計(jì)query各階段耗時(shí)
一次query分為scanAndMerge、groupBy、aggregation三個(gè)階段,通過(guò)統(tǒng)計(jì)發(fā)現(xiàn)主要耗時(shí)在ScanAndMerge階段。
統(tǒng)計(jì)scan與merge耗時(shí)
scanAndMerge主要邏輯是一個(gè)while循環(huán),從一個(gè)BlockingQueue取數(shù)據(jù)(有一組writer異步向BlockingQueue寫(xiě)數(shù)據(jù)),然后merge到一個(gè)map的數(shù)據(jù)結(jié)構(gòu)。通過(guò)統(tǒng)計(jì)發(fā)現(xiàn)主要耗時(shí)發(fā)生在merge結(jié)算。
jprofile統(tǒng)計(jì)cpu熱點(diǎn)
使用jprofile統(tǒng)計(jì)發(fā)現(xiàn)merge邏輯確實(shí)是cpu熱點(diǎn),merge有十萬(wàn)次調(diào)用,而merge內(nèi)部的一些函數(shù)調(diào)用有千萬(wàn)次調(diào)用,懷疑merge內(nèi)部實(shí)現(xiàn)時(shí)間復(fù)雜度較高。
看merge實(shí)現(xiàn)代碼
merge內(nèi)部有兩路邏輯,遞增插入時(shí)間序列命中邏輯A,時(shí)間復(fù)雜度低;亂序時(shí)間序列命中邏輯B,時(shí)間復(fù)雜度高。正常情況下,均為遞增數(shù)據(jù),不應(yīng)命中邏輯B;但是從merge內(nèi)部時(shí)間復(fù)雜度來(lái)看,疑似命中邏輯B,疑似插入時(shí)間序列亂序。在某個(gè)query,100%復(fù)現(xiàn)此case,故決定debug對(duì)插入時(shí)間序列進(jìn)行驗(yàn)證。
嘗試使用intellij進(jìn)行debug
嘗試使用intellij進(jìn)行debug,因?yàn)榉?wù)器與mac間網(wǎng)絡(luò)太差,intellij debug需回傳大量class信息,導(dǎo)致不可用,于是放棄。
使用jdb進(jìn)行debug
在服務(wù)器上使用jdb對(duì)jvm進(jìn)行進(jìn)行debug,通過(guò)打斷點(diǎn),打印插入數(shù)據(jù)變量,發(fā)現(xiàn)插入時(shí)間序列確實(shí)為亂序,且有大量重復(fù)數(shù)據(jù)。
代碼分析
代碼邏輯很簡(jiǎn)單,從db去數(shù)據(jù)然后進(jìn)行merge,且為了提高并發(fā)會(huì)有n路此流程進(jìn)行。故亂序可能有兩種原因?qū)е?,一個(gè)是db返回的數(shù)據(jù)確實(shí)為亂序,另一個(gè)是并行n路程有沖突。
驗(yàn)證db返回?cái)?shù)據(jù)
與db間采用thrift協(xié)議通信,故模擬java程序快速寫(xiě)了一個(gè)python程序從db取數(shù)據(jù),發(fā)現(xiàn)所取數(shù)據(jù)并無(wú)重復(fù),也無(wú)亂序。且負(fù)責(zé)db的同學(xué)看代碼也非常確定,故db返回?cái)?shù)據(jù)基本確定無(wú)問(wèn)題。故懷疑n路并行查詢邏輯有問(wèn)題。
最后確認(rèn)是n路并行查詢邏輯的問(wèn)題
看配置文件,db有2個(gè)shard,內(nèi)部建立了2個(gè)shard client。但是有8路并行查詢,每個(gè)查詢邏輯對(duì)應(yīng)2個(gè)shard client中的一個(gè),導(dǎo)致有4路查詢都是對(duì)應(yīng)一個(gè)shard client。故當(dāng)某一個(gè)組查詢一批時(shí)序數(shù)據(jù)后(命中時(shí)間復(fù)雜度低的邏輯A),會(huì)再有3組查詢插入相同的時(shí)序數(shù)據(jù)(命中時(shí)間復(fù)雜度高的邏輯B),導(dǎo)致整體查詢小時(shí)間復(fù)雜度過(guò)高。
問(wèn)題解決
解決問(wèn)題的方法很簡(jiǎn)單,暫時(shí)把8路并發(fā)查詢改為2路即可。
總結(jié)
此類似case只是簡(jiǎn)單地去分析慢的原因,思路總結(jié)如下:
- 確定是否gc有問(wèn)題,如有先解決gc問(wèn)題
- 看代碼確定哪個(gè)線程慢
- 看該線程的函數(shù)cpu熱點(diǎn)
- 如果沒(méi)有熱點(diǎn),該線程可能與其他線程有鎖,可以看jvm各線程狀態(tài)時(shí)序圖、分析jstack
- 如果有熱點(diǎn),則可能有同步IO請(qǐng)求,或者高時(shí)間復(fù)雜度邏輯
附錄
jprofile使用方法
服務(wù)器端安裝jprofile程序,執(zhí)行jpenable命令,選擇要profile的jvm pid,輸入要監(jiān)聽(tīng)的端口。
在本地啟動(dòng)jprofile圖形界面,輸入ip、port,進(jìn)行profile即可。
intellij debug使用方法
服務(wù)器端java進(jìn)程啟動(dòng)參數(shù)增加
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
在本地打開(kāi)intellij,配置debug的ip、port,然后啟動(dòng)debug,可打斷點(diǎn)進(jìn)行調(diào)試。
jdb使用方法
服務(wù)器端java進(jìn)程同intellij debug增加如下參數(shù)
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
上傳java源代碼到服務(wù)器端,使用jdb命令進(jìn)行調(diào)試
jdb -sourcepath sourcecode/src/main/java/ -attach localhost:5005
常用jdb command
stop at <class full name>:<line number> // 開(kāi)啟斷點(diǎn)
clear <class full name>:<line number> // 清除斷點(diǎn)
list // 顯示當(dāng)前代碼配置
print // 打印變量值
next // 下一個(gè)
cont // 跳過(guò)本次斷點(diǎn)