概述

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=10sBenchmarkNetHTTPClientDoFastServer-4                      2000000       8774 ns/op     2619 B/op       35 allocs/opBenchmarkNetHTTPClientGetEndToEnd1TCP-4                    500000      22951 ns/op     5047 B/op       56 allocs/opBenchmarkNetHTTPClientGetEndToEnd10TCP-4                  1000000      19182 ns/op     5037 B/op       55 allocs/opBenchmarkNetHTTPClientGetEndToEnd100TCP-4                 1000000      16535 ns/op     5031 B/op       55 allocs/opBenchmarkNetHTTPClientGetEndToEnd1Inmemory-4              1000000      14495 ns/op     5038 B/op       56 allocs/opBenchmarkNetHTTPClientGetEndToEnd10Inmemory-4             1000000      10237 ns/op     5034 B/op       56 allocs/opBenchmarkNetHTTPClientGetEndToEnd100Inmemory-4            1000000      10125 ns/op     5045 B/op       56 allocs/opBenchmarkNetHTTPClientGetEndToEnd1000Inmemory-4           1000000      11132 ns/op     5136 B/op       56 allocs/op
fasthttp
$ GOMAXPROCS=4 go test -bench="kClient(Do|GetEndToEnd)" -benchmem -benchtime=10sBenchmarkClientDoFastServer-4                            50000000        397 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd1TCP-4                          2000000       7388 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd10TCP-4                         2000000       6689 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd100TCP-4                        3000000       4927 ns/op        1 B/op        0 allocs/opBenchmarkClientGetEndToEnd1Inmemory-4                    10000000       1604 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd10Inmemory-4                   10000000       1458 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd100Inmemory-4                  10000000       1329 ns/op        0 B/op        0 allocs/opBenchmarkClientGetEndToEnd1000Inmemory-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.gotype workerPool struct {    ready []*workerChan    workerChanPool sync.Pool}type workerChan struct {    lastUseTime time.Time    ch          chan net.Conn}
请求/响应 对象

请求对象Request​和响应对象Response都是通过对象池进行管理的,对应的代码如下:

// client.govar (    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.govar 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.gofunc b2s(b []byte) string {    return *(*string)(unsafe.Pointer(&b))}// b2s_new.gofunc 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.gofunc (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

推荐内容