事故現(xiàn)場
202-11-19 系統(tǒng)接收到大量的超時(shí)告警, 同時(shí)業(yè)務(wù)群里面也有很多客戶反饋服務(wù)不可用。

開始排查
首先上grafana上面查看整體的服務(wù)狀態(tài),

從圖中可以看出一點(diǎn)問題來,CPU幾乎沒有波動(dòng), tomcat線程數(shù)急劇上升, 系統(tǒng)的ops急劇下降。 這種是比較典型的資源阻塞類問題,為了印證這個(gè)想法,我們?cè)倏聪庐?dāng)時(shí)的系統(tǒng)的GC情況

從上面的GC情況下,我們可以看出來,GC還是比較平穩(wěn)的,整體的停頓市場也不多,平均在100ms以下,雖然不算好,但是肯定不會(huì)造成系統(tǒng)有如此大的停頓
服務(wù)器出現(xiàn)故障排查方法:
服務(wù)器出現(xiàn)故障,先看CPU,如果CPU持續(xù)高漲,那么肯定是服務(wù)內(nèi)部出現(xiàn)了問題,這個(gè)時(shí)候可以按照網(wǎng)上的常規(guī)解決方法,top -HP pid 查看?耗CPU比較嚴(yán)重的線程,然后導(dǎo)出對(duì)應(yīng)的線程棧信息,就
可以根據(jù)實(shí)際的業(yè)務(wù)去分析了, 這種屬于比較直觀的
比較隱晦的資源阻塞問題,此類問題分為如下兩種:
- 比較好排查的,即使接口慢,比如接口調(diào)用耗費(fèi)時(shí)間久的外部接口,有大量慢SQL,這些都會(huì)間接的導(dǎo)致整體吞吐量下降,最終導(dǎo)致tomcat線程池線程池耗盡
- 第二種就更加隱晦了,CPU,帶寬,流量,慢SQL,內(nèi)存各方面都很正常,但是tomcat線程池直線上升,最終服務(wù)器資源耗盡。 這種情況我之前有專門寫過一片文章?!秚omcat線程池排查》
這種情況可以考慮使用jstack命令,導(dǎo)出堆棧信息,這里推薦一款工具 gceasy , 可以清晰的分析出線程的狀態(tài)分布,可以很好的知道線程都堵在什么地方了。
通過上面的已知條件和我們過往的經(jīng)驗(yàn),基本上可以判定是有一些接口阻塞導(dǎo)致整體系統(tǒng)處理能力急劇下降。 首先想到的就是慢SQL。
登陸到阿里云的RDS控制臺(tái)上,查看RDS的運(yùn)行狀況

果不其然,在那個(gè)時(shí)間段,CPU已經(jīng)到了100%了,基本上可以確定是慢查詢的問題, 在慢查詢的控制臺(tái)上,立馬可以看到當(dāng)前系統(tǒng)阻塞的SQL。觸目驚心,真的不知道是哪個(gè)兔崽子寫的SQL。

罪魁禍?zhǔn)拙褪沁@條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í)行計(jì)劃

問題一目了然了,看我紅線框起來的地方,這個(gè)就是問題所在,我們可以分析下這個(gè)SQL,這個(gè)SQL
里面有兩張表,使用了left join , 其他的倒是沒什么問題,看索引走向以及掃描函數(shù),其實(shí)看上去都沒啥問題,不應(yīng)該耗時(shí)這么久。
但是看紅色框起來的部分Using where; Using join buffer (Block Nested Loop) , 這句話什么意思?
下面給大家講一下mysql在表連接的時(shí)候使用的算法,同時(shí)也讓大家理解一下為什么有小表驅(qū)動(dòng)大表的說法
mysql表關(guān)聯(lián)算法
Simple Nested Loop算法
這個(gè)算法,屬于簡單嵌套循環(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官方也不會(huì)使用這種方式的。

執(zhí)行順序:
- 先遍歷table1
- 遍歷table得到的結(jié)果,逐條遍歷table2
- 遍歷完table2之后呢,繼續(xù)逐條遍歷table3, 返回最終的結(jié)果
執(zhí)行次數(shù)基本上是: table1 * table2 * table3
Block Nested-Loop
這種算法,就是本文中生產(chǎn)環(huán)境實(shí)際遇到的,mysql默認(rèn)在沒有建立索引上面使用的算法, 這種做法和簡單嵌套循環(huán)有一點(diǎn)不同,就是加了 緩存塊 , 減少了循環(huán)次數(shù) , 將驅(qū)動(dòng)表的數(shù)據(jù)緩存到 join Buffer里面去,然后拿Join Buffer 里面的數(shù)據(jù)和內(nèi)層關(guān)聯(lián)表進(jìn)行匹配,
for (Table t:table) {
// store t in Join buffer ,
// 當(dāng)緩沖池滿了,執(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ū)動(dòng)表100條數(shù)據(jù),被驅(qū)動(dòng)表50條數(shù)據(jù),那么如果沒有Join Buffer的話,讀表次數(shù):100 * 50, 加了Join buffer之后,如果Join buffer的大小可以存儲(chǔ)50條數(shù)據(jù),那么讀表次數(shù)就是: 100/50 * 50 , 讀表次數(shù)減少了一個(gè)數(shù)量級(jí)的。
需要注意的是,只有在 連表鍵上沒有索引的時(shí)候會(huì)采用這種方式 , 也就是本文出現(xiàn)的情況。
Index Nested-loop
索引嵌套循環(huán),簡稱 INL, 說白了就是 連表鍵上有索引,就直接走索引去做嵌套查詢 , 下面我畫一張圖來解釋

驅(qū)動(dòng)表得到結(jié)果之后,是直接去索引樹上找對(duì)應(yīng)的被驅(qū)動(dòng)表的記錄,如果可以使用覆蓋索引的話,那么就不用再做回表了,這種情況下,效率是相當(dāng)高的。
得出以上結(jié)果,立馬給orderNumber 加上索引,走索引嵌套算法就可以了, 系統(tǒng)馬上就恢復(fù)正常了。
看完上面的文字,這下大家明白了為啥有小表驅(qū)動(dòng)大表的說法嗎?