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的异同
- fasthttp与net/http都支持http pipeline,即不用等第一个请求响应返回就可以发第二个,响应会按请求的顺序返回。
- fasthttp与net/http对文件的处理都是直接读取到用户的临时目录中,然后把元数据封装进Request结构体供用户使用。由于net/http设计的body是流式的,这个行为可以定制,比如不接收文件或者读取到别的目录等,可以设置不处理文件,然后在用户handler中自行用MultipartReader等类读取解析。而fasthttp要定制似乎只能修改源码了。
- 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协议)。
- fasthttp与net/http对http response body处理的不同之处。fasthttp会等用户handler返回才会开始写body。net/http内部用了个2kb的缓冲区,如果写满了handler仍然没有返回,会自动切换为chunked编码然后开始写响应头和body,这之后再更改响应头就无效了,因为body已经开始返回给客户端了,如果没有超过2kb,技术上是可以再更改响应头的,但是为了提供一个统一的接口,net/http禁止了这样的行为。
总结
- fasthttp通过对象池复用来减少内存的分配,减少垃圾的数量,进而减少gc的压力,通过协程池来减少协程创建和销毁的开销,据介绍通过这两个池可以做到零gc。
- fasthttp要等到body读取完或写入完了才会开始处理请求和响应,是非流式的。net/http把body封装成Reader和Writer供用户使用,是流式的,可以更灵活地实现定制功能。