故障描述
作為一個老牌OTA公司,公司早些年訂單主要來源是PC網站和呼叫中心。我在入職公司大約半年後,遇到一次非常詭異的故障。有一天早上,大概也是這個季節,陽光明媚,程式猿剛起床,洗洗涮涮,準備去迎接初戀般的工作日,卻突然收到一大堆報警,線上訊息佇列大量積壓;當然,我還是一如既往的非常勤奮地在9點之前就到公司的;但是作為一名新員工,環視四周,組內其他員工都還沒到公司,運維也都在路上,故障就這樣突然降臨了。我趕緊開機登入堡壘機,連線線上機器,tail 錯誤日誌。但是線上10幾個系統,我看了好幾個系統,都沒有發現有什麼錯誤,這就尷尬了。但是統計訊息佇列,超過好幾千的訊息待消費。我當時就在想,這些訊息都是什麼鬼。截圖如下:
圖一
看到這裡,你一定會問數量為604和881個的訊息是做什麼?知道這些訊息的邏輯不就解決問題了麼?話說當時我也是這麼想的,可是當時我作為一名新人,才開始接觸業務不到3個月,還完全沒有這麼深的業務積累(這個時候知道業務是多麼重要)。
既然系統看不到任何錯誤,我也沒有什麼辦法了,當時因為剛入職沒多久,還有點寄希望於領導來解決。轉眼間半個小時已經過去,故障仍然沒有恢復,從業務反饋來看,微信支付寶等支付方式不受影響。受影響的只是信用卡支付(其實當時信用卡量佔比挺高)和分銷支付(後來瞭解到,其實這兩種模式都是信用卡支付模式)。領導還在堵車,運維也只是到了幾個小兵,我找運維把幾個機器的stack列印了一下,也沒有發現什麼問題;運維也陸續到崗,運維準備出大招,重啟系統。但是就在此時,突然系統自動恢復了。所有積壓的訊息自動被消費,信用卡支付也可以了。好,系統竟然有自我修復功能,佩服;
故障原因分析
後來,經過一番努力,還是找到一點蛛絲馬跡,我發現系統的一個消費訊息的定時任務,在故障期間一直在報錯,因為是高可用的job機制,4臺機器,只有搶佔到鎖的伺服器才能獲取到訪問資料庫訊息權利,所以報錯資訊比較分散,4臺機器都有。
圖二
可以判定,這個sql一直異常導致job根本無法獲取到訊息,而另外的生產者又不斷的往佇列放訊息,進而導致訊息積壓。兩個系統關係如下:
圖三
雖然故障總結了,但是我們心裡也不踏實,如何找到系統故障的根本原因,以防止以後再次出現這種故障呢?
方法有兩種:
1、去查程式碼,所有跟這個表相關的sql,都需要仔細review一下,但是你也不一定能查到原因,因為這個場景肯定是不好復現的,要不然早就發現這個問題了。
2、藉助外力,從DB層面查導致這個sql無法執行成功的原因;
方法1看似簡單,其實非常不可行。首先,雖然跟這個表相關的sql,只有幾十個,但是都是正常的sql,沒有使用for update鎖死表的sql。也沒有存在未關閉的事務,因為事務是通過AOP配置的;
所以只能寄希望於方法2了,讓DBA去查;
好歹我們的DBA足夠給力,只用了1天多的時間就查出來了。
DBA回覆如下:
1、有事務沒有及時提交,且連線也沒有關閉,導致該事務一直處於開啟狀態並持有鎖,後續update操作是全表掃描,因此會有鎖等待。
2、最後該連線後續一直沒有操作,達到空閒超時3600秒(我們的故障時間正好也是1小時)後被mysql server斷開,鎖才被釋放。(mysql設定:wait_timeout = 3600)
最牛B的是DBA貼出了沒有提交事務的SQL;sql我就不貼出來了,我們根據DBA提供的線索,找到了程式碼的問題;
故障根本原因
後來我們檢視程式碼,如上面DBA所說,訊息沒有被消費處理,是因為有一個mysql客戶端,即我們的支付應用程式,在進行快捷支付的時候,向佇列插入一條記錄,然後在事務中向第三方發起了呼叫。使用的是httpclient工具發起的呼叫,但是設定超時時,只設定了連線超時時間(connectionTimeout)為30秒,沒有設定響應超時時間(soTimeout),這樣當出現網路問題時,程式就會一直等第三方響應,然後事務也一直沒有提交。而在job程式中,需要將這個queue的所有記錄給更新,但是又取不到表鎖(見圖三),就不斷的報lock wait timeout的錯誤;其實對使用spring AOP框架的研發,很容易犯這種錯誤。我們從 https://tech.meituan.com/2018/04/19/trade-high-availability-in-action.html 這篇總結裡面的1.5段也能看出,美團支付也在這塊也栽過坑;
圖四
到這裡,其實故障原因已經很清楚了,我們在程式碼層面也確實查到了問題。因為DBA提供的sql中,連insert sql的主機名也列了出來,並且現場沒有被破壞,我們使用jstack應該還能找到正在等待的執行緒才對;於是在時隔故障2天后,我們又讓運維把那臺機器的jvm stack給列印了一下,果然發現等待的執行緒仍然存在。
堆疊如下:
圖五
與之對應的程式碼,我就不貼了;
解決方法
1、臨時解決方法,將響應超時時間設定上,但這無法根除問題,只是降低再次出現問題的概率;
2、長久解決方案,修改框架,使用程式設計式事務,將所有遠端呼叫從事務中剝離出來。
知識點
1、事務,spring AOP
2、httpclient,超時設定
求關注