fasthttp协程池原理

430 阅读6分钟

2019.06学习笔记

fasthttp协程池,fasthttp与net/http的异同

fasthttp协程池

fasthttp实现的是一个可动态伸缩的协程池。

协程池在fasthttp中的使用

Server.Serve函数实现了经典的listen-accept-serve循环中的accept和serve。

// Serve从listener中accept连接并交给协程池处理,函数会一直阻塞到listener返回非临时性的错误
func (s *Server) Serve(ln net.Listener) error {
    var c net.Conn
    var err error

    // ...
    // 处理Listener数组,支持同时Serve多个Listener
    // ...

    //  获取处理请求的最大并发数作为协程池的最大协程数,默认值为256*1024
    maxWorkersCount := s.getConcurrency()
    //  创建协程池,每个Listener会有自己的协程池,也就是maxWorkersCount限制的是单个Listener的并发量
    wp := &workerPool{
        // 用户提供的协程池工作函数,s.serveConn即为处理一个http连接的函数
        WorkerFunc:      s.serveConn,
        // 最大协程数
        MaxWorkersCount: maxWorkersCount,
        // log
        LogAllErrors:    s.LogAllErrors,
        Logger:          s.logger(),
        // 统计连接状态的函数
        connState:       s.setState,
    }
    // 调用wp.Start()初始化协程池
    wp.Start()

    for {
        // accept一个连接,如果发生了非临时性的错误则调用wp.Stop()停止协程池后返回
        if c, err = acceptConn(s, ln); err != nil {
            wp.Stop()
            if err == io.EOF {
                return nil
            }
            return err
        }
        // 调用wp.Serve()把连接交给协程池处理
        if !wp.Serve(c) {
            // 错误处理
        }
        c = nil
    }
}

协程池的3个api函数wp.Start(),wp.Serve(),wp.Stop()

wp.Start初始化

func (wp *workerPool) Start() {
    if wp.stopCh != nil {
        panic("BUG: workerPool already started")
    }
    // stopCh主要用于通知clean协程退出
    wp.stopCh = make(chan struct{})
    stopCh := wp.stopCh
    // workerChan的分配也用对象池处理
    // 每个协程都有一个workerChan
    // type workerChan struct {
    //   lastUseTime time.Time      // 最近使用时间,clean协程用这个字段清理空闲协程
    //   ch          chan net.Conn  // 用于提交连接,如果提交的是nil表示通知协程退出
    // }
    wp.workerChanPool.New = func() interface{} {
        return &workerChan{
            ch: make(chan net.Conn, workerChanCap),
        }
    }
    go func() {
    // 开启一个clean协程,定时清理空闲时间超过MaxIdleWorkerDuration的协程
    // MaxIdleWorkerDuration默认为10s
        var scratch []*workerChan
        for {
            wp.clean(&scratch)
            select {
            case <-stopCh:
                return
            default:
                time.Sleep(wp.getMaxIdleWorkerDuration())
            }
        }
    }()
}

func (wp *workerPool) clean(scratch *[]*workerChan) {
    maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration()

    //  最近使用时间小于time.Now() - maxIdleWorkerDuration的协程都要清理掉
    criticalTime := time.Now().Add(-maxIdleWorkerDuration)

    wp.lock.Lock()
    // ready为所有空闲协程workerChan组成的数组栈
    // 用法为FILO,数组尾部作为栈顶,看注释说用栈可以提高cpu cache命中率(理论上)
    ready := wp.ready
    n := len(ready)

    // lastUseTime是协程的最近使用时间,而ready是按FILO使用的,所以ready自然就是一个按lastUseTime升序排列的数组,可以用二分找到最近使用时间小于time.Now() - maxIdleWorkerDuration的协程,这个协程以及排在它之前的协程都要清理
    l, r, mid := 0, n-1, 0
    for l <= r {
        mid = (l + r) / 2
        if criticalTime.After(wp.ready[mid].lastUseTime) {
            l = mid + 1
        } else {
            r = mid - 1
        }
    }
    i := r
    if i == -1 {
        wp.lock.Unlock()
        return
    }

    // 把要清理的协程先放到scratch数组,释放锁后才执行清理动作,减少持锁粒度
    *scratch = append((*scratch)[:0], ready[:i+1]...)
    // 把不需要清理的协程拷贝回数组的前端
    m := copy(ready, ready[i+1:])
    for i = m; i < n; i++ {
        ready[i] = nil
    }
    wp.ready = ready[:m]
    wp.lock.Unlock()

    // 仅仅往每个协程的workerChan写入一个nil就可以了,协程判断到nil会自行退出
    tmp := *scratch
    for i := range tmp {
        tmp[i].ch <- nil
        tmp[i] = nil
    }
}

