生產(chǎn)環(huán)境NoHttpResponseException異常排查解決記錄分析
業(yè)務背景
公司最近正在準備為郵儲銀行開展一個營銷活動,活動規(guī)則是:用戶使用郵儲銀行卡在線上支付一分錢,就可以領取50元現(xiàn)金券,卡券領取完畢后,系統(tǒng)會自動退還消費者的1分錢。(相當于免費給郵儲用戶發(fā)放50元現(xiàn)金券),因為發(fā)券的入口要做在小程序里面,于是這個需求就落到了C端這邊(公司的另一個業(yè)務小組),而我主要負責B端支付模塊。經(jīng)過我們商討后,初步制定的業(yè)務邏輯為:用戶打開C端小程序進行支付、然后C端將支付請求轉給B端支付模塊、B端支付模塊向微信下單、等待消費者完成支付后B端支付模塊通知C端交易完成并返回其支付方式、C端判斷支付方式是否為郵儲銀行卡(是郵儲銀行卡則發(fā)券)、然后C端調(diào)用B端支付模塊進行退款。為了方便大家理解,我嘔心瀝血的畫出了系統(tǒng)調(diào)用的時序圖。
生產(chǎn)環(huán)境發(fā)現(xiàn)的問題
1、NoHttpResponseException導致退款失敗
功能上線后,我便開始監(jiān)控B端支付模塊的交易數(shù)據(jù),前兩天的數(shù)據(jù)并沒有什么異常,支付完成的訂單都已經(jīng)退款完成。然后在第三天快下班時,我又統(tǒng)計了一遍數(shù)據(jù),發(fā)現(xiàn)竟然存在一筆沒退款的訂單,我整個人一下子就支棱了起來(不會又寫了個Bug吧~),我先在數(shù)據(jù)庫中查到訂單號,然后找運維同事拿了一下日志,發(fā)現(xiàn)支付回調(diào)是正常的,并且下游系統(tǒng)也響應了success,但是卻沒有調(diào)用退款接口進行退款。排查到這里基本已經(jīng)可以確定不是支付模塊這邊的問題了,但問題畢竟還是要解決的,于是我聯(lián)系了C端的同事,暫時先通過接口的方式把消費者的錢進行退款。然后開始排查C端系統(tǒng)的問題,通過C端的日志發(fā)現(xiàn),在請求支付模塊進行退款時存在一個異常信息,報錯信息如下
看到這個報錯,我不禁陷入了思考:C端這個日志表明確實是發(fā)起了退款請求,但是B端支付模塊根本沒收到這個退款請求,這樣一來就比較尷尬了,雙方系統(tǒng)竟然都沒問題,那只能是網(wǎng)絡問題了(找不到人背鍋,只能推給網(wǎng)絡了~~哈哈),剛開始只有一筆,我沒怎么在意,過了幾天后,陸陸續(xù)續(xù)發(fā)現(xiàn)了好幾筆類似的情況,平均幾千筆訂單就會出現(xiàn)一筆退款失敗的,并且這些訂單之間毫無規(guī)律,搞得我這幾天是干啥啥不香,于是痛下決心要深入研究一下這個問題。
2、 異常情況分析
目前能夠提供幫助的信息并不多,只有這一個報錯日志,通過在網(wǎng)上收集到的一些相關資料,發(fā)現(xiàn)了幾篇比較有借鑒價值的文章,他們的觀點也都幾乎一致:服務端主動斷開TCP鏈接,然后客戶端使用半斷開的鏈接發(fā)起請求時,服務端響應RST包導致此異常情況的發(fā)生。 大多數(shù)文章的建議是:捕獲NoHttpResponseException異常進行重試。
3、驗證思路
既然有了上述猜想,那么下一步肯定是要做驗證的,驗證一下在這個場景下確實會出現(xiàn)此現(xiàn)象。剛開始的驗證思路比較簡單,就是在服務端通過工具模擬FIN包,然后再用HttpClient繼續(xù)請求,觀察其結果,然而抓包結果顯示Httpclient會創(chuàng)建一個新的tcp鏈接進行請求,木得辦法,解鈴還須系鈴人,恐怕要看一下HttpClient源碼才能解釋這個現(xiàn)象了。
通過閱讀HttpClient源碼,大致找到了兩個比較關鍵的邏輯點
- HttpClient建立tcp鏈接的時機(三次握手的時機)
- 發(fā)送http請求的時機
tip:在三次握手之前會檢查當前tcp鏈接是否處于Open狀態(tài),若處于Open狀態(tài)則復用此鏈接,若不處于Open狀態(tài)則打開一個新的tcp鏈接,這樣一來就解釋的通為什么之前HttpClient又重新創(chuàng)建了一個TCP鏈接的現(xiàn)象了。
4、NoHttpResponseException復現(xiàn)
然后接下來是要做的就是根據(jù)之前的猜想來復現(xiàn)NoHttpResponseException場景,具體的思路如下
- 在Httpclient源碼中,等待tcp鏈接建立完成后,打上斷點
- 等服務器主動發(fā)送FIN包斷開鏈接后,再發(fā)起請求,然后觀察結果
成功復現(xiàn)了NoHttpResponseException現(xiàn)象,抓包結果如下所示
通過抓包結果分析,可以得出"服務端主動斷開TCP鏈接,然后客戶端使用半斷開的鏈接發(fā)起請求"確實會導致NoHttpResponseException現(xiàn)象,至于服務端什么情況下會主動斷開tcp鏈接?間隔多久主動斷開tcp鏈接?這里就不再討論了,讀者可以自行了解一下keep-alive機制。分析到這里,問題基本上算是解決了,生產(chǎn)環(huán)境出現(xiàn)此問題的執(zhí)行時序應該如下所示
- 客戶端HttpClient復用之前已經(jīng)Open的鏈接
- 然后進行檢查(因為此時服務端還未關閉tcp鏈接,所以鏈接可用)
- 緊接著服務端主動關閉鏈接導致鏈接不可用
- 服務端針對客戶端的請求響應了RST包
5、解決方案
從業(yè)務層面考慮,即使修復了這個問題,也還是會有很大的風險,畢竟網(wǎng)絡是未知的,因此我建議C端同事做一個補償機制,用來處理退款失敗情況。
當然網(wǎng)絡層面該優(yōu)化的也得優(yōu)化,具體步驟是在HttpClient初始化時添加重試策略。
private static CloseableHttpClient init() { // 配置請求的超時設置 RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT) .setConnectTimeout(CONNECT_TIMEOUT) .setSocketTimeout(SOCKET_TIMEOUT) .build(); // 重試策略 RETRY_COUNT=3 代表NoHttpResponseException異常重試3次 HttpRequestRetryHandler retryHandler = (exception, executionCount, context) -> { return executionCount <= RETRY_COUNT && exception instanceof NoHttpResponseException; }; return HttpClients.custom() .setConnectionManager(new PoolingHttpClientConnectionManager()) .setRetryHandler(retryHandler) .setDefaultRequestConfig(requestConfig) .build(); }
6、引發(fā)的思考
- HttpClientPool的鏈接管理策略(復用、回收等等)。
- Keep-alive機制
- 計算機網(wǎng)絡
以上就是生產(chǎn)環(huán)境NoHttpResponseException異常排查解決記錄分析的詳細內(nèi)容,更多關于生產(chǎn)環(huán)境NoHttpResponseException的資料請關注腳本之家其它相關文章!
相關文章
Spring整合Quartz定時任務并在集群、分布式系統(tǒng)中的應用
這篇文章主要介紹了Spring整合Quartz定時任務并在集群、分布式系統(tǒng)中的應用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04Java設計模式之橋接模式詳解(Bridge Pattern)
橋接模式是一種結構型設計模式,旨在將抽象部分與其實現(xiàn)部分分離,從而使兩者可以獨立地變化,橋接模式通過組合關系代替繼承關系,將抽象和實現(xiàn)解耦,使代碼更具擴展性和維護性2025-02-02java使用poi在excel單元格添加超鏈接設置字體顏色的方法
這篇文章主要介紹了java使用poi在excel單元格添加超鏈接,設置字體顏色,poi功能還是很強大的,基本能想到的功能都能通過poi實現(xiàn),本文結合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2023-09-09