這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
valyala/fasthttp 是號稱比官方net/http庫更快的 http server 庫。就去順便研究了,發現一些細節的不同。
處理 net.Conn 的 goroutine
處理net.Conn的goroutine的使用方式,和標準庫有很大差別。在標準庫,net.Listener.Accept() 到一個串連,就會開啟一個goroutine:
// Serve accepts incoming connections on the Listener l, creating a// new service goroutine for each. The service goroutines read requests and// then call srv.Handler to reply to them.func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { ...... } ...... c, err := srv.newConn(rw) if err != nil { continue } c.setState(c.rwc, StateNew) // before Serve can return go c.serve() // 在這裡建立一個goroutine處理net.Conn的實際邏輯 }}
但是在valyala/fasthttp中使用的是worker的形式,開啟固定數量的goroutine處理net.Conn。
server.go#L582:
func (s *Server) Serve(ln net.Listener) error { var lastOverflowErrorTime time.Time var lastPerIPErrorTime time.Time var c net.Conn var err error maxWorkersCount := s.getConcurrency() // 擷取worker的並發數 // 建立一個worker池 wp := &workerPool{ WorkerFunc: s.serveConn, // 每個net.Conn的處理邏輯 MaxWorkersCount: maxWorkersCount, Logger: s.logger(), } // 開啟worker內池中處理chan的清理,處理掉沒有在處理請求的chan wp.Start() for { // 從listener收到net.Conn // 這個裡面做的IP串連數量控制,超過會返回錯誤 if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil { wp.Stop() if err == io.EOF { return nil } return err } // 讓worker池去處理net.Conn if !wp.Serve(c) { 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() } } c = nil }}
下一步就是 wp.Serve(c),在 workerpool.go#L92:
func (wp *workerPool) Serve(c net.Conn) bool { ch := wp.getCh() // 從worker裡擷取一個workChan if ch == nil { return false // 如果擷取不到workChan,返回false // 上面的代碼提示錯誤,超過並發量了 } ch.ch <- c return true // 把net.Conn扔進workChan的chan中}
之後來看怎麼擷取一個workChan,在workerpool.go#L101:
func (wp *workerPool) getCh() *workerChan { var ch *workerChan createWorker := false wp.lock.Lock() chans := wp.ready n := len(chans) - 1 // 嘗試擷取wp.ready中閒置workChan if n < 0 { // 沒有閒置workChan,需要建立 if wp.workersCount < wp.MaxWorkersCount { createWorker = true wp.workersCount++ } } else { // 從wp.ready閒置workChan中取出最後一個 ch = chans[n] wp.ready = chans[:n] } wp.lock.Unlock() if ch == nil { if !createWorker { return nil } // 從公用池中取出一個workChan來用 vch := workerChanPool.Get() if vch == nil { // 公用池裡都沒有,建立一個新的 vch = &workerChan{ ch: make(chan net.Conn, 1), } } ch = vch.(*workerChan) // 在一個goroutine裡處理workChan go func() { // 開始讀取操作這個workChan wp.workerFunc(ch) // workChan用完了放回公用池 workerChanPool.Put(vch) }() } return ch}
上面看到ch.ch <- c,將net.Conn扔進了workChan的chan中。chan的處理邏輯在wp.workerFunc(ch),在workerpool.go#L152:
func (wp *workerPool) workerFunc(ch *workerChan) { var c net.Conn var err error ...... for c = range ch.ch { if c == nil { // 這裡注意,傳入nil就跳出迴圈,不處理這個workChan break } // 調用WorkerFunc處理每個net.Conn // 這個WorkerFunc在上文代碼有, // WorkerFunc:s.serveConn if err = wp.WorkerFunc(c); err != nil && err != errHijacked { errStr := err.Error() if !strings.Contains(errStr, "broken pipe") && !strings.Contains(errStr, "reset by peer") { wp.Logger.Printf("error when serving connection %q<->%q: %s", c.LocalAddr(), c.RemoteAddr(), err) } } if err != errHijacked { c.Close() } c = nil // 記得用完了放到wp.ready切片中 // 以便重複使用 if !wp.release(ch) { break } }}
看到這裡就可以總結一下。valyala/fasthttp其實是把net.Conn分配到一定數量的goroutine中執行,而不是一對一。換句話說,當goroutine數量巨大的時候,環境切換成本開始有明顯的效能影響。標準庫在並發量很大的時候面臨這個問題。valyala/fasthttp就使用了worker規避這個問題。goroutine本身就是輕量級的協程,可以即開即用。worker盡量重用每個goroutine,從而可以控制住goroutine的數量(預設的最大chan數量為256×1024)。而且如果http請求阻塞,會霸佔workChan,直到把worker裡的workChan耗盡(有keepAlive逾時配置來處理這個問題),但是這就限制使用情境。valyala/fasthttp只適合http短串連的情境,不適合做長串連,或websocket支援。
另外一個發現是*RequestCtx內容相關的池。
*RequestCtx 的池
標準庫裡對於類似的http請求上下文用的是*http.response這個對象,問題是每次都是新的。
// Serve a new connection.func (c *conn) serve() { ...... for { // 這裡返回的是*http.response w, err := c.readRequest() if c.lr.N != c.server.initialLimitedReaderSize() { // If we read any bytes off the wire, we're active. c.setState(c.rwc, StateActive) } ...... // 要求實現的 http.Handler 介面 // 在這裡被使用 serverHandler{c.server}.ServeHTTP(w, w.req) ...... }}
valyala/fasthttp類似的結構*RequestCtx用的是池,server.go#L743有:
ctx := s.acquireCtx(c)// 其實就是:func (s *Server) acquireCtx(c net.Conn) *RequestCtx { v := s.ctxPool.Get() var ctx *RequestCtx if v == nil { ctx = &RequestCtx{ s: s, } ctx.v = ctx v = ctx } else { ctx = v.(*RequestCtx) } ctx.initID() ctx.c = c return ctx}
*RequestCtx.Request和*RequestCtx.Response支援reset,使更安全的使用,比如 server.go#L776有:
err = ctx.Request.Read(br)// 就是func (req *Request) Read(r *bufio.Reader) error { req.clearSkipHeader() err := req.Header.Read(r) if err != nil { return err } if req.Header.IsPost() { req.body, err = readBody(r, req.Header.ContentLength(), req.body) if err != nil { req.Reset() // 出錯了要reset,用完了的時候同時也要 // 在L1030,releaseReader 方法 // 其實就是把r這個 *bufio.Reader 直接 Reset 再放回公用池 // 下次用的時候有一個 *bufio.Reader return err } req.Header.SetContentLength(len(req.body)) } return nil}
總的來說,用池來來減少對象數量,也是增強效能最常見的方法。標準庫和 valyala/fasthttp 都對 *bufio.Reader 和 *bufio.Writer 做了池的處理。不過對於頻繁存取的服務,池的效率提升比較有限。而且sync.Pool沒有容量控制,有時會變得不可控,需要注意一下。
Thanks
上文是在 Go實踐群(386056972) 和群友討論時順便深入閱讀的結果。感謝群友 華子 的支援。