發現了一個關於 gin 1.3.0 框架的 bug

gin 1.3.0 框架 http 響應數據錯亂問題排查

問題概述

客戶端同時發起多個http請求,gin接受到請求後,其中一個接口響應內容爲空,另一個接口響應內容包含接口1,接口2的響應內容,致使響應數據錯亂(偶現問題)golang

  • 圖1紅框標註部分爲正常請求響應
  • 圖1藍框標註部分爲異常請求響應(能夠看到編號2531的響應數據只有一個狀態碼信息,並無具體的返回內容)
  • 圖2 能夠看到編號2533的響應數據包含兩組object對象信息,其中第一條object信息應該是2531的響應數據
  • 圖1:avatar
  • 圖2:avatar

問題分析

由於此問題是偶現問題,有時響應數據又是正常的,並且本次新版本這兩個接口代碼也並無修改,因此排查問題花了很長時間,下面是我一步步排查問題的過程.web

  1. 首先懷疑是代碼邏輯問題,經過review接口代碼邏輯後,這兩個接口邏輯都很是簡單,且沒有任何邏輯關聯,因此基本上排除了接口邏輯問題。
  2. 懷疑是不是併發請求致使的問題,經過golang而且開啓多個協程模擬併發發起http請求同時調用這兩個接口100次,並無復現出這種問題,因此能夠排除併發請求致使的問題。
  3. 由於使用golang同時調用這兩個接口沒有復現此問題,懷疑是不是客戶端調用的問題,是否共用了一個http鏈接發送請求,致使最終響應結果合併到了一塊兒? review客戶端代碼後,發現代碼邏輯也沒有什麼問題,而且經過抓包後卻發現的確是後臺響應的數據就有問題,因此能夠確認就是後端的問題。
  4. 此時有同事建議打印一下兩個接口後臺請求和響應的對象內存地址看一下,是不是共用了同一個對象致使,果真打印以後發現,當數據錯亂時,兩個接口使用的是同一個對象,兩個接口沒有任何邏輯關係,爲何會使用同一個請求對象? 爲何就兩個接口會出現數據錯亂的問題? 難道是gin框架的問題? 此時咱們嘗試着調試代碼去驗證

實驗驗證

  1. 經過調試發現,調試信息如圖3所示(第1部分爲正常狀況,能夠觀察到對象指針地址不同,第2部分爲異常狀況,能夠觀察到對象指針地址同樣):
  • 圖3:avatar
  1. 此時我觀察到每次這兩個接口請求後面,都跟着另一個接口請求,如圖1所示的第2494條請求 /api/client/area/scenes 接口,而且本次新版本功能改動了這塊的邏輯,會不會是受這個接口的影響了,因而我嘗試恢復了這塊的代碼,恢復後測試屢次發現問題沒法重現,因此能夠判定是受了這塊代碼的影響.後端

  2. 然而本次修改的代碼邏輯主要是爲了兼容老版本的客戶端,爲此接口添加了一箇中間件,引入了gin框架的HandleContext(context) 方法,用來作一個統一的中間件,作路由的轉發,具體代碼邏輯如圖4所示.api

  • 圖4:avatar
  1. gin框架爲golang web開發中,很經常使用的框架,使用人員很是多,這麼明顯的問題不可能沒人發現,雖然極力的認爲不多是框架的問題,可是事實代表就是這裏的問題,因而經過查詢資料發現,此方法的確可能出現問題,如圖5所示
  • 圖5:avatar
  1. 能夠確認gin框架有問題了,但是緣由是什麼了?網上並無詳細的說明,因而我打算經過調試閱讀源碼的方式來測試,在閱讀源碼的時候我發現,本地代碼和gin最新的官方源碼已經不一致,因而我發現本地代碼版本爲1.3.0,而官方代碼已經更新到1.6.3了, 如圖6所示: 1.6.3已經刪除了 engine.pool.Put(c) 此行代碼
  • 圖6:avatar
  1. 因而我嘗試者把gin版本從1.3.0升級到1.6.3,看看問題是否已經解決,果真gin版本升級後,連續測試屢次未能重現問題,因此能夠肯定就是這裏的問題,而且問題已經解決
    雖然問題已經解決了,可是爲何刪除了這一行就能夠了? 好像並無搞清楚具體的原理是什麼? 因而我嘗試着繼續分析原理
  • engine.pool.Put(c) 函數使用的是 golang的 sync.Pool 類,sync.Pool設計的目的是用來保存和複用臨時對象,以減小內存分配,下降CG壓力,Pool對外暴露的主要有三個接口:get(),put(),new()
  • Get 返回 Pool 中的任意一個對象。若是 Pool 爲空,則調用 New 返回一個新建立的對象。
  • sync.Pool 是一個臨時對象池。一句話來歸納,sync.Pool 管理了一組臨時對象,當須要時從池中獲取,使用完畢後從再放回池中,以供他人使用。
  • Put的過程就是將臨時對象放進 Pool 裏面。
  1. 經過以下圖7也能夠看到 HandleContext 方法上面有一個 ServeHTTP 方法,能夠明顯看到此方法也調用了 engine.pool.Put(c) 方法,而且也調用了 engine.pool.Get().(Context) 方法,經過調試發現 ServeHTTP 爲http請求通用的方法,全部請求都會先調用 ServeHTTP ,若是調用了 HandleContext 則會再調用 HandleContext ,具體執行順序以下圖7所示,如圖能夠看出來 engine.pool.Put(c) 會執行兩次,這樣就會致使在sync.Pool存在兩個一樣的對象,在後面的請求中經過 engine.pool.Get().(Context) 獲取context對象時就會獲取到相同的context對象,致使ResponseWriter指針同樣,從而致使響應數據輸出到同一個接口中.
  • 圖7:avatar

小結

這次問題主要是使用了低版本的gin框架所致,因此能夠看出即便再成熟的框架,也可能會存在bug,在實際開發過程當中應該及時升級到框架的最新穩定版本, 這樣能夠防止一些潛在的bug,當發現一些未知的問題,不要憑空猜想,要儘量的調試代碼,找到問題的根本緣由.併發

參考資料:

相關文章
相關標籤/搜索