系統(tǒng)干崩了,只認(rèn)代碼不認(rèn)人
各位朋友聽我一句勸,寫代碼提供方法給別人調(diào)用時(shí),不管是內(nèi)部系統(tǒng)調(diào)用,還是外部系統(tǒng)調(diào)用,還是被動(dòng)觸發(fā)調(diào)用(比如MQ消費(fèi)、回調(diào)執(zhí)行等),一定要加上必要的條件校驗(yàn)。千萬(wàn)別信某些同事說(shuō)的這個(gè)條件肯定會(huì)傳、肯定有值、肯定不為空等等。這不,臨過(guò)年了我就被坑了一波,弄了個(gè)生產(chǎn)事故,年終獎(jiǎng)基本是涼了半截。
為了保障系統(tǒng)的高可用和穩(wěn)定,我發(fā)誓以后只認(rèn)代碼不認(rèn)人。文末總結(jié)了幾個(gè)小教訓(xùn),希望對(duì)你有幫助。
一、事發(fā)經(jīng)過(guò)
我的業(yè)務(wù)場(chǎng)景是:業(yè)務(wù)A有改動(dòng)時(shí),發(fā)送MQ,然后應(yīng)用自身接受到MQ后,再組合一些數(shù)據(jù)寫入到Elasticsearch。以下是事發(fā)經(jīng)過(guò):
(1) 收到一個(gè)業(yè)務(wù)A的異常告警,當(dāng)時(shí)的告警如下:
(2) 咋一看覺得有點(diǎn)奇怪,怎么會(huì)是Redis異常呢?然后自己連了下Redis沒有問(wèn)題,又看了下Redis集群,一切正常。所以就放過(guò)了,以為是偶然出現(xiàn)的網(wǎng)絡(luò)問(wèn)題。
(3) 然后技術(shù)問(wèn)題群里 客服 反饋有部分用戶使用異常,我警覺性的感覺到是系統(tǒng)出問(wèn)題了。趕緊打開了系統(tǒng),確實(shí)有偶發(fā)性的問(wèn)題。
(4) 于是我習(xí)慣性的看了幾個(gè)核心部件:
- 網(wǎng)關(guān)情況、核心業(yè)務(wù)Pod的負(fù)載情況、用戶中心Pod的負(fù)載情況。
- Mysql的情況:內(nèi)存、CPU、慢SQL、死鎖、連接數(shù)等。
(5) 果然發(fā)現(xiàn)了慢SQL和元數(shù)據(jù)鎖時(shí)間過(guò)長(zhǎng)的情況。找到了一張大表的全表查詢,數(shù)據(jù)太大,執(zhí)行太慢,從而導(dǎo)致元數(shù)據(jù)鎖持續(xù)時(shí)間太長(zhǎng),最終數(shù)據(jù)庫(kù)連接數(shù)快被耗盡。
SELECT xxx,xxx,xxx,xxx FROM 一張大表
(6) 立馬Kill掉幾個(gè)慢會(huì)話之后,發(fā)現(xiàn)系統(tǒng)仍然沒有完全恢復(fù),為啥呢?現(xiàn)在數(shù)據(jù)庫(kù)已經(jīng)正常了,怎么還沒完全恢復(fù)呢?又繼續(xù)看了應(yīng)用監(jiān)控,發(fā)現(xiàn)用戶中心的10個(gè)Pod里有2個(gè)Pod異常了,CPU和內(nèi)存都爆了。難怪使用時(shí)出現(xiàn)偶發(fā)性的異常呢。于是趕緊重啟Pod,先把應(yīng)用恢復(fù)。
(7) 問(wèn)題找到了,接下來(lái)就繼續(xù)排查為什么用戶中心的Pod掛掉了。從以下幾個(gè)懷疑點(diǎn)開始分析:
- 同步數(shù)據(jù)到Elasticsearch的代碼是不是有問(wèn)題,怎么會(huì)出現(xiàn)連不上Redis的情況呢?
- 會(huì)不會(huì)是異常過(guò)多,導(dǎo)致發(fā)送異常告警消息的線程池隊(duì)列滿了,然后就OOM?
- 哪里會(huì)對(duì)那張業(yè)務(wù)A的大表做不帶條件的全表查詢呢?
(8) 繼續(xù)排查懷疑點(diǎn)a,剛開始以為:是拿不到Redis鏈接,導(dǎo)致異常進(jìn)到了線程池隊(duì)列,然后隊(duì)列撐爆,導(dǎo)致OOM了。按照這個(gè)設(shè)想,修改了代碼,升級(jí),繼續(xù)觀察,依舊出現(xiàn)同樣的慢SQL 和 用戶中心被干爆的情況。因?yàn)闆]有異常了,所以懷疑點(diǎn)b也可以被排除了。
(9) 此時(shí)基本可以肯定是懷疑點(diǎn)c了,是哪里調(diào)用了業(yè)務(wù)A的大表的全表查詢,然后導(dǎo)致用戶中心的內(nèi)存過(guò)大,JVM來(lái)不及回收,然后直接干爆了CPU。同時(shí)也是因?yàn)槿頂?shù)據(jù)太大,導(dǎo)致查詢時(shí)的元數(shù)據(jù)鎖時(shí)間過(guò)長(zhǎng)造成了連接不能夠及時(shí)釋放,最終幾乎被耗盡。
(10) 于是修改了查詢業(yè)務(wù)A的大表必要校驗(yàn)條件,重新部署上線觀察。最終定位出了問(wèn)題。
二、問(wèn)題的原因
因?yàn)樵谧兏鼧I(yè)務(wù)B表時(shí),需要發(fā)送MQ消息( 同步業(yè)務(wù)A表的數(shù)據(jù)到ES),接受到MQ消息后,查詢業(yè)務(wù)A表相關(guān)連的數(shù)據(jù),然后同步數(shù)據(jù)到Elasticsearch。
但是變更業(yè)務(wù)B表時(shí),沒有傳業(yè)務(wù)A表需要的必要條件,同時(shí)我也沒有校驗(yàn)必要條件,從而導(dǎo)致了對(duì)業(yè)務(wù)A的大表的全表掃描。因?yàn)椋?/p>
某些同事說(shuō),“這個(gè)條件肯定會(huì)傳、肯定有值、肯定不為空...”,結(jié)果我真信了他!??!
由于業(yè)務(wù)B表當(dāng)時(shí)變更頻繁,發(fā)出和消費(fèi)的MQ消息較多,觸發(fā)了更多的業(yè)務(wù)A的大表全表掃描,進(jìn)而導(dǎo)致了更多的Mysql元數(shù)據(jù)鎖時(shí)間過(guò)長(zhǎng),最終連接數(shù)消耗過(guò)多。
同時(shí)每次都是把業(yè)務(wù)A的大表查詢的結(jié)果返回到用戶中心的內(nèi)存中,從而觸發(fā)了JVM垃圾回收,但是又回收不了,最終內(nèi)存和CPU都被干爆了。
至于Redis拿不到連接的異常也只是個(gè)煙霧彈,因?yàn)榘l(fā)送和消費(fèi)的MQ事件太多,瞬時(shí)間有少部分線程確實(shí)拿不到Redis連接。
最終我在消費(fèi)MQ事件處的代碼里增加了條件校驗(yàn),同時(shí)也在查詢業(yè)務(wù)A表處也增加了的必要條件校驗(yàn),重新部署上線,問(wèn)題解決。
三、總結(jié)教訓(xùn)
經(jīng)過(guò)此事,我也總結(jié)了一些教訓(xùn),與君共勉:
(1) 時(shí)刻警惕線上問(wèn)題,一旦出現(xiàn)問(wèn)題,千萬(wàn)不能放過(guò),趕緊排查。不要再去懷疑網(wǎng)絡(luò)抖動(dòng)問(wèn)題,大部分的問(wèn)題,都跟網(wǎng)絡(luò)無(wú)關(guān)。
(2) 業(yè)務(wù)大表自身要做好保護(hù)意識(shí),查詢處一定要增加必須條件校驗(yàn)。
(3) 消費(fèi)MQ消息時(shí),一定要做必要條件校驗(yàn),不要相信任何信息來(lái)源。
(4) 千萬(wàn)別信某些同事說(shuō),“這個(gè)條件肯定會(huì)傳、肯定有值、肯定不為空”等等。為了保障系統(tǒng)的高可用和穩(wěn)定,咱們只認(rèn)代碼不認(rèn)人。
(5) 一般出現(xiàn)問(wèn)題時(shí)的排查順序:
- 數(shù)據(jù)庫(kù)的CPU、死鎖、慢SQL。
- 應(yīng)用的網(wǎng)關(guān)和核心部件的CPU、內(nèi)存、日志。
(6) 業(yè)務(wù)的可觀測(cè)性和告警必不可少,而且必須要全面,這樣才能更快的發(fā)現(xiàn)問(wèn)題和解決問(wèn)題。