網站首頁 編程語言 正文
事件背景
到了年末,沒有太多事情,總算有時間深度優化自己的 golang http webservice 框架,基于 gin 的。公司目前不少項目都是基于這個 webservice 框架開發的,所以我有責任保持這個框架的性能和穩定性。
在自己仔細讀處理 middleware 中一個通用函數 GenerateRequestBody 時發現,之間寫的代碼太過粗暴,雖然一直能能穩定運行,但是總感覺哪里不對,同時也沒有利用 sync.pool,明顯這里可以優化,對在高并發的時候有很大幫助。
越看以前自己實現的 GenerateRequestBody 內容,越覺得太過簡單,幾乎沒有什么思考,尤其在 gin middleware 中,這個函數在每一個 http 會話都會命中,同時設置這個函數作為 webservice 框架公開函數,也會被其他小伙伴調用,所以真的需要認真考慮。
前置知識
GenerateRequestBody 函數分析
廢話不多說,先上代碼,我們一起看看代碼的問題。
func GenerateRequestBody(c *gin.Context) string { body, err := c.GetRawData() // 讀取 request body 的內容 if err != nil { body = utils.StringToBytes("failed to get request body") } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創建 io.ReadCloser 對象傳給 request body return utils.BytesToString(body) // 返回 request body 的值 }
咋一看好像沒有什么,我們不妨更深入代碼一探究竟。
github.com/gin-gonic/gin@v1.8.1/context.go
// GetRawData returns stream data. func (c *Context) GetRawData() ([]byte, error) { return ioutil.ReadAll(c.Request.Body) }
ReadAll 會把 request body 中的所有的字節全部讀出,然后返回一個 []byte 數組。
src/io/ioutil/ioutil.go
// NopCloser returns a ReadCloser with a no-op Close method wrapping // the provided Reader r. // // As of Go 1.16, this function simply calls io.NopCloser. func NopCloser(r io.Reader) io.ReadCloser { return io.NopCloser(r) }
src/io/io.go
// NopCloser returns a ReadCloser with a no-op Close method wrapping // the provided Reader r. func NopCloser(r Reader) ReadCloser { return nopCloser{r} } type nopCloser struct { Reader } func (nopCloser) Close() error { return nil }
ioutil.NopCloser 實際就是一個包裝接口,把 Reader 接口封裝成一個帶有 Close 方法的對象,而且 Close 方法是一個直接返回的空函數。所以這里就有一個問題,如果你想調用 Close 關閉這個 io.ReadCloser 對象。我只能在邊上,呵呵呵,你懂我的意思。
回歸正題,這些代碼大家應該看起來很眼熟才對。沒錯,這是網絡上 gin 框架多次讀取 http request body 中內容的解決方案。 能想像很多小伙伴就是 copy + paste 了事,流量小或者沒有什么大規模應用場景下沒有什么問題。如果流量大了?應用規模很多?那怎辦?
gin 如何正確多次讀取 http request body 的內容呢? 正確的姿勢是什么呢?
追本溯源
gin 只不過是一個 router 框架,真正的 http 請求處理是 golang 中的 net/http 包來負責的。要找到 gin 如何正確多次讀取 http request body 內容的方法,就一定要往下追。
寫過 golang http client 的小伙伴都知道,需要手動執行 resp.Body.Close() 這樣的方法釋放連接。要不然會因為底層 tcp 端口耗盡,導致無法創建連接。我們通過一個簡單例子看下:
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { resp, _ := doGet("http://www.baidu.com") defer resp.Body.Close() //go的特殊語法,main函數執行結束前會執行 resp.Body.Close() fmt.Println(resp.StatusCode) //有http的響應碼輸出 if resp.StatusCode == http.StatusOK { //如果響應碼為200 body, err := ioutil.ReadAll(resp.Body) //把響應的body讀出 if err != nil { //如果有異常 fmt.Println(err) //把異常打印 log.Fatal(err) //日志 } fmt.Println(string(body)) //把響應的文本輸出到console } } /** 以GET的方式請求 **/ func doGet(url string) (r *http.Response, e error) { resp, err := http.Get(url) if err != nil { fmt.Println(resp.StatusCode) fmt.Println(err) log.Fatal(err) } return resp, err }
通過上面的代碼,我們能看到 defer resp.Body.Close() 的代碼,它就是要主動關閉連接。那么也有一個類似的問題,golang 中 net/http 包的 server 代碼是不是也要主動管理連接呢?
類似如下:
bodyBytes, _ := ioutil.ReadAll(req.Body) req.Body.Close() // 這里調用Close req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
但是官方的代碼注釋里卻寫不需要在處理函數里調用 Close:Request.Body:"The Server will close the request body. The ServeHTTP Handler does not need to."
感覺好奇怪,golang 中 net/http 包的 server 自己能關閉 request,那跟上面類似執行 req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 替換了 req.Body 原有內容,那么 golang 中 net/http 包的 server 還能正確關閉以前的 req.Body 嘛?如果不能關閉,那么類似 GenerateRequestBody 函數這樣的執行過程,必然在大并發下,必然導致內存泄露和大量 GC 回收,影響服務響應。
值得深入
帶著上面的問題,在網上尋找了很久,沒有能找到解決問題的方法,也沒有人把為什么說清楚。沒有思路,在各種不確定的假設上,提供一個公司級的底層 webservice 框架,必然被公司技術委員會的主席們挑戰。
說到這里,一不做二不休,直接干就是,往下肝。 順著服務的啟動流程找到了 golang 中 net/http 包的 server.go 文件,然后一個一個方法慢慢趴,直到找到了 func (c *conn) serve(ctx context.Context) {} 這個函數,總算看到了具體內容。
src/net/http/server.go
// Serve a new connection. func (c *conn) serve(ctx context.Context) { ... for { w, err := c.readRequest(ctx) // 讀取 request 內容 ... } ... // HTTP cannot have multiple simultaneous active requests.[*] // Until the server replies to this request, it can't read another, // so we might as well run the handler in this goroutine. // [*] Not strictly true: HTTP pipelining. We could let them all process // in parallel even if their responses need to be serialized. // But we're not going to implement HTTP pipelining because it // was never deployed in the wild and the answer is HTTP/2. inFlightResponse = w serverHandler{c.server}.ServeHTTP(w, w.req) // 處理請求 inFlightResponse = nil w.cancelCtx() if c.hijacked() { return } w.finishRequest() // 關閉請求 ... }
看到這里,想要解決問題只要看兩個函數 finishRequest 和 readRequest 就可以了。
finishRequest 函數分析
func (w *response) finishRequest() { ... // Close the body (regardless of w.closeAfterReply) so we can // re-use its bufio.Reader later safely. w.reqBody.Close() // 關閉 request body ???,在這里? ... }
是這里? 就在這里關閉了? 但是這里是 response 啊,不是 request。 繼續點開看看 response 結構體是什么?
// A response represents the server side of an HTTP response. type response struct { ... req *Request // request for this response reqBody io.ReadCloser ... }
這里有一個 req 是 Request 的指針,那么還有一個 reqBody 作為 io.ReadCloser 是為了干嘛? 不解!不解!不解!
readRequest 函數分析
// Read next request from connection. func (c *conn) readRequest(ctx context.Context) (w *response, err error) { ... req, err := readRequest(c.bufr) if err != nil { if c.r.hitReadLimit() { return nil, errTooLarge } return nil, err } ... w = &response{ ... req: req, reqBody: req.Body, ... } ... }
看到這里,突然這個世界晴朗了,所有的事情好像都明白了。心細的小伙伴一定看出來眉目了,很有可能真是:一拍大腿的提高。
readRequest 讀取到 req 信息后,在創建 response 的對象時,同時將 req 賦值給了 response 中的 req 和 reqBody。 也就是說 req.Body 和 reqBody 指向了同一個對象。 換句話說,我改變了 req.Body 的指向,reqBody 還保留著最初的 io.ReadCloser 對象的引用。 不管我怎么改變 req.Body 的值,哪怕是指向了 nil,也不會影響 server 調用 finishRequest() 函數來關閉 io.ReadCloser 對象,因為 finishRequest 內部調用的是 reqBody。
得出結論
middleware 中的 req.Body 和 response 中的 reqBody 是兩個變量。初期,req.Body 和 reqBody 中存放了同一個地址。但是,當 req.body = io.NoCloser 時,只是改變了 req.Body 中的指針,而 reqBody 仍舊指向原始請求的 body,故不需要在 middleware 中執行關閉。
在 golang 開發提交記錄中也找到了類似的說明,并解決了這個問題。所以說在 Go 1.6 之后已經不用擔心這個問題了。
提交信息:
net/http: don't panic after request if Handler sets Request.Body to nil。
大致的意思是,不用再擔心把 req.Body 設置 nil,其實也就是不用再擔心重置 req.Body 了,更加不用手動關閉 req.Body。
上手開發
搞清楚了 golang 中 net/http 包的 server 中對請求的 request body 處理流程,那么 gin 這邊也好開發了。 首先我們回到之前的 GenerateRequestBody 函數。
func GenerateRequestBody(c *gin.Context) string { body, err := c.GetRawData() // 讀取 request body 的內容 if err != nil { body = utils.StringToBytes("failed to get request body") } c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 創建 io.ReadCloser 對象傳給 request body return utils.BytesToString(body) // 返回 request body 的值 }
雖然不需要每次關閉 c.Request.Body 了,但是我們要注意,沒調用一次都會調用 bytes.NewBuffer 和 ioutil.NopCloser 一次。ioutil.NopCloser 這個還好是一個包裝,之前我們看到了相關的代碼。但是 bytes.NewBuffer 是一個重量級的家伙,我第一反應是不是可以用 sync.pool 來緩存這個這部分的代碼?
實際當然是可以的,但是 GenerateRequestBody 是一個函數,c.Request.Body 新的指向在隨后的 gin handler 中也要用,明顯在 GenerateRequestBody 內部對 sync.pool 執行 Get 和 Put 明顯不合適。
怎么解決呢?也很簡單,在 gin 的框架 http request 會話是跟 Context 對象綁定的,所以直接在 Context 操作,并將 sync.pool Get 對象放入 Context,然后在 Context 銷毀之前對 sync.pool 執行 Put 歸還。
流程圖如下:
gin Middleware 代碼
func ginRequestBodyBuffer() gin.HandlerFunc { return func(c *gin.Context) { var b *RequestBodyBuff // 創建緩存對象 b = bodyBufferPool.Get().(*RequestBodyBuff) b.bodyBuf.Reset() c.Set(ConstRequestBodyBufferKey, b) // 下一個請求 c.Next() // 歸還緩存對象 if o, ok := c.Get(ConstRequestBodyBufferKey); ok { b = o.(*RequestBodyBuff) b.bodyBuf.Reset() // bytes.Buffer 要 reset,但是 slice 就不能,這個做 io.CopyBuffer 用的 c.Set(ConstRequestBodyBufferKey, nil) // 釋放指向 RequestBodyBuff 對象 bodyBufferPool.Put(o) // 歸還對象 c.Request.Body = nil // 釋放指向創建的 io.NopCloser 對象 } } }
新 GenerateRequestBody 代碼
func GenerateRequestBody(c *gin.Context) string { var b *RequestBodyBuff if o, ok := c.Get(ConstRequestBodyBufferKey); ok { b = o.(*RequestBodyBuff) } else { b = newRequestBodyBuff() } body, err := boostio.ReadAllWithBuffer(c.Request.Body, b.swapBuf) // 讀取 request body 的內容,此時 body 的 []byte 是全新的一個數據 copy if err != nil { body = utils.StringToBytes("failed to get request body") boost.Logger.Errorw(utils.BytesToString(body), "error", err) } _, err = b.bodyBuf.Write(body) // 把內容重新寫入 bytes.Buffer if err != nil { c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) boost.Logger.Errorw(utils.BytesToString(body), "error", err) } else { c.Request.Body = ioutil.NopCloser(b.bodyBuf) } return utils.BytesToString(body) }
測試代碼
對開發好的代碼執行循環測試,用短鏈接測試。
while true;do curl -i http://127.0.0.1:8080/yy/; done
總結
我們通過上面的操作和使用,基本確認了 golang 中 net/http 包中對 request body 的處理流程。 通過簡單的開發,我們實現了 gin 正確多次讀取 http request body 內容的方法,同時加入了 sync.pool 支持。減少了頻繁 bytes.NewBuffer 創建對資源的消耗,以及提高了資源的利用效率。
原文鏈接:https://juejin.cn/post/7182895543721394236
相關推薦
- 2023-01-26 Python中的lambda和apply用法及說明_python
- 2022-03-25 在?ASP.NET?Core?中為?gRPC?服務添加全局異常處理_ASP.NET
- 2022-09-10 Go語言中的IO操作及Flag包的用法_Golang
- 2023-01-31 golang獲取變量或對象類型的幾種方式總結_Golang
- 2022-06-19 WPF項目在設計界面調用后臺代碼_實用技巧
- 2022-08-15 Python?time模塊之時間戳與結構化時間的使用_python
- 2022-09-15 Python中eval()函數的詳細使用教程_python
- 2022-07-27 關于pytest結合csv模塊實現csv格式的數據驅動問題_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支