閱讀 valyala/fasthttp —— 比官方庫更快的 HTTP 包

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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) 和群友討論時順便深入閱讀的結果。感謝群友 華子 的支援。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.