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


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

問題概述

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

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

問題分析

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

  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所示.

  • 圖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,當發現一些未知的問題,不要憑空猜測,要盡可能的調試代碼,找到問題的根本原因.

參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM