文章目录
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短连接的场景,不适合做长连接。