Golang的池化设计

Why Pool?

先埋坑: Go 有没有池化的必要? 为什么要池化?

Go 自从出生以来都被我们冠以"高并发"的Tag, 回头细想和深究一下,你会发现其实都是由 goroutine 实现的 , 我们都知道 与 Thread 相比, 创建 Goroutine 的代价非常小。从程序表现上,也像极了Thread, 每个 Program 至少包含一个 Goroutine(至少一个**Main Goroutine**) 所有的其他 Goroutines 都依附于 Main Goroutine(如果 Main Goroutine Terminated, 其他 Goroutines 也会 Terminated), Goroutine 总是工作在后台。

多线程``多进程是为了提高系统的并发能力,当前系统下,这边就需要系统调度,一个线程可以拆分为 "用户态"线程"内核态"线程,这两者需要进行绑定,我们一般称 内核态线程线程用户态线程协程

线程-CPU-视角

从上面可以看出 协程线程 有映射关系,这样就来了 M:N 关系(为什么不是 1:1, N:1 ? )

M-N调度

Goroutine 优点:

  • 相比 Thread 代价更小
  • Goroutine 其在 stack 的大小可以根据程序要求进行变化,但是 Thread 是固定的
  • Goroutine 使用 Channel 进行通信,Channel 用于 Goroutines 访问共享内存的时候防止竞争的。
  • 从程序上一个线程可能拥有许多的 Goroutines 关联,如果这些关联的 有一个 Goroutine 去阻塞了线程,那么剩余的 Goroutine 将分配给其他的 OS Thread,且这种切换操作是对开发者屏蔽的。

Goroutine 调度器

回头看将 Goroutine协程 有区别的。调度

好了,那么 Goroutine是如何去调度的, 这个需要了解 Golang协程G-P-M 调度模型? 官方: https://docs.google.com/document/d/1ETuA2IOmnaQ4j81AtTGT40Y4_Jr6_IDASEKg0t0dBR8/edit#

Goroutine的调度模型示意图:

  • 废弃的Goroutine调度器
    • 老的Goroutine调度器
  • Goroutine调度器
    • 新的Goroutine调度器

从上图中可以看出

Goroutine调度器可以运行的goroutine分配到工作线程, M: 代表 Thread P: 代表 Processor G: 代表 Goroutine

  • 废弃的调度方式
    • M 要执行、返还G都必须访问全局的G队列(这个队列是加锁的,这就导致了访问全局G队列是通过互斥锁保证的。)
    • 创建、销毁、调度G都需要每个M获取锁,形成了竞争
    • 系统调度(CPUM之间的切换),导致频繁的线程阻塞、取消阻塞操作,增加开销
  • 新的调度方式
    • Global Queue存放等待运行的 G
    • P Local Queue同上,存放等待运行的G,不能超过256个, 在新建G的时候优先加入到P Local Queue,如果Queue已满,就移动Local Queue中的一半GGlobal Queue
    • P, 所有的P都在程序启动的时候创建,并保存在数组中,最多GOMAXPROCS
    • M, 线程想运行任务必须获取到P,P才是GM的离合器,从P Local Queue获取GP Local Queue为空时候,M会尝试从Global Queue获取一批G放到P Local Queue,或者从其他的P Local Queue窃取一半的G放到自己的P Local Queue
    • M执行运行GG执行后,MP获取下一个G,不断的重复执行。

P.S.:

关于PM的关系

  • P
  • P的数量($GOMAXPROCSruntimeGOMAXPROCS())决定,在程序执行的任意时刻都只有$GOMAXPROCS在同时执行
  • 在确定P的最大数量n后,运行时系统会根据这个数量创建nP
  • M
  • M的数量go语言限制, 设置M的最大数量,当然这个默认值基本可以忽略(10000, 内核很难支持这么多)
  • runtime/debug中的SetMaxThreads函数
  • 一个M阻塞了,会创建新的M
  • 没有足够的M来关联P并运行其中可运行的G(M都阻塞了、P还有很多就绪等待的,就会寻找空闲的M,如果没有空闲的M,就会选择创建新的M)

