fasthttp 爲什麼比標準庫快 10 倍 ?
概述
fasthttp
是一個使用 Go 語言開發的 HTTP 包,主打高性能,針對 HTTP 請求響應流程中的hot path
代碼進行了優化,達到零內存分配,性能比標準庫的net/http
快 10 倍。
上面是來自官方 Github
主頁的項目介紹,拋開其介紹內容不談,光從名字本身來看,作者對項目代碼的自信程度可見一斑。
本文不會講解 fasthttp
的應用方法,而是會重點分析 fasthttp
高性能的背後實現原理。
基準測試
我們可以通過基準測試看看 fasthttp
是否真的如描述所言,吊打標準庫的 net/http
,下面是官方提供的基準測試結果:
net/http
$ GOMAXPROCS=4 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkNetHTTPClientDoFastServer-4 2000000 8774 ns/op 2619 B/op 35 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1TCP-4 500000 22951 ns/op 5047 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10TCP-4 1000000 19182 ns/op 5037 B/op 55 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100TCP-4 1000000 16535 ns/op 5031 B/op 55 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1Inmemory-4 1000000 14495 ns/op 5038 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10Inmemory-4 1000000 10237 ns/op 5034 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100Inmemory-4 1000000 10125 ns/op 5045 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1000Inmemory-4 1000000 11132 ns/op 5136 B/op 56 allocs/op
fasthttp
$ GOMAXPROCS=4 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkClientDoFastServer-4 50000000 397 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1TCP-4 2000000 7388 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10TCP-4 2000000 6689 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100TCP-4 3000000 4927 ns/op 1 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1Inmemory-4 10000000 1604 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10Inmemory-4 10000000 1458 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100Inmemory-4 10000000 1329 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1000Inmemory-4 10000000 1316 ns/op 5 B/op 0 allocs/op
基準結果對比
從基準測試結果來看,fasthttp
的執行速度要比標準庫的 net/http
快很多,此外,fasthttp
的內存分配方面優化到了 0
, 完勝 net/http
。
核心優化點
筆者選擇的 valyala/fasthttp[1] 版本爲 v1.45.0
。
對象複用
workerPool
workerpool
對象表示 連接處理
工作池,這樣可以控制連接建立後的處理方式,而不是像標準庫 net/http
一樣,對每個請求連接都啓動一個 goroutine
處理, 內部的 ready
字段存儲空閒的 workerChan
對象,workerChanPool
字段表示管理 workerChan
的對象池。
// workerpool.go
type workerPool struct {
ready []*workerChan
workerChanPool sync.Pool
}
type workerChan struct {
lastUseTime time.Time
ch chan net.Conn
}
請求 / 響應 對象
請求對象 Request
和響應對象 Response
都是通過對象池進行管理的,對應的代碼如下:
// client.go
var (
requestPool sync.Pool
responsePool sync.Pool
)
// 從對象池中獲取 Request 對象
func AcquireRequest() *Request {
...
}
// 歸還 Request 對象到對象池中
func ReleaseRequest(req *Request) {
...
}
// 從對象池中獲取 Response 對象
func AcquireResponse() *Response {
...
}
// 歸還 Response 對象到對象池中
func ReleaseResponse(resp *Response) {
...
}
Cookie 對象
Cookie
對象也是通過對象池進行管理的,對應的代碼如下:
// cookie.go
var cookiePool = &sync.Pool{
New: func() interface{} {
return &Cookie{}
},
}
// 從對象池中獲取 Cookie 對象
func AcquireCookie() *Cookie {
...
}
// 歸還 Cookie 對象到對象池中
func ReleaseCookie(c *Cookie) {
...
}
其他對象複用
$ grep -inr --include \*.go "sync.Pool" $(go list -f {{.Dir}} github.com/valyala/fasthttp) | wc -l
# 輸出如下
38
通過輸出結果可以看到,fasthttp
中一共有 38
個對象是通過對象池進行管理的,可以說幾乎複用了所有對象,So Crazy!
[]byte 複用
fasthttp
中複用的對象在使用完成後歸還到對象池之前,需要調用對應的 Reset
方法進行重置,如果對象中包含 []byte
類型的字段, 那麼會直接進行復用,而不是初始化新的 []byte
, 例如 URI
對象的 Reset
方法:
// 重置 URI 對象
// 從方法的內部實現中可以看到,類型爲 []byte 的所有字段都被複用了
func (u *URI) Reset() {
u.pathOriginal = u.pathOriginal[:0]
u.scheme = u.scheme[:0]
u.path = u.path[:0]
u.queryString = u.queryString[:0]
u.hash = u.hash[:0]
u.username = u.username[:0]
u.password = u.password[:0]
u.host = u.host[:0]
...
}
此外,涉及到單個字段的修改,如果字段是 []byte
類型,還是會直接複用,例如 Cookie
對象的這幾個方法:
func (c *Cookie) SetValue(value string) {
c.value = append(c.value[:0], value...)
}
func (c *Cookie) SetValueBytes(value []byte) {
c.value = append(c.value[:0], value...)
}
func (c *Cookie) SetKey(key string) {
c.key = append(c.key[:0], key...)
}
func (c *Cookie) SetKeyBytes(key []byte) {
c.key = append(c.key[:0], key...)
}
上面幾個方法的內部實現中,無一例外,都對 []byte
類型的參數進行了複用。
[]byte 和 string 轉換
fasthttp
專門提供了 []byte
和 string
這兩種常見的數據類型相互轉換的方法 ,避免了 內存分配 + 複製
,提升性能。
// s2b_new.go
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// b2s_new.go
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
高性能的 bytebufferpool
fasthttp
並沒有直接使用標準庫中的 bytes.Buffer
對象,而是引用了作者的另外一個包 valyala/bytebufferpool[2], 這個包的核心優化點是 避免內存拷貝 + 底層 byte 切片複用
,感興趣的讀者可以看看官方給出的 基準測試結果 [3]。
避免反射
fasthttp
中的所有 對象深拷貝
內部實現中都沒有使用 反射
,而是手動實現的,這樣可以完全規避 反射
帶來的影響,例如 Cookie
對象的拷貝實現:
// cookie.go
// Cookie 對象拷貝實現
func (c *Cookie) CopyTo(src *Cookie) {
c.Reset()
c.key = append(c.key, src.key...)
c.value = append(c.value, src.value...)
c.expire = src.expire
c.maxAge = src.maxAge
c.domain = append(c.domain, src.domain...)
c.path = append(c.path, src.path...)
c.httpOnly = src.httpOnly
c.secure = src.secure
c.sameSite = src.sameSite
}
從上面的代碼中可以看到,拷貝
的內部實現就是手動挨個複製字段,非常 原始
的解決方案。
另外,請求對象 Request
和響應對象 Response
的拷貝實現和 Cookie
有異曲同工之處:
// client.go
func (req *Request) CopyTo(dst *Request) {
...
}
func (resp *Response) CopyTo(dst *Response) {
...
}
fasthttp 的問題
軟件工程沒有銀彈,高性能的背後必然是以某些條件作爲代價的,fasthttp
的主要問題有:
-
• 降低了代碼可讀性 (如果不瞭解
fasthttp
的設計理念,貿然讀代碼很可能無法理解各種方法實現) -
• 增加了開發複雜性,代碼開發量要比使用標準庫高,對象複用導致了
申請/歸還
流程彷佛回到了C/C++
語言手動管理內存模式 -
• 增加了開發者心智負擔,如果已經習慣了標準庫的開發模式,很容易寫出
Bug
-
• 如果業務中有
異步
處理場景,框架核心的對象複用
機制可能導致各種問題,如對象提前歸還、對象指針hang
起、還有更嚴重的對象字段被重置後繼續引用 (這類業務邏輯問題比較難排查)
多核系統的性能優化技巧
-
• 使用
reuseport
監聽 (SO_REUSEPORT
允許在多核服務器上線性擴展服務器性能,詳細信息請參閱 這個鏈接 [4] ) -
• 使用
GOMAXPROCS=1
爲每個 CPU 核運行一個單獨的服務器實例 (進程和 CPU 綁定) -
• 確保多隊列網卡的中斷均勻分佈在 CPU 內核之間,詳細信息請參閱 [這個鏈接](https://blog.cloudflare.com/how-to-achieve-low-latency/
fasthttp 最佳實踐
-
• 儘可能複用對象和
[]byte buffers
, 而不是重新分配 -
• 使用
[]byte
特性技巧 -
• 使用
sync.Pool
對象池 -
• 在生產環境對程序進行性能分析,
go tool pprof --alloc_objects app mem.pprof
通常比go tool pprof app cpu.pprof
更容易體現性能瓶頸 -
• 爲
hot path
上的代碼編寫測試和基準測試 -
• 避免
[]byte
和string
直接進行類型轉換,因爲這可能會導致內存分配 + 複製
,可以參考fasthttp
包內的s2b
方法和b2s
方法 -
• 定期對代碼進行 競態檢測 [5], 一般會直接集成到
CI
中 -
• 使用
quicktemplate
而非html/template
模板
是否採用 fasthttp
fasthttp
是爲一些高性能邊緣場景設計的,如果你的業務需要支撐較高的 QPS
並且保持一致的低延遲時間,那麼採用 fasthttp
是非常合理的, 反之 fasthttp
可能並不適合 (增加開發複雜度和開發者心智負擔)。大多數情況下,標準庫 net/http
是更好的選擇,因爲它簡單易用並且兼容性很高。如果你的業務流量很少,那麼兩者之間的 所謂性能差異
幾乎可以忽略。
Reference
-
• valyala/fasthttp[6]
-
• 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]
競態檢測: 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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aFdRLWAziAb_4aUdHYUipg