事故現(xiàn)場
202-11-19 系統(tǒng)接收到大量的超時告警, 同時業(yè)務(wù)群里面也有很多客戶反饋服務(wù)不可用。
開始排查
首先上grafana上面查看整體的服務(wù)狀態(tài)艺配,
從圖中可以看出一點問題來捏鱼,CPU幾乎沒有波動, tomcat線程數(shù)急劇上升, 系統(tǒng)的ops急劇下降唧喉。 這種是比較典型的資源阻塞類問題,為了印證這個想法忍抽,我們再看下當時的系統(tǒng)的GC情況
從上面的GC情況下八孝,我們可以看出來,GC還是比較平穩(wěn)的鸠项,整體的停頓市場也不多干跛,平均在100ms以下,雖然不算好祟绊,但是肯定不會造成系統(tǒng)有如此大的停頓
服務(wù)器出現(xiàn)故障排查方法:
服務(wù)器出現(xiàn)故障楼入,先看CPU,如果CPU持續(xù)高漲牧抽,那么肯定是服務(wù)內(nèi)部出現(xiàn)了問題嘉熊,這個時候可以按照網(wǎng)上的常規(guī)解決方法,top -HP pid
查看?耗CPU比較嚴重的線程扬舒,然后導(dǎo)出對應(yīng)的線程棧信息阐肤,就
可以根據(jù)實際的業(yè)務(wù)去分析了, 這種屬于比較直觀的
比較隱晦的資源阻塞問題讲坎,此類問題分為如下兩種:
- 比較好排查的孕惜,即使接口慢,比如接口調(diào)用耗費時間久的外部接口晨炕,有大量慢SQL衫画,這些都會間接的導(dǎo)致整體吞吐量下降,最終導(dǎo)致tomcat線程池線程池耗盡
- 第二種就更加隱晦了瓮栗,CPU削罩,帶寬,流量费奸,慢SQL鲸郊,內(nèi)存各方面都很正常,但是tomcat線程池直線上升货邓,最終服務(wù)器資源耗盡秆撮。 這種情況我之前有專門寫過一片文章』豢觯《tomcat線程池排查》
這種情況可以考慮使用jstack命令职辨,導(dǎo)出堆棧信息盗蟆,這里推薦一款工具 gceasy , 可以清晰的分析出線程的狀態(tài)分布舒裤,可以很好的知道線程都堵在什么地方了喳资。
通過上面的已知條件和我們過往的經(jīng)驗,基本上可以判定是有一些接口阻塞導(dǎo)致整體系統(tǒng)處理能力急劇下降腾供。 首先想到的就是慢SQL仆邓。
登陸到阿里云的RDS控制臺上,查看RDS的運行狀況
果不其然伴鳖,在那個時間段节值,CPU已經(jīng)到了100%了,基本上可以確定是慢查詢的問題榜聂, 在慢查詢的控制臺上搞疗,立馬可以看到當前系統(tǒng)阻塞的SQL。觸目驚心须肆,真的不知道是哪個兔崽子寫的SQL匿乃。
罪魁禍首就是這條SQL
SELECT
o.*
FROM
`jm_order` o
LEFT JOIN jm_order_unregistered_driver d ON o.number = d.orderNumber
WHERE
o.valid = 1
AND o.state = 1
AND o.driver_uid = 0
AND o.agents_uid = 0
AND d.driverPhone IS NULL
AND o.is_push_regular_car = 0
AND o.is_lock = 2
AND (
o.from_date > '2020-11-18'
OR (
o.from_date = '2020-11-18'
AND o.from_day >= 16
))
AND o.type IN (
11,
12)
通過explain
關(guān)鍵字查詢執(zhí)行計劃
問題一目了然了,看我紅線框起來的地方豌汇,這個就是問題所在幢炸,我們可以分析下這個SQL,這個SQL
里面有兩張表拒贱,使用了left join 宛徊, 其他的倒是沒什么問題,看索引走向以及掃描函數(shù)柜思,其實看上去都沒啥問題岩调,不應(yīng)該耗時這么久巷燥。
但是看紅色框起來的部分Using where; Using join buffer (Block Nested Loop)
, 這句話什么意思赡盘?
下面給大家講一下mysql在表連接的時候使用的算法,同時也讓大家理解一下為什么有小表驅(qū)動大表
的說法
mysql表關(guān)聯(lián)算法
Simple Nested Loop算法
這個算法缰揪,屬于簡單嵌套循環(huán)陨享, 說白了就是外層表的結(jié)果作為第一層循環(huán),內(nèi)層表作為第二層循環(huán)钝腺,然后就這樣硬干抛姑,
for (Table t:table) {
for(Join x:joinTable){
if(t==x){
//xxxx ,說明匹配到了數(shù)據(jù)
for(){
// 如果有三種表關(guān)聯(lián)的話艳狐。
}
}
}
}
上面這種暴力關(guān)聯(lián)的方法定硝,可想而知效率那是差的一逼,基本上mysql官方也不會使用這種方式的毫目。
執(zhí)行順序:
- 先遍歷table1
- 遍歷table得到的結(jié)果蔬啡,逐條遍歷table2
- 遍歷完table2之后呢诲侮,繼續(xù)逐條遍歷table3, 返回最終的結(jié)果
執(zhí)行次數(shù)基本上是: table1 * table2 * table3
Block Nested-Loop
這種算法,就是本文中生產(chǎn)環(huán)境實際遇到的箱蟆,mysql默認在沒有建立索引
上面使用的算法, 這種做法和簡單嵌套循環(huán)有一點不同沟绪,就是加了 緩存塊
, 減少了循環(huán)次數(shù) 空猜, 將驅(qū)動表的數(shù)據(jù)緩存到 join Buffer
里面去绽慈,然后拿Join Buffer
里面的數(shù)據(jù)和內(nèi)層關(guān)聯(lián)表進行匹配,
for (Table t:table) {
// store t in Join buffer ,
// 當緩沖池滿了辈毯,執(zhí)行匹配
if(Join Buffer is full){
for(Join x:joinTable){
if(t in Buffer){
//xxxx 坝疼,說明匹配到了數(shù)據(jù)
for(){
// 如果有三種表關(guān)聯(lián)的話。
}
}
clear join buffer // 清空緩存池
}
}
}
從這里可以看到漓摩,使用了緩存池的話裙士,減少了很多次數(shù),比如:驅(qū)動表100條數(shù)據(jù)管毙,被驅(qū)動表50條數(shù)據(jù)腿椎,那么如果沒有Join Buffer
的話,讀表次數(shù):100 * 50夭咬, 加了Join buffer
之后啃炸,如果Join buffer
的大小可以存儲50條數(shù)據(jù),那么讀表次數(shù)就是: 100/50 * 50 卓舵, 讀表次數(shù)減少了一個數(shù)量級的南用。
需要注意的是,只有在 連表鍵上沒有索引的時候會采用這種方式
掏湾, 也就是本文出現(xiàn)的情況裹虫。
Index Nested-loop
索引嵌套循環(huán),簡稱 INL融击, 說白了就是 連表鍵上有索引筑公,就直接走索引去做嵌套查詢
, 下面我畫一張圖來解釋
驅(qū)動表得到結(jié)果之后尊浪,是直接去索引樹上找對應(yīng)的被驅(qū)動表的記錄匣屡,如果可以使用覆蓋索引的話,那么就不用再做回表了拇涤,這種情況下捣作,效率是相當高的。
得出以上結(jié)果鹅士,立馬給orderNumber
加上索引券躁,走索引嵌套算法就可以了, 系統(tǒng)馬上就恢復(fù)正常了。
看完上面的文字也拜,這下大家明白了為啥有小表驅(qū)動大表的說法嗎旭贬?