Java性能調(diào)優(yōu)小計(jì)

近期查了一個(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é)如下:

  1. 確定是否gc有問(wèn)題,如有先解決gc問(wèn)題
  2. 看代碼確定哪個(gè)線程慢
  3. 看該線程的函數(shù)cpu熱點(diǎn)
  4. 如果沒(méi)有熱點(diǎn),該線程可能與其他線程有鎖,可以看jvm各線程狀態(tài)時(shí)序圖、分析jstack
  5. 如果有熱點(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)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容