FastHTTP原始碼分析——“百花齊放”的協程池
原文:HTTP%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E2%80%9C%E7%99%BE%E8%8A%B1%E9%BD%90%E6%94%BE%E2%80%9D%E7%9A%84%E5%8D%8F%E7%A8%8B%E6%B1%A0.md" rel="nofollow,noindex" target="_blank">FastHTTP原始碼分析——“百花齊放”的協程池
宣告
閱讀本編文章需要go語言基礎和對資源池有一些瞭解。
go 版本為1.11,FastHTTP 為2018-11-23的最新master版本
前言
在開始前我們先來簡單定義一下協程池:能夠達到協程資源複用
。在這個定義下協程池的實現可以說是“百花齊放”了,找一下熱門的go語言開源專案都會有協程池的不同實現方式。 有基於連結串列實現的pingcap/tidb/blob/v1.0.9/util/goroutine_pool/gp.go" rel="nofollow,noindex" target="_blank">Tidb
,有基於環形佇列實現的Jaeger
,有基於陣列棧實現的FastHTTP
等,種類繁多任君選擇。這麼多的協程池實現可以歸納成二種:
這2種實現中,個人比較喜歡第二種按需建立,FastHTTP 也是使用第二種方式,所以我們來看看它是如何實現的。
FastHTTP協程池簡介
在介紹FastHTTP 協程池之前先做一下簡單的介紹。workerChan 和協程一一對應,相同的生命週期,可以把workerChan 看成是協程的門牌,使用憑證,引路子等。 整個協程池的實現主要由workerPool 和workerChan 組成。FastHTTP 的協程池使用按需建立的方式,當有一個請求進來時建立一個協程,請求處理完成,就會把協程的workerChan 放入workerPool 的陣列棧[workerPool.ready ]裡面,再有新的請求就從workerPool.ready 獲取workerChan ,複用協程,以此迴圈。
協程池用在哪裡
-
go官方原生
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的
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則使用協程池處理。
獲取workerChan
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
就會建立。
把workerChan放入Slice尾部
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-4104937881320 ns/op107818560 B/op401680 allocs/op BenchmarkFastHttpPool-410380807481 ns/op13444607 B/op169946 allocs/op BenchmarkAntsPoll-410429482715 ns/op20756724 B/op302093 allocs/op PASS okstudy_go/gopool72.891s
從上面的對比來看使用協程池的收益還不少。
結語
FastHTTP 協程池的實現方式是我所瞭解的幾種實現中,效能是比較突出的,當然其他協程池的實現方式也很有學習參考價值,在這個過程中複習了連結串列,陣列棧,環形佇列的使用場景。收穫頗多。