原文:FastHTTP源码分析——“百花齐放”的协程池git
阅读本编文章须要go语言基础和对资源池有一些了解。github
go 版本为1.11,FastHTTP为2018-11-23的最新master版本数组
在开始前咱们先来简单定义一下协程池:可以达到协程资源复用
。在这个定义下协程池的实现能够说是“百花齐放”了,找一下热门的go语言开源项目都会有协程池的不一样实现方式。 有基于链表实现的Tidb,有基于环形队列实现的Jaeger,有基于数组栈实现的FastHTTP等,种类繁多任君选择。这么多的协程池实现能够概括成二种:app
这2种实现中,我的比较喜欢第二种按需建立,FastHTTP也是使用第二种方式,因此咱们来看看它是如何实现的。源码分析
在介绍FastHTTP协程池以前先作一下简单的介绍。workerChan和协程一一对应,相同的生命周期,能够把workerChan当作是协程的门牌,使用凭证,引路子等。 整个协程池的实现主要由workerPool和workerChan组成。性能
http.Server
net/http/server.go #2805 func (srv *Server) Serve(l net.Listener) error { ...... for { rw, e := l.Accept() ...... //FastHTTP在这步使用协程池 go c.serve(ctx) } }
fasthttp.ListenAndServe
github.com/valyala/fasthttp/server.go 1489 func (s *Server) Serve(ln net.Listener) error { ...... for { if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil { ...... } //对应go原生的 go c.serve(ctx) if !wp.Serve(c) { ...... } ...... } }
在go原生的
http.Server
包中,当接收到新请求就会启动一个协程处理,而FastHTTP则使用协程池处理。
github.com/valyala/fasthttp/workerpool.go #156 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 ch = ready[n] ready[n] = nil wp.ready = ready[:n] } wp.lock.Unlock() if ch == nil { //若是协程数超过上限,直接抛弃当前请求 if !createWorker { return nil } vch := wp.workerChanPool.Get() if vch == nil { vch = &workerChan{ ch: make(chan chan struct{}, workerChanCap), } } ch = vch.(*workerChan) //ch和协程绑定 go func() { wp.workerFunc(ch) wp.workerChanPool.Put(vch) }() } return ch }
在go语言中不一样协程之间的通信使用channel
,在协程池中也不例外,FastHTTP建立了一个协程,就会和一个workerChan
绑定,使用方根据这个workerChan
就可使用协程池里的资源。从上面的代码能够看出,使用协程池的资源,都是先从Slice的尾部弹出workerChan
,在把workerChan
交给使用放,若是Slice没有workerChan
就会建立。
github.com/valyala/fasthttp/workerpool.go #194 func (wp *workerPool) release(ch *workerChan) bool { //用户清理 ch.lastUseTime = time.Now() wp.lock.Lock() if wp.mustStop { wp.lock.Unlock() return false } //往尾部追加 wp.ready = append(wp.ready, ch) wp.lock.Unlock() return true }
当协程完成工做后,就会把
workerChan
放回Slice尾部,以待其余请求使用。
workerChan
github.com/valyala/fasthttp/workerpool.go #98 func (wp *workerPool) clean(scratch *[]*workerChan) { ...... currentTime := time.Now() wp.lock.Lock() ready := wp.ready n := len(ready) i := 0 for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration { i++ } *scratch = append((*scratch)[:0], ready[:i]...) if i > 0 { m := copy(ready, ready[i:]) for i = m; i < n; i++ { ready[i] = nil } wp.ready = ready[:m] } wp.lock.Unlock() ...... tmp := *scratch for i, ch := range tmp { //让协程中止工做 ch.ch <- nil tmp[i] = nil } }
按期清理是为了不在常态下空闲的协程过多,加剧了调度层的负担。使用按需建立协程池的方式存在这样一个问题,高峰期的时候建立了不少协程,高峰期事后不少协程处于空闲状态,这就形成了没必要要的开销。因此须要一种过时机制。在这里数组栈(FILO)的优势也体现出来了,由于栈的特色不活跃的workerChan
都放在了数组的头部,因此只须要从数组头部开始轮询,一直到找到未过时的workerChan
,再把这部分清理掉,就达到清理的效果,而且不须要轮询整个数组。
花了点时间对FastHTTP的协程池进行了压测 代码。
apple:gopool apple$ go test -bench=. -test.benchmem goos: darwin goarch: amd64 pkg: study_go/gopool BenchmarkNotPool-4 10 4937881320 ns/op 107818560 B/op 401680 allocs/op BenchmarkFastHttpPool-4 10 380807481 ns/op 13444607 B/op 169946 allocs/op BenchmarkAntsPoll-4 10 429482715 ns/op 20756724 B/op 302093 allocs/op PASS ok study_go/gopool 72.891s
从上面的对比来看使用协程池的收益还很多。
FastHTTP协程池的实现方式是我所了解的几种实现中,性能是比较突出的,固然其余协程池的实现方式也颇有学习参考价值,在这个过程当中复习了链表,数组栈,环形队列的使用场景。收获颇多。