wp.Serve提交连接

func (wp *workerPool) Serve(c net.Conn) bool {
    // wp.getCh获取一个空闲协程的workerChan,然后把连接提交给协程处理
    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 {
        // 没有空闲协程了,判断当前协程数有没超过MaxWorkersCount限制,没有就设置创建标志,增加协程计数
        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 {
            // 超出了最大协程数量限制,返回nil,fasthttp判断到返回值是nil会sleep一段时间
            return nil
        }
        vch := wp.workerChanPool.Get()
        ch = vch.(*workerChan)
        // 创建新的协程,消费对应workerChan提交的连接
        go func() {
            wp.workerFunc(ch)
            wp.workerChanPool.Put(vch)
        }()
    }
    // 到这里ch不为nil,要么是获取到空闲协程要么是创建了新的,返回结果
    return ch
}

func (wp *workerPool) workerFunc(ch *workerChan) {
    var c net.Conn

    var err error
    for c = range ch.ch {
        // nil表示需要退出,可能是wp.Stop被调用了或者空闲太久被clean协程清理了
        if c == nil {
            break
        }
        // 调用用户传入的ServeConn函数处理连接,可以处理多个http请求,支持pipeline
        if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
            // ...
            // 处理错误,输出log
            // ...
        }
        if err == errHijacked {
            // 这个连接被用户剥离了,可能协议升级为了websocket等,这时不要关闭连接
            wp.connState(c, StateHijacked)
        } else {
            // 这个连接的请求处理完成了,关闭连接
            _ = c.Close()
            wp.connState(c, StateClosed)
        }
        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()
    // 判断是否自己在处理连接时wp.Stop被调用了,是则直接退出
    if wp.mustStop {
        wp.lock.Unlock()
        return false
    }
    // 把workerChan返回ready栈顶,等待下一个连接被提交
    wp.ready = append(wp.ready, ch)
    wp.lock.Unlock()
    return true
}

wp.Stop退出

func (wp *workerPool) Stop() {
    if wp.stopCh == nil {
        panic("BUG: workerPool wasn't started")
    }
    // 通知clean协程退出
    close(wp.stopCh)
    wp.stopCh = nil
    wp.lock.Lock()
    // 通知所有空闲协程退出,也是网workerChan写一个nil就行了
    ready := wp.ready
    for i := range ready {
        ready[i].ch <- nil
        ready[i] = nil
    }
    wp.ready = ready[:0]
    // 对于正在忙的协程,做完一个循环后会判断mustStop标志,如果为true也会退出
    wp.mustStop = true
    wp.lock.Unlock()
}

fasthttp与net/http的异同

  1. fasthttp与net/http都支持http pipeline,即不用等第一个请求响应返回就可以发第二个,响应会按请求的顺序返回。
  2. fasthttp与net/http对文件的处理都是直接读取到用户的临时目录中,然后把元数据封装进Request结构体供用户使用。由于net/http设计的body是流式的,这个行为可以定制,比如不接收文件或者读取到别的目录等,可以设置不处理文件,然后在用户handler中自行用MultipartReader等类读取解析。而fasthttp要定制似乎只能修改源码了。
  3. fasthttp与net/http对http request body处理的不同之处。fasthttp对于非文件和chunked编码的处理都是直接读取完整个body才调用用户的handler。net/http对于非文件和chunked编码的处理是封装了相应的limitedReader(通过content-lenght请求头),chunkedReader然后马上调用用户handler,将body的读取权交给用户,如果用户handler返回后body没读完,会调用io.Discard把body读完(为了符合http协议)。
  4. fasthttp与net/http对http response body处理的不同之处。fasthttp会等用户handler返回才会开始写body。net/http内部用了个2kb的缓冲区,如果写满了handler仍然没有返回,会自动切换为chunked编码然后开始写响应头和body,这之后再更改响应头就无效了,因为body已经开始返回给客户端了,如果没有超过2kb,技术上是可以再更改响应头的,但是为了提供一个统一的接口,net/http禁止了这样的行为。

总结

  1. fasthttp通过对象池复用来减少内存的分配,减少垃圾的数量,进而减少gc的压力,通过协程池来减少协程创建和销毁的开销,据介绍通过这两个池可以做到零gc。
  2. fasthttp要等到body读取完或写入完了才会开始处理请求和响应,是非流式的。net/http把body封装成Reader和Writer供用户使用,是流式的,可以更灵活地实现定制功能。