Fasthttp 为什么比标准库快 10 倍 ?
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.
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
- • 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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK