上一篇文章阐述了fasthttp的workpool原理。除了workerpool,fasthttp还大量使用了别的技巧来提升性能,本文将对典型的技巧予以一一介绍。并在最后介绍fasthttp推荐的一些best practices。
fasthttp使用的技巧
1. 大量使用sync.Pool
fasthttp中很多对象都通过使用sync.Pool达到复用的目的,一是减少内存分配,二是减少GC。复用的对象包括:
- workerpool
- RequestCtx
- reader
- writer
我写了一个benchmark代码,比较利用sync.Pool创建RequestCtx和直接创建:
import (
"sync"
"testing"
"github.com/valyala/fasthttp"
)
func BenchmarkPool(b *testing.B) {
ctxPool := sync.Pool{
New: func() interface{} {
return new(fasthttp.RequestCtx)
},
}
for i := 0; i < b.N; i++ {
ctx := ctxPool.Get()
ctxPool.Put(ctx)
}
}
func BenchmarkNoPool(b *testing.B) {
var ctx *fasthttp.RequestCtx
for i := 0; i < b.N; i++ {
ctx = new(fasthttp.RequestCtx)
_ = ctx
}
}
执行benchmark:
go test -bench=. -benchtime=10s -benchmem syncpool_test.go
结果如下:
BenchmarkPool-12 958146309 11.90 ns/op 0 B/op 0 allocs/op
BenchmarkNoPool-12 38607038 281.9 ns/op 1408 B/op 1 allocs/op
可以看出(11.90 ns/op vs 281.9 ns/op),使用sync.Pool性能提升了 20 多倍。
2. 用slice而非map来存储kv
众所周知,http请求的header以及body其实就是一个kv字典,所以一般用map[string]string或者map[string][]string来表示。但是fasthttp使用了[]slice来存储kv,这样做的好处是:当参数使用完需要清理时不用释放内存,只需将长度变为0,其申请的内存还在,下次使用的时候直接覆盖就可以了,这样便于复用,避免重新申请内存。来看具体的代码:
type Args struct {
noCopy noCopy //nolint:unused,structcheck
args []argsKV
buf []byte
}
type argsKV struct {
key []byte
value []byte
noValue bool
}
// Reset clears query args. 清理时只需要将长度变为0
func (a *Args) Reset() {
a.args = a.args[:0]
}
func allocArg(h []argsKV) ([]argsKV, *argsKV) {
n := len(h)
if cap(h) > n { //分配时,如果容量够,那么不需要重新申请内存,直接增加长度即可
h = h[:n+1]
} else { //容量不够时才需重新分配
h = append(h, argsKV{
value: []byte{},
})
}
return h, &h[n]
}
这样做有一个 弊端 :当要查询的时候,时间复杂度是O(N),因为要遍历整个slice。但是一般一个请求kv项不会特别多,所以查询的性能损耗不明显:
func peekArgBytes(h []argsKV, k []byte) []byte {
for i, n := 0, len(h); i < n; i++ {
kv := &h[i]
if bytes.Equal(kv.key, k) {
return kv.value
}
}
return nil
}
3. 大量使用[]byte,而非string
在golang中,string是不可变的。也就是说要修改一个string,需要重新分配一块内存。而使用[]byte有两个好处:
1)[]byte可以支持修改,不需要重新申请内存。
2)[]byte使用后可以像以下一样将其长度置为0以表示清理,这样可以用来复用,避免下次使用重新申请。
var h []byte
func Reset(h []byte){
h = h[:0]
}
4. []byte和string的转换
string的底层其实是StringHeader结构:
type StringHeader struct {
Data uintptr
Len int
}
而slice底层是SliceHeader结构:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
如果直接用类型强转,则需新申请一块内存来存放Data数据。
然而两者结构相似,只是slice多了一个Cap属性。fasthttp基于这一点优化了两者的相互转换。
[]byte转换为string时,只需要将指针强转即可,相当于丢弃了Cap属性:
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
string转换为[]byte时,则新建一个string结构体,但是将[]byte的Data和Len赋给新建的结构体,无需新申请一块Data内存。
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
}
注: []byte和string的优化依赖golang底层对两者的实现,如果后面的版本中底层实现有所变动,可能该优化不再适用。
5. bufio.Peek代替bufio.ReadBytes
在bufio库中,Peek和ReadBytes都是读取字节,不同的是Peek是直接返回Reader的buf,而ReadBytes新申请一块内存,并将要读的那些字节从reader.buf中拷贝到新申请的内存中。所以,适用Peek的好处是读取字节时,可避免新申请内存。
func (b *Reader) Peek(n int) ([]byte, error) {
// ...
// 0 <= n <= len(b.buf)
var err error
if avail := b.w - b.r; avail < n {
// not enough data in buffer
n = avail
err = b.readErr()
if err == nil {
err = ErrBufferFull
}
}
return b.buf[b.r : b.r+n], err
}
func (b *Reader) ReadBytes(delim byte) ([]byte, error) {
full, frag, n, err := b.collectFragments(delim)
// Allocate new buffer to hold the full pieces and the fragment.
buf := make([]byte, n)
n = 0
// Copy full pieces and fragment in.
for i := range full {
n += copy(buf[n:], full[i])
}
copy(buf[n:], frag)
return buf, err
}
Best Practices
在fasthttp的基础上,这里罗列一些关于golang开发提升性能的best practice:
1)不要分配对象和[]byte,尽量复用它们,建议使用sync.Pool
2)可以通过pprof来分析程序:
go tool pprof --alloc_objects your-program mem.pprof # 分析对象分配
go tool pprof your-program cpu.pprof #分析cpu
3)写一些压测代码,来比较性能,golang官方测试文档
4)避免直接用类型强转[]byte和string,这会带来一些内存分配和拷贝开销。fasthttp提供巧妙的方法来做转换,避免了内存分配和拷贝,但是依赖于golang底层对slice和string的实现。
5)关于[]byte的一些小tip:
- golang可以使用nil类型的slice:
var (
// 均未初始化
dst []byte
src []byte
)
// 以下case,即使dst或者src是nil,都是没有问题的
dst = append(dst, src...)
copy(dst, src)
(string(src) == "")
(len(src) == 0)
src = src[:0]
// src是nil时也不会panic
for i, ch := range src {
doSomething(i, ch)
}
所以像下面的长度检查是不需要的:
srcLen := 0
if src != nil {
srcLen = len(src)
}
直接这样用就可以:
srcLen := len(src)
- string可以直接append在[]byte后面
dst = append(dst, "foobar"...)
- []byte可以将其长度扩展至cap,这在[]byte复用的时候经常用到
buf := make([]byte, 100)
a := buf[:10] // len(a) == 10, cap(a) == 100.
b := a[:100] // is valid, since cap(a) == 100.
- []byte和string的转换优化,这个上面已经提过好几次了。
总结
fasthttp是一个优秀的http框架,虽然它的一些用法和标准库不太一样,但是其底层优化性能的技巧很值得借鉴,在优化服务性能的时候,这些经验都可以拿来学习。