一個隱藏在支付系統很長時間的雷

猿界汪汪隊發表於2019-05-24

這個案例是最近剛發生不久的,只是這個雷的歷史實在是久遠。

公司在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、池技術

 

更多案例請關注微信公眾號猿界汪汪隊

 

相關文章