测试
使用一个简单的例子测试fasthttp和原生net/http库,代码如下:
fasthttp:
1 2 3 4 5 6 7 8 9 10 11 | func FastHttpBench() { router := fasthttprouter.New() router.GET("/", FastHandle) fasthttp.ListenAndServe(":8080", router.Handler) } func FastHandle(ctx *fasthttp.RequestCtx) { a := rand.Intn(100) a = a * a fmt.Fprintln(ctx, strconv.Itoa(a)) } |
http:
1 2 3 4 5 6 7 8 9 10 | func HttpBench() { http.HandleFunc("/", HttpHanle) http.ListenAndServe(":8080", nil) } func HttpHanle(w http.ResponseWriter, r *http.Request) { a := rand.Intn(100) a = a * a fmt.Fprintln(w, strconv.Itoa(a)) } |
使用wrk测试(4线程,1000连接,持续60秒)
1 | wrk -t4 -c1000 -d60s --latency http://localhost:8080 |
fasthttp:
http:
可以看到请求数量以及响应时间上fasthttp的表现都更加的出色。
WorkPool实现
在并发量比较大的情况下,net/http对每一个请求都开启一个新的goroutine,并且在完成请求之后回收,这样会导致GC的压力很大。fasthttp使用workpool来完成对goroutine的复用。
看一下fasthttp中WorkPool实现的大致思路:
1、WorkPool结构体:
1 2 3 4 5 6 7 | type workerPool struct { WorkerFunc ServeHandler … ready []*workerChan workerChanPool sync.Pool … } |
2、在server的serve函数中调用到了WorkPool的serve函数中,其实现如下:
1 2 3 4 5 6 7 8 | func (wp *workerPool) Serve(c net.Conn) bool { ch := wp.getCh() if ch == nil { return false } ch.ch <- c return true } |
3、getCh()的实现,首先是看ready中有没有空闲的grroutine,有的话直接取出来处理请求。如果空闲队列是空的,则新建一个goroutine来处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | func (wp *workerPool) getCh() *workerChan { var ch *workerChan createWorker := false wp.lock.Lock() ready := wp.ready n := len(ready) - 1 if n < 0 { if wp.workersCount < wp.MaxWorkersCount { createWorker = true wp.workersCount++ } } else { ch = ready[n] ready[n] = nil wp.ready = ready[:n] } wp.lock.Unlock() if ch == nil { if !createWorker { return nil } vch := wp.workerChanPool.Get() ch = vch.(*workerChan) //在此处处理请求 go func() { wp.workerFunc(ch) wp.workerChanPool.Put(vch) }() } return ch } |
4、处理请求时使用server传过来的workFunc处理请求
1 2 3 4 5 6 7 8 9 10 11 | func (wp *workerPool) workerFunc(ch *workerChan) { … for{ wp.WorkerFunc(c) … if !wp.release(ch) { break } … } } |
每次处理完一个channel之后,把这个goroutine对应的workChan append到wp.ready中,等到下一次serve的调用从ready的切片中取出来继续在这个goroutine的for循环中继续处理,开始下一次循环。
sync.Pool
fasthttp中使用的sync.Pool也对其性能的提升起到了很大的作用,比如workPool中的workerChanPool也是用的sync.Pool实现的。可以在源码中看到,每次ready数组空的时候,通过Get去申请内存,调用完了workerFunc的时候Put来释放内存到pool中。
[]byte和string之间的转换
fasthttp中有实现byte数组和string之间的转换函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } func s2b(s string) (b []byte) { /* #nosec G103 */ bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) /* #nosec G103 */ sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b } |
可以看到都只是进行指针的转换和赋值操作,并没有进行重新的内存分配和赋值,所以会比直接使用类型转换操作来的快,但是源码中也有写道依赖于StringHeader和SliceHeader的实现,需要注意go后续的版本会不会变动。