综上看 Goroutine 的调度器设计的考虑:

  • 复用线程:避免频繁的创建、销毁线程。对线程复用
    • work stealing 机制: 当P Local Queue无可运行的G时候,尝试从其他线程绑定的的PP Local Queue获取G
    • hand off 机制: 当线程因为G进行系统调用阻塞时候,线程释放绑定的P,把P转移给其他空闲的线程执行
  • 并行GOMAXPROCS设置P的数量,最多有$GOMAXPROCS个线程分布在多个CPU进行同时运行,GOMAXPROCS也限制了并发,假如GOMAXPROCS<CPU核数,则最多用GOMAXPROCSCPU核进行并行
  • Global Queue,弱化了Global Queue的设计,当M执行work stealing从其他的P偷不到G时候,它可以从Global Queue获取G

Go func 的调度流程

直接展示流程图

gofunc调度流程

  • go func 开启一个 goroutine
  • 两种类型的存储Gqueue, 一种P Local Queue,一种Global Queue。新创建的G会优先保存在PLocal queue,如果P Local Queue满了就会保存到Global Queue
  • G运行的载体是M(即线程), 且一个M必须持有一个PM:P = 1:1M会从P Local Queue获取一个可执行状态的G来执行,如果P Local Queue为空,会从Global Queue获取一批可执行的G或者从其他MP组合获取一半的可执行的G来存储到其所关联的Local Queue来执行。
  • M循环的从P中获取G来执行的
  • M执行某个G时候如果发生syscall或者其他block操作,M阻塞了,如果当前还有一下其他的G在执行,runtime会把这个线程MPdetach,创建一个新的M(如果空闲从空闲中获取)来服务这个P
  • M 调用结束,G 尝试获取一个空闲的 P 执行,并放入到 这个 P Local Queue,如果获取不到 P, 那么线程M进入 休眠状态,加入到空闲线程中,这个 G 会被存放到 Global Queue 中。

总结论调

上面了解 Golang 实现协程机制 与 goroutine 的调度机制,那么 Golang 开发需要做池化么?

要论述次问题,需要做的是结合真实应用场景。

  • 假设是一个小型的项目,且机器的配置完全撑的住预知的并发量,对 goroutine 做池化就没必要了。当然不排除后续需要的进行的优化。
  • 但是针对一个现有的互联网、物联网等商业环境上,绝大部分生产系统在个人认为还是都是需要做池化动作的。

需求场景描述: 当一个百万主播开启直播,在该网红要直播的时候,要对其大量的订阅粉丝进行推送直播开启通知的。

Memory上: 直接依赖goroutine的设计,将粉丝都处理出来,依赖非同步的批量处理来并行处理(开启每个goroutine进行200个的订阅粉丝通知操作)。 假设一个机器使用2GBMemory,每个goroutine2KB,则理论上能开启100w个初始化状态的goroutine, 但实际上每个goroutine的大小跟随开启的数目而增长上去。 在考虑到系统预留等,和其他情况取1/10的作为稳定状态,同样的配置在一台机器上只能跑10wgoroutine, 那么在之前的需求,一个百万粉丝的主播开播,这样光完成一个主播就需要在一瞬间启动1wgoroutine,那么在高峰热门时段,一台机器最多也只支撑了10个主播开播。且高频率开启销毁 goroutine 造成CPU``100%

总结上述操作问题:

  • 需要一个限制goroutine最多数量
  • 需要缓存临时存储,让有限的goroutine能缓冲的消化掉
  • goroutine能重用,减少产生的代价

综合看完,这就是一个池化的设计实现方式。

  • 第一点管理goroutine, 使用一个 goroutine array 来管理
  • 缓存设计,设计queue, goroutine 的协作通信通过 channel, channel(unbuffered channel 、buffered channel) 使用这个作为 queue,预设置0(走unbuffered channel),其他的可以设置 queue的长度,走buffered channel机制,
  • goroutine能重用,开始创建的时候,预设置一个初始的大小,并且设置一个最大的 pool 最大限制。 在初始的时候只创建核心的,然后随着任务的增长和需求赠度,慢慢的增长数量直至达到pool的限制。策略上优先任务放入到queue,queue满了后再新建goroutine去执行并加入到pool, 这样的设计,在突发情况能应接。当系统空闲,能将过多的goroutine 进行回收回复到初始化大小。

当然也有其他的优秀的开源池化组件,当前

Ref:

comments powered by Disqus