一個 Bug 改了三次,汗流浹背了。。
共 4385字,需瀏覽 9分鐘
·
2024-07-30 17:00
大家好,我是程序員魚皮。還記得么?幾天前我寫了一篇文章 《一下午連續(xù)故障兩次,誰把我們接口堵死了?!》 ,我從團隊管理的視角給大家分享了我們的編程導航網(wǎng)站線上故障的經(jīng)歷和解決方案。
雖然這次的故障解決的效率不高,但還是得表揚下我團隊的開發(fā)同學,認真寫了一篇完整的復盤文章。今天,咱們就換一個視角,來看看親身經(jīng)歷和解決故障的開發(fā)同學,他的心路歷程是怎樣的?
四個字總結(jié):汗流浹背 !
昨天是個汗流浹背的周一,我們的后端服務竟然在一天內(nèi)三次不可用,最長的一次達到了四十分鐘!到底發(fā)生了什么?且聽我細說。
在每周例行的需求評審會后,我就開始對我的需求進行排期,規(guī)劃這周要做的工作,當我開始著手規(guī)劃時,突然發(fā)現(xiàn)線上服務有幾個沒人發(fā)現(xiàn)的后端小 bug,于是我開始了悄無聲息的改 bug。正當我改的上頭時,同事小 y 突然喊我:線上怎么訪問不了了!
我猛然一驚,我擦,不會是那幾個小 bug 給線上干崩了吧?但是轉(zhuǎn)念一想應該也不是,我趕緊放下手上的工作,開始排查線上服務。
第一次排查
定位問題
1、登錄網(wǎng)站,確認問題
我趕緊先登錄我們的網(wǎng)站,發(fā)現(xiàn)確實訪問不了。打開網(wǎng)絡控制臺發(fā)現(xiàn)前端的資源響應很快:
而后端卻一直在 pending,如圖:
2、登錄容器平臺,查看后端服務狀態(tài)
由于我們的后端部署在云托管容器平臺,于是我下意識的先去平臺查看服務的狀態(tài),發(fā)現(xiàn)我們服務的平均響應竟然達到了 21 秒!
然后我合理推測肯定是 qps 猛增,結(jié)果發(fā)現(xiàn),qps 很穩(wěn)定,再看看內(nèi)存、CPU 占用都還算平滑:
3、登錄接口監(jiān)控平臺定位具體問題
此時我已經(jīng)發(fā)覺事情不太對,既然 qps、內(nèi)存、CPU 都還算正常,那怎么接口響應這么慢?不過我們后端服務接入了某云服務商的應用實時監(jiān)控服務,我趕緊進入控制臺查看詳細的數(shù)據(jù),一進去就發(fā)現(xiàn)平均每分鐘的響應時間達到了 16.2s:
此時我的心理:
但是很快我鎮(zhèn)定下來,我要一點點排查到問題所在,正好這個接口監(jiān)控平臺提供了這些監(jiān)控,我就一個一個點進去看都有啥問題。
4、查看 jvm 監(jiān)控
于是我趕緊打開 jvm 監(jiān)控定位下問題,我直呼好家伙,怎么每隔五分鐘 FullGC 一次?因為每次 FullGC 都會暫停應用程序的執(zhí)行,這么頻繁的 FullGC 顯然是有問題的,怪不得線上接口一直無法訪問。
5、查看線程池監(jiān)控
但是光看 jvm 監(jiān)控也定位不到問題,我需要趕緊找到 FullGC 的根本原因,于是我點開了線程池監(jiān)控,好家伙,這 TM 所有的線程全都上場了,甚至還有一堆在排隊的。。。
這是我更不能能理解了,到底是什么阻塞了所有的線程?
6、 查看數(shù)據(jù)庫連接池監(jiān)控
帶著疑問,我又打開了數(shù)據(jù)庫連接池監(jiān)控,好家伙,什么鬼?為什么連接池滿了?
解決問題(bushi)
看到數(shù)據(jù)庫連接池全部爆滿,我就知道肯定是在查數(shù)據(jù)的時候,所有的請求都在等待連接池空閑,也就導致線程全部阻塞,最終導致頻繁 FullGC,但是也不合理,因為所有的數(shù)據(jù)庫請求按理來說都會自動釋放掉鏈接呀,為什么連接池會滿呢?但是這時候線上事故已經(jīng)發(fā)生很久了,我得先讓用戶能訪問網(wǎng)站再說,要不然用戶還不得罵死我,我趕緊在 Spring Boot 的 application.yml 中配置數(shù)據(jù)庫連接池的最大容量為 20,如下:
spring:
hikari:
maximum-pool-size: 20
然后發(fā)布到線上,很快線上就可以正常訪問了,使用很絲滑~
第二次排查
我本來也以為事情告一段落,可以繼續(xù)修我的 bug 了,順便我還跟同時吐槽了一波,hikaricp 就是不好用,回頭要是有機會一定得換成 druid,還能監(jiān)控 sql、防 sql 注入之類的。。。剛吐槽著,同事小 y 又高呼:線上又卡住了,又不行了,快去看看!
我心里一沉,好家伙,不給我喘息的機會是吧,我趕緊去看看數(shù)據(jù)庫連接池監(jiān)控:
怎么回事?20 個連接都不夠用?這不對吧?到底是誰一直占著連接不放手!于是我趕緊進入容器平臺的 webshell,使用 jstack 看看是哪個線程卡死了,發(fā)現(xiàn)都是 TIMED_WAITING,好吧,并不能代表什么,和我第一次排查問題時查看的線程池監(jiān)控的結(jié)果一致。
暫時沒有找到根本原因,但為了線上能繼續(xù)訪問,我只能先把這些問題拋之腦后,重新發(fā)布一版,把連接池清空掉重新來。
第三次排查
這次我老實了,感覺這不是我能處理得了的 bug,趕緊請上我們團隊的技術大佬 yes 哥,我把情況大概給 yes 哥說明了一下:我們線上的連接池總是被很快的耗盡,但是排查不到是哪個地方占用了連接不釋放。yes 哥立馬就反應過來:慢 sql 你排查了嗎?我一驚,好像沒排查。。。
定位問題
這次,我直接查看慢 sql,果然有個慢 sql 執(zhí)行了七千多次,而且平均時間居然達到了 1.4 秒,這連接池根本沒有閑著的時候啊!
分析問題
再仔細看下這個 sql,發(fā)現(xiàn) scene 這個字段沒有加索引,也就是說每次這個請求都會走一遍全表掃描,然后我看了下這個 sql 執(zhí)行的場景,是在微信公眾號掃碼登錄時,前端輪詢用戶是否已經(jīng)掃碼并關注公眾號,如果掃碼關注了公眾號則登錄成功。大致的流程如下:
那么很顯然,我們的問題就出在后端根據(jù)場景碼輪詢用戶信息這里,這里有個慢 sql,因為我們的 scene 沒有設置索引,因此導致每次查詢用戶是否掃碼登錄時都要進行全表掃描,用 explain 看下這個 sql:
解決問題
但是當時我比較猶豫,因為加上這個字段后,大概率只會命中一次,只要有一次查到了,這個 scene 就無效了,所以我總感覺這個索引加了會浪費性能。我跟 yes 哥表達了我自己的想法,但是 yes 哥的想法是,就算他只有一次生效,至少不會在輪詢的時候一直掃全表,畢竟這個 sql 是前端輪詢的產(chǎn)物。
我想了想有道理,這時候,我們老板出來了(也就是魚皮):怎么回事,你們在討論什么?!線上服務不可用都已經(jīng)四十多分鐘了,還不先把服務恢復了再討論詳細解決方案?
于是我趕緊按照 yes 哥的方案,把這個索引加上了,讓我沒想到的是,效果立竿見影,線上一下就可用了,看下連接池的變化:
此時我長舒了一口氣,終于算是搞定了,這時候我再 explain 一下,發(fā)現(xiàn)這時候已經(jīng)是走索引了:
好吧,我為我的無知向 hikaricp 道歉,不是你的鍋,是我的鍋~
后來我又查了下阿里巴巴的 Java 開發(fā)手冊,發(fā)現(xiàn)這本手冊第五章中的第(二)節(jié)里明確寫了創(chuàng)建索引時要避免的誤解,如下圖:
我就是因為第二點認為索引不應該隨便創(chuàng)建,因為可能會導致拖慢記錄的更新之類的,后來想了下,其實 user 表的更新并不頻繁,包括這個 scene 的更新也不頻繁,因為用戶在正常使用過程中又有幾次會重新觸發(fā)登錄呢?
好了,汗流浹背的周一就這么度過了,算是長了個記性,趕緊把所有的慢 sql 都看一下,能加索引加加索引,不能加索引的,看看能不能換個實現(xiàn)方式。以后排查問題又多了個思路,線上的連接池用完了要先排查下有沒有慢 sql 導致鏈接一直沒被釋放,再往下排查。
魚皮總結(jié)
通過這次事故,暴露了我們開發(fā)同學經(jīng)驗的不足。
-
遇到服務卡死問題時,趕緊先擴個容新增一臺可用實例,然后再對著原有故障實例的現(xiàn)場進行排查。 -
時間緊迫的情況下加大數(shù)據(jù)庫連接數(shù)沒問題,但是才加了 10,顯然是有點太保守了,我們的數(shù)據(jù)庫還是扛得住的。 -
應該能夠預料到問題并沒有根本解決,并且趕緊繼續(xù)觀察和排查,怎么就開始做別的了呢? -
排查定位問題的效率不高,像 “如何定位線程池爆滿問題” 這種八股文知識還是要背背的。
而且我在 上一篇文章 中也提到了,導致本次故障的慢 SQL 我們早就發(fā)現(xiàn)并且發(fā)到群里了,結(jié)果團隊幾個開發(fā)竟然沒一個人去處理。。。
當然,最遺憾的是,這篇寫出來的事故復盤仍然是殘缺的!
現(xiàn)在的這個解決方案并不完整,在我看來還是 臨時 解決了這個問題。信不信,并發(fā)量再大點,系統(tǒng)的某個地方還會出現(xiàn)類似的問題?難道到時候再汗流浹背一次?俗話說事不過三,這次的損失我們忽略不計,下一次可就不一定了。
既然我們用了可以根據(jù)資源利用率自動擴容的容器平臺,那么當并發(fā)量增大、單個節(jié)點處理不過來時,就應該自動擴容,自然能夠應對更大的并發(fā)請求。通過本次事故,我們發(fā)現(xiàn)請求連接數(shù)滿的時候,節(jié)點的 CPU 利用率才不到 20%、內(nèi)存才不到 60%,根本達不到擴容的閾值。所以應該適度增大數(shù)據(jù)庫連接池數(shù)量、增大服務器請求處理線程的數(shù)量,提高系統(tǒng)資源利用率,并且通過壓力測試來驗證能否觸發(fā)自動擴容。或者調(diào)整容器的擴容策略,也是一種方案。
最后,希望普天下的程序員寫代碼都不遇 bug。
點擊關注公眾號,閱讀更多精彩內(nèi)容

