故障描述
作為一個老牌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,超時設置
求關注

