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.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([]byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

// b2s_new.go
func s2b(s string) ([]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 最佳實踐

是否採用 fasthttp

fasthttp 是爲一些高性能邊緣場景設計的,如果你的業務需要支撐較高的 QPS 並且保持一致的低延遲時間,那麼採用 fasthttp 是非常合理的, 反之 fasthttp 可能並不適合 (增加開發複雜度和開發者心智負擔)。大多數情況下,標準庫 net/http 是更好的選擇,因爲它簡單易用並且兼容性很高。如果你的業務流量很少,那麼兩者之間的 所謂性能差異 幾乎可以忽略。

Reference

引用鏈接

[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