一個隱藏在支付系統很長時間的雷
這個案例是最近剛發生不久的,只是這個雷的歷史實在是久遠。
公司在3月底因為一次騰訊雲專線故障,整個支付系統在高峰期停止服務將近10分鐘。而且當時為了快速解決問題止損,重啟了支付服務,事後也就沒有了現場。我們支付組在技術架構上原先對專線故障的場景做了降級預案,但故障時預案並沒有生效,所以這次我們需要排查清楚降級沒有生效的原因(沒有現場的事後排查,挑戰非常大)。
微信支付流程
首先回顧一下微信支付的流程(也可以參考https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_4):
這個過程是同步的,如果我們的支付系統因為網路問題,沒有取到prepay_id,那麼使用者就無法支付;
我們的預案
我們的預案非常簡單,就是在請求api.mch.weixin.qq.com時,在HTTPClient中設定了一個超時時間,當支付請求超時時,我們就請求微信支付的另外一個備用域名api2.mch.weixin.qq.com,我們的超時時間設定的是3秒;
故障現象
每次網路抖動的時候,我們從監控中都能發現,我們的超時時間並沒有完全起作用。從故障後的監控看平均執行時間達到了10秒,超時時間(3秒)完全不管用:從日誌中進一步分析到,很多請求都是在10秒以上,甚至10分鐘後才報超時異常。10分鐘後再降級到備份域名顯然已經沒有什麼意義了。這讓我們開發很不解,為什麼HttpClient的超時設定沒有生效,難道是HttpClient的bug?
以前我們也懷疑過自己封裝的HTTPClient元件有問題,但是我們寫了一個併發程式測試過,當時並沒有測試出有序列問題或者不支援併發的問題;
真相-系統層面瓶頸點HttpClient
最近通過我們測試(我們組其中一個開發在測試環境對故障進行了復現)和調研後,我們發現支付系統使用的封裝後的HttpsClient工具,同一時間最多隻允許發起兩個微信支付請求;當這兩個請求沒有迅速返回的時候(也就是網路抖動的時候),後面新的請求,只能排隊等候,進而block住執行緒耗盡tomcat的執行緒;超時未生效的原因是因為CloseableHttpClient預設的實現對網路連線採用了連線池技術,當連線數達到最大連線數時,後續的請求只能排隊等待連線,根本就無法取得發起網路請求的機會,所以也談不上連線超時和響應超時;
系統本來應該這樣:
實際卻是這樣:
參考和論證
我們從HttpClient的官方文件中證實了這一點,同時也寫程式進行了驗證(這其中的配置比較複雜和深入,計劃後續再寫一篇文章進行說明,請持續關注汪汪隊);
官方文件:http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html 2.3.3. Pooling connection manager
我們訪問微信支付域名api.mch.weixin.qq.com,無論我們發起多少個請求, 在httpclient中就是對應一個route(一個host和port對應一個route),而每個route預設最多隻有兩個connection;而這個Route的預設值,我們程式碼中沒有修改。所以,一臺tomcat,實際上同一時間最多隻會有兩個請求傳送到微信。網路抖動的時候,請求都會需要很長時間才能返回,因為我們設定的是3秒響應超時,所以,當網路抖動時,我們單臺機器的qps就是3秒2個,極限情況下一分鐘最多40個請求;更糟糕的情況,我們的程式中微信退款的超時時間設定的是30秒,所以如果是退款請求,那就是1分鐘只能處理4個請求,10臺伺服器一分鐘也就只能處理40個請求;因為支付和退款都是共用的一個HttpClient連線池,所以退款和支付會互相影響;
按照HttpClient的設計,支付系統真實請求過程大概如下:
經驗教訓
1、對於微信支付,缺少壓測。之前壓測都是基於支付寶,而支付寶的呼叫模式和微信完全不一樣,導致無法及時發現這個瓶頸;
2、研發對HttpClient等使用池技術的元件,原理了解不夠深入,沒有修改預設策略,最終形成了瓶頸;
3、對報警細節觀察不是很到位,每次網路抖動我們只看到了網路方面的問題,卻忽略了程式中超時引數未生效的細節,從而多次錯失發現程式缺陷的機會,所以“細節決定成敗”;
知識點
1、HttpClient,Route
2、微信支付
3、池技術
更多案例請關注微信公眾號猿界汪汪隊