fasthttp 线程池

1. fasthttp携程池关键结构体

去掉了部分不重要成员;

type workerPool struct {
    // Function for serving server connections.
    // It must leave c unclosed.
    WorkerFunc func(c net.Conn) error
    
    MaxIdleWorkerDuration time.Duration

    lock                sync.Mutex
    workersCount int
    ready              []*workerChan

    workerChanPool sync.Pool //避免每次频繁分配workerChan,使用pool
}
type workerChan struct { //工作协程
    lastUseTime time.Time
    ch          chan net.Conn // 带缓冲区 chan 处理完了一个conn 通过for range 再处理下一个,都在一个协程里面
}
2 fasthttp 协程池背景

fasthttp协程池,不是预分配,而是按需创建;

func (wp *workerPool) Serve(c net.Conn) bool {
    ch := wp.getCh() // 协程池内Get一个workChan
    ch.ch <- c
    return true // 把Client net.Conn扔进workChan的chan中
}
3. fasthttp socket侦听逻辑
func (s *Server) Serve(ln net.Listener) error {
    wp := &workerPool{// 协程池
        WorkerFunc:      s.serveConn, // 客户端处理连接逻辑,相同函数
    }
    wp.Start()

    for {
        // 从listener收到net.Conn
        c, err = acceptConn(s, ln, &lastPerIPErrorTime)
        // 让worker池去处理net.Conn
        wp.Serve(c)
    }
}
4. 获取client连接channel结构体封装

fasthttp的workerPool是lazyLoading,并不是一开始就创建N个worker。这么做省内存,大部分业务大时间服务器都不会有超高的并发压力,因此fasthttp作为通用框架,lazyLoading是默认的策略!

// workerpool.go
func (wp *workerPool) getCh() *workerChan {
    var ch *workerChan
    createWorker := false

    wp.lock.Lock()
    ready := wp.ready
    n := len(ready) - 1
    // 尝试获取wp.ready中空闲的workChan
    if n < 0 {
        // ready 为空,需要创建新的 workerChan,比如刚启动后,第一个请求
        if wp.workersCount < wp.MaxWorkersCount {
            createWorker = true
            wp.workersCount++
        }
    } else {
        // 从wp.ready空闲的workChan中取出最后一个
        ch = ready[n]
        ready[n] = nil
        wp.ready = ready[:n]
    }
    wp.lock.Unlock()

    if ch == nil {
        //  从 sync.Pool中取出 workerChan
        vch := wp.workerChanPool.Get()
        if vch == nil {
            vch = &workerChan{
                ch: make(chan net.Conn, workerChanCap),
            }
        }
        ch = vch.(*workerChan)
        // 创建goroutine处理请求,接收一个 chan *workerChan 作为参数
        go func() {
            // 上面 ch.ch <- c,将net.Conn扔进了workChan的chan中。chan的处理逻辑在wp.workerFunc(ch)
            wp.workerFunc(ch)
            wp.workerChanPool.Put(vch)// workChan用完了放回复用池
        }()
    }
    return ch
}
5. workerFunc循环执行

当请求密集时,一个 worker goroutine 可能会串行处理多个 connection。
wokerChan 在 Pool 中被复用,对GC的压力会减小很多。

func (wp *workerPool) workerFunc(ch *workerChan) {
    var c net.Conn
    for c = range ch.ch {
        if c == nil { // 这里注意,传入nil就跳出循环,不处理这个workChan
            break
        }
        // 调用WorkerFunc处理每个net.Conn
        // WorkerFunc = s.serveConn
        wp.WorkerFunc(c)
        // 每次release到ready队列时,直接放到队尾,每次取也是从队尾取。因此fasthttp的worker队列是FILO的,即先进后出。这会导致在并发小的情况下很多先入队的worker会一直空闲。因此fasthttp也支持设置IdleDuration参数,定期清理空闲的worker减少资源占用。
        if !wp.release(ch) {
            break
        }
    }
}
func (wp *workerPool) release(ch *workerChan) bool {
	ch.lastUseTime = time.Now()
	wp.lock.Lock()
	defer wp.lock.Unlock()
	wp.ready = append(wp.ready, ch) // 归还 ch 到ready,这里很巧妙,这样 getch 的时候就又可以把新的conn放到这个协程处理
	return true
}
6. clean 逻辑

wp.Start中启动一个goroutine,定期执行clean操作。wp.clean其实就是从头遍历ready队列,把空闲时间超过maxIdleWorkerDuration的都清理掉。
清理也很简单,直接向该channel发送一个nil就行了。别忘了之前workFunc中,当收到一个nil之后就直接break出大循环,做些收尾工作然后退出函数,整个goroutine也就可以被runtime回收了。
定期清理是为了避免在常态下空闲的协程过多,加重了调度层的负担。使用按需创建协程池的方式存在这样一个问题,高峰期的时候创建了很多协程,高峰期过后很多协程处于空闲状态,这就造成了不必要的开销。所以需要一种过期机制。在这里数组栈(FILO)的优点也体现出来了,因为栈的特点不活跃的workerChan都放在了数组的头部,所以只需要从数组头部开始轮询,一直到找到未过期的workerChan,再把这部分清理掉,就达到清理的效果,并且不需要轮询整个数组。

func (wp *workerPool) clean(scratch *[]*workerChan) {
    maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration()
    
    // Clean least recently used workers if they didn't serve connections
    // for more than maxIdleWorkerDuration.
    currentTime := time.Now()

    wp.lock.Lock()
    ready := wp.ready
    n := len(ready)
    i := 0
   // 从队列头部取出超过 最大空闲时间 的workerChan。
   // 最后使用的workerChan 一定是放回队列尾部。
   for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration {
        i++
    }
    // 把空闲的放入 scratch, 剩余的放回 ready
    *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()

    // Notify obsolete workers to stop.
    // This notification must be outside the wp.lock, since ch.ch
    // may be blocking and may consume a lot of time if many workers
    // are located on non-local CPUs.
    tmp := *scratch
    // 销毁操作就是向 chan net.Conn 中塞入一个 nil
    for i, ch := range tmp {
        ch.ch <- nil
        tmp[i] = nil
    }
}
10. 结论
  • fasthttp内部是把终端tcp连接(net.Conn)分配到一定数量的goroutine中执行,协程复用。标准库在并发量很大的时候面临一个连接对应一个协程,大并发时,协程切换消耗较大。
  • worker尽量重用每个goroutine,从而可以控制住goroutine的数量(默认的最大chan数量为256×1024)。但是如果http请求阻塞,会霸占workChan,直到把worker里的workChan耗尽,fasthttp只适合http短连接的场景,不适合做长连接。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值