fasthttp官方文档表示其性能达到golang标准库net/http的十倍,原因之一就是fasthttp使用了workerpool来处理conn,而标准库中是每次都新建一个goroutine。本文就来一窥fasthttp的workerpool的原理。
1. 结构
fasthttp中定义了workerpool结构体,其主要属性如下:
属性名 | 类型 | 用途 |
---|---|---|
WorkerFunc | ServerHandler | 处理连接的函数 |
MaxWorkersCount | time.Duration | worker最大的闲置时间,超过这个时间就会被清理 |
lock | sync.Mutex | 用于内部同步 |
workersCount | int | 当前正在使用的worker数量 |
mustStop | bool | 标记每个worker处理结束后退出 |
ready | []*workerChan | 保存可用的worker |
stopCh | chan struct{} | 用于让后台清理goroutine退出 |
workerChanPool | sync.Pool | 保存被清理的worker,如果ready没有可用的worker,便通过此pool.Get()创建一个新的worker |
值得注意的是:这里的worker和goroutine并不是对等的概念。worker其实就是一个conn的channel,每一个channel都有一个goroutine监听着。每当有连接到来时,就把连接塞到worker的channel中,这样监听的goroutine就可以执行对应的处理逻辑。
2. 原理
2.1 执行逻辑
2.2 启动worker
func (wp *workerPool) Serve(c net.Conn) bool {
ch := wp.getCh()
if ch == nil {
return false
}
ch.ch <- c
return true
}
当有连接需要处理时,先从workerpool中拿一个worker,再把conn塞到该worker的channel中去处理。至于如何获取worker,先判断workerpool的ready数组中是否有可用的worker,如果有则取一个出来处理conn,如果没有,则创建一个worker。
func (wp *workerPool) getCh() *workerChan {
var ch *workerChan
// ... 判断有没有可用的worker,若没有,是否可以创建新的worker
if ch == nil {
if !createWorker {
return nil // 不能创建,直接返回空
}
vch := wp.workerChanPool.Get() // 通过sync.Pool创建一个新的workerChan
ch = vch.(*workerChan)
go func() {
wp.workerFunc(ch)
wp.workerChanPool.Put(vch) //放回pool供下次使用
}()
}
return ch
}
那么每处理一个conn之后,怎样将worker挂起不销毁,供后面复用呢?继续看wp.workerFunc方法。
func (wp *workerPool) workerFunc(ch *workerChan) {
var c net.Conn
var err error
for c = range ch.ch {
if c == nil {
break
}
if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
// ... 处理conn
}
if !wp.release(ch) {
break
}
}
wp.lock.Lock()
wp.workersCount--
wp.lock.Unlock()
}
处理完conn之后,会调用release函数将worker(其实就是对应的channel)保存在ready数组,相当于告知系统该worker可用,然后此goroutine在监听ch.ch时由于阻塞被系统挂起,但是并没有销毁,等待下次从ready数组中取出去处理conn。
2.3 停止worker
从workerFunc.workerFunc()中的for循环可以看出,如果要停止一个worker,只要给对应的ch塞nil,就会跳出for循环,从而goroutine执行结束。
2.4 后台清理worker
如果worker保存在ready数组中过久,那么说明要处理的连接并不多,而worker闲置过久也是一种资源浪费,所以fasthttp会对worker进行回收。
workerpool的ready成员保存了可用的worker,后台每隔一个MaxIdleWorkerDuration时间就遍历一次ready,将idle时间超过MaxIdleWorkerDuration的worker停止。
go func() {
var scratch []*workerChan
for {
wp.clean(&scratch)
select {
case <-stopCh:
return
default:
time.Sleep(wp.getMaxIdleWorkerDuration())
}
}
}()
每个worker记录了上次执行的时间(lastUseTime),ready中的worker按照lastUseTime顺序排列。所以通过二分法可以知道需要清理的有哪些worker,然后给这些worker的ch塞nil便可停止。
3. 总结
- sync.Pool是资源复用的一种重要手段。
- workerpool的思想值得借鉴。这里的worker其实就是通过goroutine +监听专有channel。当有对象从channel中取出并执行时,就是一个运行中的worker。当处理完,继续监听channel时,就是一个等待的worker。此时放到池子(ready)里,等待下一次取出执行。可以参考这篇文章