公司產品在上線運行中,有使用人員反映發起了一筆帳務請求,記帳100元,前端正常交易完成後,後臺記了兩筆100元的帳務,引起了很危險的短款帳務問題。html
經多方覈實,確認如今確實存在客戶端通信重複發送了兩次,且僅接受第二次的返回信息致使了此問題發生。前端
凡事先從自省入手,咱們先不去吐槽爲啥服務端沒有作好冪等性和防重功能,先分析下到底客戶端是否是真的有問題,所以產生了如下的分析排查過程。linux
客戶端採用CEF框架,基於Chromium 76版本;發起請求基於axios框架,前端框架採用Vue.js 2.0技術。ios
一、總結整個網絡部署架構圖:nginx
圖一、網絡部署架構圖git
二、根據上圖的網絡架構,依次從後到前拿到生產日誌,首先分析兩次請求的服務端日誌是否徹底一致,結論爲兩次服務端日誌比對徹底一致。github
三、觀察業務網關日誌,發現兩筆請求間隔19s,無任何其餘信息。web
圖二、業務網關日誌截圖chrome
四、觀察反向代理服務器日誌,發現兩筆請求之間都存在websocket嘗試從新握手重連的狀況:json
圖三、反向代理服務器日誌截圖
該日誌反映出上下兩筆相同的POST請求,中間出現了網絡斷開的狀況,所以致使了websocket進行了重連。
五、觀察客戶端日誌,的確出現了網絡斷開狀況,websocket嘗試重連,並在後續重連成功。
圖四、客戶端日誌截圖
六、從上圖此時排查客戶端日誌,發現客戶端日誌在發起通信請求日誌後,未接收到任何http異常日誌記錄,僅有websocket異常日誌記錄,從客戶端視角看,只是發了一次請求,過了19s後收到200響應狀態碼。
綜上問題分析: 咱們發現客戶端應用層只發起了一次http請求,此時網絡出現波動,反向代理服務器記錄客戶端發起了兩筆http請求,中間夾雜着一次websocket重連,服務端也是收到兩筆請求,所以下一步須要排查客戶端與反向代理服務器以前到底由於什麼緣由致使了問題出現。
一、到底在客戶端與反向代理服務器之間是誰進行了重發,致使了此次問題呢
二、首選懷疑的是nginx的重發機制,能夠看到nginx在出現異常時,會從新請求後臺服務。 nginx 重發機制 - yxy_linux - 博客園 (cnblogs.com)。但根據一開始說明的架構,對ngnix參數進行修改,測試時確實出現了重發,但網關日誌中記錄了一條,與本次現象不符
三、再次懷疑的是axios重發,在github上也見到相關的帖子,有人反饋過使用axios致使重複提交問題,不過因爲缺乏重現場景,問題關閉
查看axios源碼發現,axios底層發送也是使用XMLHttpRequest實現,從axios過程來看,不存在重發代碼處理
四、隨着上面nginx與axios機制排除後,把目光轉向了Http協議,網上搜索發現http對重發有處理,能夠看到網頁說明跟此次現象很像,都是中途出現了斷網。HTTP請求重發 - SegmentFault 思否
根據上述場景,對問題復現。
五、斷掉客戶端網絡,嘗試重現。本地兩臺PC機,一臺模擬客戶端,一臺模擬服務端,兩臺PC機經過路由器相連,客戶端使用axios直接發post請求,服務端接收請求會睡眠幾秒模擬處理業務流程再返回,使用wireshark抓包觀察客戶端服務端發包狀況,嘗試問題復現。
客戶端點擊按鈕發送post請求,當即拔開網線,等待幾秒後把網線插回,客戶端馬上收到了Disconnect異常,生產環境未出現此類異常,所以此場景不正確。
圖五、瀏覽器測試截圖
六、斷掉服務端網絡,嘗試重現。客戶端點擊按鈕發送post請求,服務端收到請求後當即拔網線,等待幾秒後把網線插回,觀察客戶端Network狀況及抓包數據:
客戶端Network顯示發送了兩次請求,一次爲OPTION請求,頁面顯示爲Preflight請求,一次爲正常POST請求。幾毫秒後Preflight的OPTION請求返回,POST請求一直處於pending狀態。(圖6)
圖六、瀏覽器測試截圖
以後觀察wireshark日誌,發如今拔開服務端網線,等待一段時間,再插回網線的一小段時間後,客戶端會再次發起一次請求,且此時客戶端無感知,Network一直顯示pending狀態,以後服務端處理完第二次請求後返回給客戶端,客戶端接收到服務端正常響應報文,狀態碼200。該場景與生產徹底一致,復現完成,100%能夠復現。(圖七、8)
圖七、Wireshark測試截圖
圖八、瀏覽器測試截圖
一、綜上排查分析,是客戶端瀏覽器發起了HTTP1.1協議的請求,在服務端的網絡出現異常斷開的狀況下,給瀏覽器發送了一個RESET指令,瀏覽器自發地發起了第二筆請求,且對應用層無感知。下一步就須要思考爲何瀏覽器會自發地發起了第二筆請求,且對應用層無感知,致使應用層視角看是發起了一筆請求,接收到了第二筆的響應結果。
二、首先,平臺使用的76版本的Chromium內核瀏覽器默認如今都使用了HTTP1.1協議,其中的鏈接方式爲:Connection:Keep-Alive,此種鏈接方式能夠改善這種狀態,即在一次TCP鏈接中只進行一次握手階段(如圖7wireshark所示),後續能夠持續發送多份數據而不會斷開鏈接。經過使用keep-alive機制,能夠減小tcp鏈接創建次數,也意味着能夠減小TIME_WAIT狀態鏈接,以此提升性能和提升httpd服務器的吞吐率(更少的tcp鏈接意味着更少的系統內核調用,socket的accept()和close()調用)。那麼,是否能夠經過關閉KeepAlive解決該問題?
三、Connection:Keep-Alive屬於瀏覽器不容許修改協議頭,那麼除了Keep-Alive之外,還有哪些狀況會致使重複連接呢?經過觀察wireshark和瀏覽器的Network,發現正常的POST請求前會發送一次OPTIONS請求,該請求的目的是:瀏覽器會首先使用 OPTIONS 方法發起一個預請求,判斷接口是否可以正常通信,若是不能就不會發送真正的請求過來,若是測試通信正常,則開始真正的請求。所以,會不會是OPTIONS請求產生了一些影響?
四、從這一角度出發,咱們先思考和查證了一下,什麼Content-Type類型會致使發送OPTIONS請求。經過寫一個簡單的XmlHttpRequest發送POST請求給服務端,wireshark抓包發現,簡單的XmlHttpRequest在服務端網絡斷開的狀況下,重連後也不會發送第二筆請求,只有axios纔會發送第二筆,那麼須要對兩種狀況的抓包分析報文頭進行比對。
圖九、Wireshark測試截圖
五、經過比對兩種請求的發包狀況(圖9),XmlHttpRequest發送的content-type爲:text/plain,而axios發送的content-type爲:application/json。那麼下一步,咱們手動將axios默認的application/json的content-type改爲text/plain,發現再次斷開服務端網絡重連後,瀏覽器不會再次自動發送第二筆請求了(圖十、圖11)。
圖十、Wireshark測試截圖
圖十一、瀏覽器測試截圖
六、所以,致使這次反向代理服務器以後整個鏈路收到兩筆請求的最根本緣由,就是content-type:application/json內容類型,瀏覽器斷定會在服務端網絡恢復後自動重發請求,且對應用層無感知。
那麼content-type到底有哪些種類?是否有一些定義和歸類?經過查找發現:HTTP請求分爲簡單請求與複雜請求,簡單請求不會發送OPTIONS預請求,簡單請求須要知足如下條件:
因此,簡單請求的content-type通常爲:text/plain,multipart/form-data,application/x-www-form-urlencoded。
通過咱們屢次使用各類content-type類型與不一樣種類瀏覽器(chrome與firefox)驗證,總結了幾種狀況以下:
Content-type | chrome是否會默認重發第二次 | firefox是否會默認重發第二次 |
---|---|---|
application/json | 會 | 會 |
text/plain | 否 | 會 |
application/x-www-form-urlencoded。 | 否 | 否 |
multipart/form-data | 未測 | 未測 |
最終得出結論,客戶端發送http請求,最合適應該爲application/x-www-form-urlencoded。
將客戶端axios的content-type,手動改成application/x-www-form-urlencoded,因爲該content-type爲form-data,所以須要使用qs對body進行序列化改造。
底層是什麼緣由,致使瀏覽器重發,具體哪些會重發,哪些不會?
www.w3.org/Protocols/r… developer.mozilla.org/zh-CN/docs/… dev.to/p0oker/why-… dev.to/effingkay/c… developer.mozilla.org/en-US/docs/… segmentfault.com/a/119000000… blog.csdn.net/edward30/ar…