golang的协程模型虽然支持海量的并发,但是并发到了一定量级后,调度的性能会变得很差, 当然,大部分情况下并不会有这样的问题。
那为什么还需要一个pool呢?
原因可能有以下几点:
- 程序员的追求,君不见别人的轮子圆又圆;
- 当极端情况下,例如cpu满载,服务处理不过来的时候,goroutine会大量创建,从而加剧 问题,当然这个也可以设置gouroutine的数量上限来缓解;
- 复用goroutine,减小GC压力;
- 封装并发代码,使用起来更为简单。
在调研的过程中,发现了tidb在go升级 到1.11之后,删除了自己实现的pool,于是我很好奇难道pool真的已经是不必要的了么?所以又 找了几个比较容易被用来做参考的pool实现:
经过一番测试,有一个基本的结论,在goroutine的规模不至于大到导致占用太多cpu资源的时候, 原生的go语句会执行的更快,但同时内存的使用也是不受控制的。性能的损耗导致的运行时间会 增长一些,但是不至于不能忍受,所以结论是一个封装良好的Pool还是有必要的。
一个pool基本的功能是获取资源,释放回资源,管理资源的生命周期。进阶一点的功能可能有管理 池的大小,预创建资源,空闲资源自动释放,允许暂时的资源溢出。
回归到我自己的需求,我需要启动一个进程,从消息队列中获取数据,然后分发到池内(由于每个 消息需要多个goroutine来完成,并且需要等待所有的结果返回后才能进行下一步,所以这里其实 是需要多个pool来一起工作的),由池内的goroutine完成消息的消费后返回给主进程,由主进程 ack消息,完成消息处理的流程。大概的示意图如下。
+----------+
| |
+-------->+ Pool |
+--------------+ | | |
| | | +----------+
| | | | |
| Master +----------->+ Pool |
| | | | |
| | | +----------+
+--------------+ | | |
+-------->+ Pool |
| |
+----------+
然而,其实并没有必要维护多个pool对象,因为这样会有冗余数据和计算(比如池对象,池的配置,池的 自动资源配置)。所以这里其实可以引入分组的概念,多个pool共用一个池对象,共用其所有配置, 但可以等待一个单独分组的所有goroutine都完成后再返回结果。
另外,由于这个池其实是一个goroutine的池,其实并不需要从池里真正地取出资源,而只需把任务 扔进池里,等待完成就ok了,不过设计的时候还是可以保留资源对象,这样更方便组织代码。
最后,聊一下怎么设计任务的传递,Golang里能够runable的原生对象就只有function了,当然, 使用reflect也可以实现任务的执行,不过总所周知的reflect性能会比较差,所以这里不考虑 使用。那么传入的其实是一个function以及它运行需要的上下文,也就是传说中的闭包。那么返回 的呢?考虑到需要有时候需要判断任务是否成功,有时候还需要任务的结果,那么可以考虑返回一个 Job对象,包含Success和Result两个字段,分别用于保存是否成功和任务结果。
人非图灵,难免有不会的地方,这里记录下做这个半路碰到的问题:
由于还没开始做,也没有高性能golang程序的经验,只能大概猜猜了:
1. channel 错误地使用或者本身存在的局限导致性能问题。
这里算是一个总结吧,经过测试,在我的mbp-2019上,大概要500万以上的goroutine的时候,调度
才会有明显的压力,我感觉我这辈子可能都用不到那么多的goroutine,当然我测试的goroutine很
轻量,只使用了time.Sleep()
,这个方法只会分配一次内存,也没多少cpu的操作。
而且,从原理上讲,一个pool不会运行的比golang的调度器更快,因为golang相当于只是往队列里
扔了一个任务,然后调度器去执行,所以go
关键字是非常快的,主要的问题还是在大规模goroutine
的调度问题,以及内存消耗的问题,但限制goroutine的规模简单的用一个channel就可以办到了。
没必要为了这个搞一个pool。可能搞个pool主要还是为了造轮子。
当然,也有可能还有我没有了解,或者不愿意了解的部分(对,就是morestack这个runtime的调用)。
- 较详细的性能分析,在什么情况下用池性能会好一些?内存会少吃一些?(控制变量,多少goroutine,处理什么类型的任务)