引言
fasthttp是一個非常優秀的web server框架,號稱比官方的net/http快10倍以上。fasthttp用了很多黑魔法。俗話說,原始碼面前,了無祕密,我們今天通過原始碼來看一看她的goroutine pool的實現。
熱身
fasthttp寫server和原生的net/http寫法上基本沒有區別,這裡就不舉例子。直接找到入口函式,在根目錄下的server.go檔案中,我們從函式ListenAndServe()
跟蹤進去。從埠監聽到處理請求的函式呼叫鏈如下。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
func ListenAndServe(addr string, handler RequestHandler) error { s := &Server{ Handler: handler, } return s.ListenAndServe(addr) } // ListenAndServe serves HTTP requests from the given TCP addr. func (s *Server) ListenAndServe(addr string) error { ln, err := net.Listen("tcp", addr) if err != nil { return err } return s.Serve(ln) } // Serve blocks until the given listener returns permanent error. func (s *Server) Serve(ln net.Listener) error { ... wp := &workerPool{ WorkerFunc: s.serveConn, MaxWorkersCount: maxWorkersCount, LogAllErrors: s.LogAllErrors, Logger: s.logger(), } wp.Start() //啟動worker pool for { if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil { wp.Stop() if err == io.EOF { return nil } return err } if !wp.Serve(c) { s.writeFastError(c, StatusServiceUnavailable, "The connection cannot be served because Server.Concurrency limit exceeded") c.Close() if time.Since(lastOverflowErrorTime) > time.Minute { s.logger().Printf("The incoming connection cannot be served, because %d concurrent connections are served. "+ "Try increasing Server.Concurrency", maxWorkersCount) lastOverflowErrorTime = time.Now() } time.Sleep(100 * time.Millisecond) } c = nil } } |
上面程式碼中workerPool就是一個執行緒池。相關程式碼在server.go檔案的同級目錄下的workerpool.go檔案中。我們從上面程式碼涉及到的往下看。首先是workerPool struct
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
type workerPool struct { WorkerFunc func(c net.Conn) error MaxWorkersCount int LogAllErrors bool MaxIdleWorkerDuration time.Duration Logger Logger lock sync.Mutex workersCount int mustStop bool ready []*workerChan stopCh chan struct{} workerChanPool sync.Pool } type workerChan struct { lastUseTime time.Time ch chan net.Conn } |
workerPool sturct
中的WorkerFunc
是conn的處理函式,類似net/http
包中的ServeHTTP
。因為所有conn的處理都是一樣的,所以WorkerFunc
不需要和傳入的每個conn繫結,整個worker pool共用一個。workerChanPool
是sync.Pool物件池。
MaxIdleWorkerDuration是worker空閒的最長時間,超過就將worker關閉。workersCount是worker的數量。ready是可用的worker列表,也就是說所有goroutine worker是存放在一個陣列裡面的。這個陣列模擬一個類似棧的FILO佇列,也就是說我們每次使用的worker都從佇列的尾部開始取。wp.Start()
啟動worker pool。wp.Stop()
是出錯處理。wp.Serve(c)
是對conn進行處理的函式。我們先看一下wp.Start()
。
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 35 |
func (wp *workerPool) Start() { if wp.stopCh != nil { panic("BUG: workerPool already started") } wp.stopCh = make(chan struct{}) stopCh := wp.stopCh go func() { var scratch []*workerChan for { wp.clean(&scratch) select { case <-stopCh: return default: time.Sleep(wp.getMaxIdleWorkerDuration()) } } }() } func (wp *workerPool) Stop() { ... close(wp.stopCh) wp.stopCh = nil wp.lock.Lock() ready := wp.ready for i, ch := range ready { ch.ch <- nil ready[i] = nil } wp.ready = ready[:0] wp.mustStop = true wp.lock.Unlock() } |
簡單來說,wp.Start()
啟動了一個goroutine,負責定期清理worker pool中過期worker(過期=未使用時間超過MaxIdleWorkerDuration)。清理操作都在wp.clean()
函式中完成,這裡就不繼續往下看了。stopCh
是一個標示worker pool停止的chan。上面的for-select-stop是很常用的方式。wp.Stop()
負責停止worker pool的處理工作,包括關閉stopCh,清理閒置的worker列表(這時候還有一部分worker在處理conn,待其處理完成通過判斷wp.mustStop來停止)。這裡需要注意的一點是做資源清理的時候,對於channel需要置nil。下面看看最重要的函式wp.Serve()
。
核心
下面是wp.Serve()
函式的呼叫鏈。wp.Serve()
負責處理來自client的每一條連線。其中比較關鍵的函式是wp.getCh()
,她從worker pool的可用空閒worker列表尾部取出一個可用的worker。這裡有幾個邏輯需要注意的是:1.如果沒有可用的worker(比如處理第一個conn是,worker pool還是空的)則新建;2.如果worker達到上限,則直接不處理(這個地方感覺略粗糙啊!)。go func()
那幾行程式碼就是新建worker,我們放到下面說。
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 35 36 37 38 39 40 41 42 43 44 45 46 |
func (wp *workerPool) Serve(c net.Conn) bool { ch := wp.getCh() if ch == nil { return false } ch.ch <- c return true } 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() if vch == nil { vch = &workerChan{ ch: make(chan net.Conn, workerChanCap), } } ch = vch.(*workerChan) go func() { wp.workerFunc(ch) wp.workerChanPool.Put(vch) }() } return ch } |
workerFunc()
函式定義如下(去掉了很多不影響主線的邏輯),結合上一篇《如何裸寫一個goroutine pool》,還是熟悉的配方,熟悉的味道。這裡要看的wp.release()
是幹啥的。因為前面的wp.Serve()
函式只處理一個conn,所以for迴圈執行一次我們就可以把worker放到空閒佇列中去等待下一次conn過來,從程式碼中可以看出來放回果然是放到空閒佇列的末尾(可算和上面呼應上了)。還有上面提到的mustStop
,如果worker pool停止了,mustStop
就為true,那麼workerFunc
就要跳出迴圈,也就是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 |
func (wp *workerPool) workerFunc(ch *workerChan) { var c net.Conn var err error for c = range ch.ch { if c == nil { break } ... c = nil if !wp.release(ch) { break } } wp.lock.Lock() wp.workersCount-- wp.lock.Unlock() } 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 } |
總結
除了fasthttp,我還看了github上其他開源且star數在100以上的goroutine pool的實現,基本核心原理都在我上一篇文章中說的那些。fasthttp的實現多了一層goroutine回收機制,不得不說確實挺巧妙。fasthttp效能這麼好一定是有其原因的,原始碼之後再慢慢讀。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式