fasthttp 快在哪里
坊間傳言 fasthttp 在某些場景下比 nginx 還要快,說明 fasthttp 中應該是做足了優化。我們來做一些相關的驗證工作。
先是簡單的 hello server 壓測。下面的結果是在 mac 上得到的,linux 下可能會有差異。
fasthttp:
wrk -c36 -t12 -d 5s http://127.0.0.1:8080 Running 5s test @ http://127.0.0.1:808012 threads and 36 connectionsThread Stats Avg Stdev Max +/- StdevLatency 267.58us 42.44us 0.90ms 79.18%Req/Sec 11.05k 391.97 11.79k 87.42%672745 requests in 5.10s, 89.18MB read Requests/sec: 131930.78 Transfer/sec: 17.49MB標準庫:
wrk -c36 -t12 -d 5s http://127.0.0.1:8080 Running 5s test @ http://127.0.0.1:1000212 threads and 36 connectionsThread Stats Avg Stdev Max +/- StdevLatency 310.94us 163.45us 14.41ms 93.42%Req/Sec 9.74k 1.01k 12.80k 75.82%593327 requests in 5.10s, 63.37MB read Requests/sec: 116348.87 Transfer/sec: 12.43MBrust 的 actix,編譯選項帶 --release:
wrk -c36 -t12 -d 5s http://127.0.0.1:9999 Running 5s test @ http://127.0.0.1:999912 threads and 36 connectionsThread Stats Avg Stdev Max +/- StdevLatency 267.31us 20.52us 622.00us 86.07%Req/Sec 11.11k 364.03 11.64k 82.68%676329 requests in 5.10s, 68.37MB read Requests/sec: 132629.68 Transfer/sec: 13.41MB好家伙,雖然是 hello 服務,但是 fasthttp 在性能上竟然趕上 rust 編寫的服務了,確實有點夸張。這也間接證明了,“某些場景”至少可能真的和不帶 GC 的語言性能差不多。
說明 fasthttp 里所做的優化是值得我們做點研究的。不過 fasthttp 搞的這些優化點也并不是很神奇,首先是很常見的 goroutine workerpool,對創建的 goroutine 進行了重用,其本身的 workerpool 結構:
type workerPool struct {// Function for serving server connections.// It must leave c unclosed.WorkerFunc ServeHandler.....ready []*workerChan }核心就是 ready 數組,該數組的元素是已經創建出來的 goroutine 的 job channel。
type workerChan struct {lastUseTime time.Timech chan net.Conn }這么多年過去了,基本的 workerpool 的模型還是沒什么變化。最早大概是在 handling 1 million requests with go 提到,fasthttp 中的也只是稍有區別。
具體的請求處理流程也比較簡單:
tcp accept -> workerpool.Serve -> 從 workerpool 的 ready 數組中獲取一個 channel -> 將當前已 accept 的連接發送到 channel 中 -> 對端消費者調用 workerFunc
這里這個 workerFunc 其實就是 serveConn,之所以不寫死成 serveConn 主要還是為了在測試的時候能替換掉做 mock,不新鮮。
主要的 serve 流程:
func (s *Server) serveConn(c net.Conn) (err error) {for {ctx := s.acquireCtx(c)br = acquireReader(ctx) // or br, err = acquireByteReader(&ctx)// read request header && bodybw = acquireWriter(ctx)s.Handler(ctx) // 這里就是 listenAndServe 傳入的那個 handlerif br != nil {releaseReader(s, br)}if bw != nil {releaseWriter(s, bw)}if ctx != nil {s.releaseCtx(ctx)}} }在整個 serve 流程中,幾乎所有對象全部都進行了重用,ctx(其中有 Request 和 Response 結構),reader,writer,body read buffer。可見作者對于內存重用達到了偏執的程度。
同時,對于 header 的處理,rawHeaders 是個大 byte 數組。解析后的 header 的 value 如果是字符串類型,其實都是指向這個大 byte 數組的,不會重復生成很多小對象:
如果是我們自己寫這種 kv 結構的 header,大概率就直接 map[string][]string 上了。
通過閱讀 serveConn 的流程我們也可以發現比較明顯的問題,在執行完用戶的 Handler 之后,fasthttp 會將所有相關的對象全部釋放并重新推進對象池中,在某些場景下,這樣做顯然是不合適的,舉個例子:
當用戶流程中異步啟動了 goroutine,并且在 goroutine 中使用 ctx.Request 之類對象時就會遇到并發問題,因為在 fasthttp 的主流程中認為 ctx 的生命周期已經結束,將該 ctx 放回了 sync.Pool,然而用戶依然在使用。想要避免這種問題,用戶需要將各種 fasthttp 返回的對象人肉拷貝一遍。
從這點上來看,基于 sync.Pool 的性能優化往往也是有代價的,無論在什么場景下使用 sync.Pool,都需要對應用程序中的對象生命周期進行一定的假設,這種假設并不見得適用于 100% 的場景,否則這些手段早就進標準庫,而非開源庫了。
對于庫的用戶來說,這樣的優化手段輕則帶來更高的心智負擔,重則是線上 bug。在使用開源庫之前,還是要多多注意。非性能敏感的業務場景,還是用標準庫比較踏實。
參考資料
[1] https://github.com/valyala/fasthttp
[2] https://medium.com/smsjunk/handling-1-million-requests-per-minute-with-golang-f70ac505fca
總結
以上是生活随笔為你收集整理的fasthttp 快在哪里的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 架构的腐化是必然的
- 下一篇: 深度阅读之《Concurrency in