工作协程池的设计

语言:Go

正所谓:“黑猫白猫,能抓住老鼠的猫就是好猫”。在业务中,能实现需要功能的设计就是好设计了。我们做一些方便扩展的工作自然是好的,但如果做的过于臃肿,开发成本、维护成本远远超出自己应投入的工作量的话,那就不应该做。

一个设计模型

下图是一个工作池的设计模型,我想就这个模型进行一番分析。

工作协程池设计模型.jpg

这个模型的工作场景

我们使用这个模型,在初始化的时候,开启了固定数量的 worker 一直在运行——获取任务并执行。业务中在生成了任务 job,之后会先放到工作池的全局任务队列 globalQueue 中,任务分发器 dipatcher 从任务队列中获取任务,并轮询分发到各个 worker 的 taskQueue 中。worker 一直从 taskQueue 中获取任务并执行。

对这个模型分析后的一些理解

我在网上搜索了一些线程池的设计理念,想知道一个完美的设计是什么样子的。搜索之后发现,自己要写文章来解释这种“完美”设计的功底还不够,不如就当前我的设计模型,对照搜索到的一些特性作一番分析。这样对自己会有更大的裨益。

用于异步场景提高工作效率

比如,一个协程 A 从接收到客户端的请求时创建出来,当执行到发现后面的工作可以用工作池去做时,封装好 job 然后发给工作池,这个时候这个协程 A 就可以结束或者执行其他工作,然后工作池 P 接管任务的执行。

这个过程中:

同步关系场景:
如果协程 A 没有其他任务并行执行且需要等待任务的 resp 的话,那似乎没什么优化,反倒麻烦了一些传递任务的工作。
因为这种情况下协程 A 本身来做的话就是顺序执行的过程,交给工作池做这个任务的话,A 本身也需要阻塞等待,所以反倒增加了任务传递给协程池的工作量。没有任何优化。

异步关系场景:
如果协程 A 交给工作池的任务是不需等待 resp 的任务或者做一些自己的工作后等待 resp 时,比如日志的打印工作不需要等待结果,那么协程 A 可以将任务分发后集中精力做自己的业务,即在这种场景下就提高了协程 A 的工作效率。

所以,总结,效率优化的一个场景:将工作池用于异步的工作任务。

由上,进一步思考,如果在业务中使用大量的工作池传递异步任务,比如多模块的设计模型(比如 protoactor),如果每个模块做成一个协程池,A 模块将消息传递出去后自己就不再等待,那么在工作任务的传递过程中就优化了协程的创建销毁工作,这将提高整个服务的执行效率。

实际中,我们业务中使用的 protoactor 模型没用工作池先不说,模块在发送消息之后,会有一个 channel 做阻塞等待。由上分析这其实与同步操作没什么差别。优化这个问题似乎可以用回调方法的方式来做优化。
就是说,模块 A 发送消息到模块 B 后,携带一个回调方法做后续处理,这时模块 A 的这个协程就可以释放占用了。

worker 弹性伸缩平衡工作效率和内存使用

上面的模型的一个缺陷是:固定的 worker 数量

worker 数量固定时,如果任务过多,worker 执行不过来,那会造成 job 的积压,工作效率低下;如果 worker 数量过多,没多少 job,那大量 worker 空等待,造成一定的内存资源浪费(一个 go 协程占大约 2~4KB)。

那么如何将 worker 做成弹性伸缩的呢?

考虑过切片和 map 有动态伸缩的特性,但似乎不能独立替换 channel,若和 channel 结合使用的话也会有并发问题需要加锁进行控制。
而且 channel 内部实现是个循环队列。增加缓冲空间只不过是多了几个 worker 类型的空间大小,所以占用的空间也不会很大。

channel 是一个天然的循环队列。这真是太棒了!

想法一:使用对象池获取 worker

worker 也是个对象,所以可以尝试从对象池中获取。可以在 worker 上加一个属性区分 worker 的状态,是运行中还是尚未初始化。但是放回对象池的话,不能是运行状态的 worker,因为这样的话 worker 对象不会被回收,起不到收缩的作用。

所以对象池生产 worker 对象只能用在生产初始化的 worker,生产出来之后还需要自己执行 start()。

想法二:动态维护 worker 数量

思路:

  • 伸展控制:给 worker 附加一个计数的值,向 taskQueue 中添加任务加一,完成任务减一,判断这个值(如大于等于 1 时)进行伸展控制;
  • 收缩控制:给 worker 附加一个时间参数,记录 worker 空闲的时间,根据时间进行收缩控制;(注1)
  • 收缩控制:给在协程池上增加一个计数,记录连续获取到空闲 worker 的次数,可以按某种策略对这个次数进行验证,进而控制 worker 的收缩。

使用上述方案进行收缩控制时,还需要控制计数不要重复循环,所以要记录计数第一个的 workerID,到重新遇到这个 workerID 是一个周期循环。这中间有连续的空闲 worker 最大个数超过总 worker 个数的某个百分比后,进行一定量的收缩。

上面注1方案不适用于上图中定义的协程池,因为上图中的 worker 是轮询接收任务的,要想对时长进行判断,那么不能用这种方式。就是使得空闲的 worker 一直空闲下来,这样才好收集到那些一直用不到的 worker:这样的话工作池的 dispatcher 用 channel 来控制就不好了,可以改用排序的堆进行控制比较好。

总结

  • 工作池优化了协程创建和销毁工作的性能损耗;可将异步且无响应的任务放到工作池中来提高工作效率。这些任务如:
    • IO 操作:日志打印、网络请求
    • 数据库操作
  • 动态维护 worker 数量的伸缩,达到工作效率与内存使用之间的平衡

2021-10-10 做了某公司的笔试题,写了一个简单的协程池,参考: RobinTsai/tearupGo/workpool 对于它的思考见 issue/8

你可能感兴趣的:(工作协程池的设计)