fasthttp 是一个使用 Go 语言开发的 HTTP 包,主打高性能,针对 HTTP 请求响应流程中的 hot path 代码进行了优化,达到零内存分配,性能比标准库的 net/http 快 10 倍。
上面是来自官方 Github 主页的项目介绍,抛开其介绍内容不谈,光从名字本身来看,作者对项目代码的自信程度可见一斑。
本文不会讲解 fasthttp 的应用方法,而是会重点分析 fasthttp 高性能的背后实现原理。
我们可以通过基准测试看看 fasthttp 是否真的如描述所言,吊打标准库的 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$ 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 对象表示 连接处理 工作池,这样可以控制连接建立后的处理方式,而不是像标准库 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!
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 类型的参数进行了复用。
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
}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 是为一些高性能边缘场景设计的,如果你的业务需要支撑较高的 QPS 并且保持一致的低延迟时间,那么采用 fasthttp 是非常合理的, 反之 fasthttp 可能并不适合 (增加开发复杂度和开发者心智负担)。大多数情况下,标准库 net/http 是更好的选择,因为它简单易用并且兼容性很高。 如果你的业务流量很少,那么两者之间的 所谓性能差异 几乎可以忽略。
[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
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or