Fasthttp 為什么比標準庫快 10 倍 ?
概述
fasthttp? 是一個使用 Go 語言開發(fā)的 HTTP 包,主打高性能,針對 HTTP 請求響應流程中的 hot path? 代碼進行了優(yōu)化,達到零內存分配,性能比標準庫的 net/http 快 10 倍。
上面是來自官方 Github 主頁的項目介紹,拋開其介紹內容不談,光從名字本身來看,作者對項目代碼的自信程度可見一斑。
本文不會講解 fasthttp? 的應用方法,而是會重點分析 fasthttp 高性能的背后實現(xiàn)原理。
基準測試
我們可以通過基準測試看看 fasthttp? 是否真的如描述所言,吊打標準庫的 net/http,下面是官方提供的基準測試結果:
net/http
fasthttp
基準結果對比
從基準測試結果來看,fasthttp? 的執(zhí)行速度要比標準庫的 net/http? 快很多,此外,fasthttp? 的內存分配方面優(yōu)化到了 0?, 完勝 net/http。
核心優(yōu)化點
筆者選擇的 valyala/fasthttp[1] 版本為 v1.45.0。
對象復用
workerPool
workerpool? 對象表示 連接處理? 工作池,這樣可以控制連接建立后的處理方式,而不是像標準庫 net/http? 一樣,對每個請求連接都啟動一個 goroutine? 處理, 內部的 ready? 字段存儲空閑的 workerChan? 對象,workerChanPool? 字段表示管理 workerChan 的對象池。
請求/響應 對象
請求對象 Request? 和響應對象 Response 都是通過對象池進行管理的,對應的代碼如下:
Cookie 對象
Cookie 對象也是通過對象池進行管理的,對應的代碼如下:
其他對象復用
通過輸出結果可以看到,fasthttp? 中一共有 38 個對象是通過對象池進行管理的,可以說幾乎復用了所有對象,So Crazy!
[]byte 復用
fasthttp? 中復用的對象在使用完成后歸還到對象池之前,需要調用對應的 Reset? 方法進行重置,如果對象中包含 []byte? 類型的字段, 那么會直接進行復用,而不是初始化新的 []byte?, 例如 URI? 對象的 Reset 方法:
此外,涉及到單個字段的修改,如果字段是 []byte? 類型,還是會直接復用,例如 Cookie 對象的這幾個方法:
上面幾個方法的內部實現(xiàn)中,無一例外,都對 []byte 類型的參數(shù)進行了復用。
[]byte 和 string 轉換
fasthttp? 專門提供了 []byte? 和 string? 這兩種常見的數(shù)據(jù)類型相互轉換的方法 ,避免了 內存分配 + 復制,提升性能。
高性能的 bytebufferpool
fasthttp? 并沒有直接使用標準庫中的 bytes.Buffer? 對象,而是引用了作者的另外一個包 valyala/bytebufferpool[2], 這個包的核心優(yōu)化點是 避免內存拷貝 + 底層 byte 切片復用,感興趣的讀者可以看看官方給出的 基準測試結果[3]。
避免反射
fasthttp? 中的所有 對象深拷貝? 內部實現(xiàn)中都沒有使用 反射?,而是手動實現(xiàn)的,這樣可以完全規(guī)避 反射? 帶來的影響,例如 Cookie 對象的拷貝實現(xiàn):
從上面的代碼中可以看到,拷貝? 的內部實現(xiàn)就是手動挨個復制字段,非常 原始 的解決方案。
另外,請求對象 Request? 和響應對象 Response? 的拷貝實現(xiàn)和 Cookie 有異曲同工之處:
fasthttp 的問題
軟件工程沒有銀彈,高性能的背后必然是以某些條件作為代價的,fasthttp 的主要問題有:
- ? 降低了代碼可讀性 (如果不了解 fasthttp 的設計理念,貿然讀代碼很可能無法理解各種方法實現(xiàn))
- ? 增加了開發(fā)復雜性,代碼開發(fā)量要比使用標準庫高,對象復用導致了 申請/歸還 流程彷佛回到了 C/C++ 語言手動管理內存模式
- ? 增加了開發(fā)者心智負擔,如果已經習慣了標準庫的開發(fā)模式,很容易寫出 Bug
- ? 如果業(yè)務中有 異步? 處理場景,框架核心的 對象復用 機制可能導致各種問題,如對象提前歸還、對象指針 hang 起、還有更嚴重的對象字段被重置后繼續(xù)引用 (這類業(yè)務邏輯問題比較難排查)
多核系統(tǒng)的性能優(yōu)化技巧
- ? 使用 reuseport 監(jiān)聽 (SO_REUSEPORT 允許在多核服務器上線性擴展服務器性能,詳細信息請參閱 這個鏈接[4] )
- ? 使用 GOMAXPROCS=1 為每個 CPU 核運行一個單獨的服務器實例 (進程和 CPU 綁定)
- ? 確保多隊列網(wǎng)卡的中斷均勻分布在 CPU 內核之間,詳細信息請參閱 [這個鏈接](https://blog.cloudflare.com/how-to-achieve-low-latency/
fasthttp 最佳實踐
- ? 盡可能復用對象和 []byte buffers, 而不是重新分配
- ? 使用 []byte 特性技巧
- ? 使用 sync.Pool 對象池
- ? 在生產環(huán)境對程序進行性能分析,go tool pprof --alloc_objects app mem.pprof 通常比 go tool pprof app cpu.pprof 更容易體現(xiàn)性能瓶頸
- ? 為 hot path 上的代碼編寫測試和基準測試
- ? 避免 []byte 和 string 直接進行類型轉換,因為這可能會導致 內存分配 + 復制,可以參考 fasthttp 包內的 s2b 方法和 b2s 方法
- ? 定期對代碼進行 競態(tài)檢測[5], 一般會直接集成到 CI 中
- ? 使用 quicktemplate 而非 html/template 模板
是否采用 fasthttp
fasthttp? 是為一些高性能邊緣場景設計的,如果你的業(yè)務需要支撐較高的 QPS? 并且保持一致的低延遲時間,那么采用 fasthttp? 是非常合理的, 反之 fasthttp? 可能并不適合 (增加開發(fā)復雜度和開發(fā)者心智負擔)。大多數(shù)情況下,標準庫 net/http? 是更好的選擇,因為它簡單易用并且兼容性很高。 如果你的業(yè)務流量很少,那么兩者之間的 所謂性能差異 幾乎可以忽略。
Reference
- ? Go 高性能代碼的 30 個 Tips
- ? valyala/fasthttp[6]
- ? fasthttp中運用哪些go優(yōu)化技巧?
- ? fasthttp 快在哪里[7]
- ? fasthttp剖析[8]
引用鏈接
[1]? valyala/fasthttp: ??https://github.com/valyala/fasthttp??
[2]? valyala/bytebufferpool: ??https://github.com/valyala/bytebufferpool??
[3]? 基準測試結果: ??https://omgnull.github.io/go-benchmark/buffer/??
[4]? 這個鏈接: ??https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/??
[5]? 競態(tài)檢測: ??https://go.dev/doc/articles/race_detector??
[6]? valyala/fasthttp: ??https://github.com/valyala/fasthttp??
[7]? fasthttp 快在哪里: ??https://xargin.com/why-fasthttp-is-fast-and-the-cost-of-it/??
[8]? fasthttp剖析: https://www.jianshu.com/p/a0e766f8dcb0