fasthttp 的 goroutine pool 实现探究
引言
fasthttp是一个非常优秀的web server框架,号称比官方的net/http快10倍以上。fasthttp用了很多黑魔法。俗话说,源码面前,了无秘密,我们今天通过源码来看一看她的goroutine pool的实现。
热身
fasthttp写server和原生的net/http写法上基本没有区别,这里就不举例子。直接找到入口函数,在根目录下的server.go文件中,我们从函数ListenAndServe()
跟踪进去。从端口监听到处理请求的函数调用链如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
func ListenAndServe(addr string, handler RequestHandler) error {
s := &Server{
Handler: handler,
}
return s.ListenAndServe(addr)
}
// ListenAndServe serves HTTP requests from the given TCP addr.
func (s *Server) ListenAndServe(addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
// Serve blocks until the given listener returns permanent error.
func (s *Server) Serve(ln net.Listener) error {
...
wp := &workerPool{
WorkerFunc: s.serveConn,
MaxWorkersCount: maxWorkersCount,
LogAllErrors: s.LogAllErrors,
Logger: s.logger(),
}
wp.Start() //启动worker pool
for {
if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil {
wp.Stop()
if err == io.EOF {
return nil
}
return err
}
if !wp.Serve(c) {
s.writeFastError(c, StatusServiceUnavailable,
"The connection cannot be served because Server.Concurrency limit exceeded")
c.Close()
if time.Since(lastOverflowErrorTime) > time.Minute {
s.logger().Printf("The incoming connection cannot be served, because %d concurrent connections are served. "+
"Try increasing Server.Concurrency", maxWorkersCount)
lastOverflowErrorTime = time.Now()
}
time.Sleep(100 * time.Millisecond)
}
c = nil
}
}
|
上面代码中workerPool就是一个线程池。相关代码在server.go文件的同级目录下的workerpool.go文件中。我们从上面代码涉及到的往下看。首先是workerPool struct
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type workerPool struct {
WorkerFunc func(c net.Conn) error
MaxWorkersCount int
LogAllErrors bool
MaxIdleWorkerDuration time.Duration
Logger Logger
lock sync.Mutex
workersCount int
mustStop bool
ready []*workerChan
stopCh chan struct{}
workerChanPool sync.Pool
}
type workerChan struct {
lastUseTime time.Time
ch chan net.Conn
}
|
workerPool sturct
中的WorkerFunc
是conn的处理函数,类似net/http
包中的ServeHTTP
。因为所有conn的处理都是一样的,所以WorkerFunc
不需要和传入的每个conn绑定,整个worker pool共用一个。workerChanPool
是sync.Pool对象池。
MaxIdleWorkerDuration是worker空闲的最长时间,超过就将worker关闭。workersCount是worker的数量。ready是可用的worker列表,也就是说所有goroutine worker是存放在一个数组里面的。这个数组模拟一个类似栈的FILO队列,也就是说我们每次使用的worker都从队列的尾部开始取。wp.Start()
启动worker pool。wp.Stop()
是出错处理。wp.Serve(c)
是对conn进行处理的函数。我们先看一下wp.Start()
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
func (wp *workerPool) Start() {
if wp.stopCh != nil {
panic("BUG: workerPool already started")
}
wp.stopCh = make(chan struct{})
stopCh := wp.stopCh
go func() {
var scratch []*workerChan
for {
wp.clean(&scratch)
select {
case <-stopCh:
return
default:
time.Sleep(wp.getMaxIdleWorkerDuration())
}
}
}()
}
func (wp *workerPool) Stop() {
...
close(wp.stopCh)
wp.stopCh = nil
wp.lock.Lock()
ready := wp.ready
for i, ch := range ready {
ch.ch <- nil
ready[i] = nil
}
wp.ready = ready[:0]
wp.mustStop = true
wp.lock.Unlock()
}
|
简单来说,wp.Start()
启动了一个goroutine,负责定期清理worker pool中过期worker(过期=未使用时间超过MaxIdleWorkerDuration)。清理操作都在wp.clean()
函数中完成,这里就不继续往下看了。stopCh
是一个标示worker pool停止的chan。上面的for-select-stop是很常用的方式。wp.Stop()
负责停止worker pool的处理工作,包括关闭stopCh,清理闲置的worker列表(这时候还有一部分worker在处理conn,待其处理完成通过判断wp.mustStop来停止)。这里需要注意的一点是做资源清理的时候,对于channel需要置nil。下面看看最重要的函数wp.Serve()
。
核心
下面是wp.Serve()
函数的调用链。wp.Serve()
负责处理来自client的每一条连接。其中比较关键的函数是wp.getCh()
,她从worker pool的可用空闲worker列表尾部取出一个可用的worker。这里有几个逻辑需要注意的是:1.如果没有可用的worker(比如处理第一个conn是,worker pool还是空的)则新建;2.如果worker达到上限,则直接不处理(这个地方感觉略粗糙啊!)。go func()
那几行代码就是新建worker,我们放到下面说。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
func (wp *workerPool) Serve(c net.Conn) bool {
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 {
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 {
return nil
}
vch := wp.workerChanPool.Get()
if vch == nil {
vch = &workerChan{
ch: make(chan net.Conn, workerChanCap),
}
}
ch = vch.(*workerChan)
go func() {
wp.workerFunc(ch)
wp.workerChanPool.Put(vch)
}()
}
return ch
}
|
workerFunc()
函数定义如下(去掉了很多不影响主线的逻辑),结合上一篇《如何裸写一个goroutine pool》,还是熟悉的配方,熟悉的味道。这里要看的wp.release()
是干啥的。因为前面的wp.Serve()
函数只处理一个conn,所以for循环执行一次我们就可以把worker放到空闲队列中去等待下一次conn过来,从代码中可以看出来放回果然是放到空闲队列的末尾(可算和上面呼应上了)。还有上面提到的mustStop
,如果worker pool停止了,mustStop
就为true,那么workerFunc
就要跳出循环,也就是goroutine结束了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
func (wp *workerPool) workerFunc(ch *workerChan) {
var c net.Conn
var err error
for c = range ch.ch {
if c == nil {
break
}
...
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()
if wp.mustStop {
wp.lock.Unlock()
return false
}
wp.ready = append(wp.ready, ch)
wp.lock.Unlock()
return true
}
|
总结
除了fasthttp,我还看了github上其他开源且star数在100以上的goroutine pool的实现,基本核心原理都在我上一篇文章中说的那些。fasthttp的实现多了一层goroutine回收机制,不得不说确实挺巧妙。fasthttp性能这么好一定是有其原因的,源码之后再慢慢读。
fasthttp 的 goroutine pool 实现探究的更多相关文章
- 如果裸写一个goroutine pool
引言 在上文中,我说到golang的原生http server处理client的connection的时候,每个connection起一个goroutine,这是一个相当粗暴的方法.为了感受更深一点, ...
- goroutine pool,WaitGroup,chan 示例
服务端高并发编程经常需要写很多goroutine来服务每一个连接,如何正确使用goroutine池是又拍云的工程师们需要考虑的问题,今天这篇文章,分享给同样需要使用go语言的小伙伴们. 文/陶克路 本 ...
- 通过 Channel 实现 Goroutine Pool
最近用到了 Go 从 Excel 导数据到服务器内部 用的是 http 请求 但是发现一个问题 从文件读取之后 新开 Goroutine 会无限制新增 导致全部卡在初始化请求 于是乎就卡死了 问题模拟 ...
- Goroutine并发调度模型深度解析之手撸一个协程池
golanggoroutine协程池Groutine Pool高并发 并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环 ...
- go中控制goroutine数量
控制goroutine数量 前言 控制goroutine的数量 通过channel+sync 使用semaphore 线程池 几个开源的线程池的设计 fasthttp中的协程池实现 Start Sto ...
- Golang(九)简单 Goroutine 池实现
0. 前言 最近使用 Golang 写一个并发执行的测试脚本 之前习惯使用 Java,习惯性想先建一个线程池.然后意识到 Golang 没有封装好的线程池 结合之前学习的 Goroutine 原理和 ...
- Awesome Go
A curated list of awesome Go frameworks, libraries and software. Inspired by awesome-python. Contrib ...
- golang协程池设计
Why Pool go自从出生就身带“高并发”的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在g ...
- Go 语言相关的优秀框架,库及软件列表
If you see a package or project here that is no longer maintained or is not a good fit, please submi ...
随机推荐
- 基于阻塞队列的生产者消费者C#并发设计
这是从上文的<<图文并茂的生产者消费者应用实例demo>>整理总结出来的,具体就不说了,直接给出代码,注释我已经加了,原来的code请看<<.Net中的并行编程-7 ...
- pyqt5 动画在QThread线程中无法运行问题
自己做了一个tcp工具,在学习动画的时候踩了坑,需求是根据上线变绿色,离线变灰色,如果连接断开了,则变为灰色 问题现象: 可以看到点击"连接","离线"的时候动 ...
- 学习ASP.NET Core Razor 编程系列八——并发处理
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- Next Permutation 下一个排列
Implement next permutation, which rearranges numbers into the lexicographically next greater permuta ...
- Pascal's Triangle(杨辉三角)
Given numRows, generate the first numRows of Pascal's triangle. For example, given numRows = 5,Retur ...
- Day9 基于TCP的套接字和基于UDP的套接字
服务端: ss=socket() #创建服务器套接字 ss.bind() #把地址绑定到套接字 ss.listen() #监听套接字, inf_loop: #服务器无限循环 cs=ss.accept( ...
- Django 2.0 Release note阅读简记
最前面就是大家都知道的这个版本开始只支持py3.4+,而且下一个大版本就不支持3.4,再就是建议所有插件开始放弃1.11 1.最惊艳的变化,就是URL配置正则表达式的简化,旧的: url(r'^art ...
- Storyboard的几点缺憾
Storyboard作为iOS主推的UI开发方式,不管接受也好,不接受也好,在未来几年,都会逐渐在产业界流行,之前bignerd在其ios开发第四版中,作者曾经说过一节的Storyboard优缺点分析 ...
- Eclipse+Resin开发环境迁移中发生的一些问题
换新机器了,系统也从XP升级到64位WIn7.某些旧工具直接无法用了.下面简单谈一下标题的内容 1.非泛型的容器类引入在JDK1.7以下编译好像已经不行了.比如Java.util.ArrayList这 ...
- QQ connect client request's parameters are invalid, invalid openid 问题的解决
很多人的这个问题是POST的时候发生,我的也恰好在POST的时候发生.后来我发现可能是因为QQ的这个后端是采用类PHP的语言开发,在动态语言的获取参数时POST参数和GET参数是可以分开读取的,也就是 ...