1

Fasthttp 为什么比标准库快 10 倍 ?

 1 year ago
source link: https://www.51cto.com/article/751378.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Fasthttp 为什么比标准库快 10 倍 ?

作者:蛮荆 2023-04-07 08:17:39
本文不会讲解 fasthttp 的应用方法,而是会重点分析 fasthttp 高性能的背后实现原理。

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

  • • Go 高性能代码的 30 个 Tips
  • • valyala/fasthttp[6]
  • • fasthttp中运用哪些go优化技巧?
  • • 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

责任编辑:武晓燕 来源: 洋芋编程

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK