goroutine实现并发worker

之前在写异步发送数据的时候聊过一些buffer的东西。因为‘内部分享’的原因,所以需要细化buffer在发送数据过程中的角色和作用。下面分几个阶段聊这事:

干净接口的Requests/s是15w+,假设引入发送数据逻辑的性能上限

  • 直接push数据,使用redigo/redis这个client库,压测时,cpu占用率高,且性能(9k+)表现十分不理想。pprof看了下,发现cpu大量时间消费在redis库中,用于与redis建立新的tcp连接。调整client库维护的连接pool后(50->500),性能上来很多3w+。再次调大连接池后,性能没有明显提升,大量的cpu时间也是耗费在client库中,用于向redis发送数据。这里有个注意事项,app和redis之间保持的tcp连接不能随意增大,一般redis是共享的,redis内部应该会有针对各host连接的限制。如果个别app保持过多连接,会影响其他app的性能。
  • 接下来如果和redis之间的连接就是50,那么该如何提升接口性能。根据之前pprof的分析,应该减少新创建redis连接的概率,使其不再过度占用cpu,这里可以设想一下:client库+redis是一个饭店的厨房,当用户请求直接落在厨房的窗口时,厨房的已有配置瞬间被占满,这时厨房的策略是新增配置来处理每个客户的请求,每次新增配置会要求所有窗口停工,随着等待用户越来越多,这个停工时间会越来越长,导致已有用户的服务质量也会下降。这里常规的优化方案是添加控制层用于流量控制,尽量使食堂处于高效率运转,也就是单位之间内可以满足更多用户,且把每个用户服务好,不能总是因为新增窗口就让已有用户得不到满足。所以在app启动时,初始化10个worker goroutine,每当请求来时,就查看是否有idle的worker,有就执行redis操作,没有则阻塞。

第二个方案不难想到,但是对于为什么它在维持同样redis连接的情况下使性能提升到维护大量redis连接的水平,我纠结了好久。在压测后,pprof,发现cpu消耗基本和500redis连接的情况一致。

之前纠结的点是,redis50个连接所能达到的服务质量是一定的,worker就20个,当高并发时,chan也会迅速被打满(redis的处理速度如果赶不上请求进来的速度速度),这样用户请求就会阻塞在写chan的地方,上述的思想过于混乱。应该把整个流程的各环节拆分:

  • redis(50连接)服务质量是一定的,只要你替它限制客流量,不要让大量的用户突然跑到它那里,否则性能会极剧下降(因为cpu浪费在新建连接上)
  • worker(20个),肯定会在高并发下有阻塞的情况,但是worker的起到了buffer的作用,当用户来的时候会有一定概率直接有idle的worker等待,可以迅速得到反馈。

通过上面的分析,2个环节的性能均得到优化,整个流程所能服务的用户请求数就会比第一种方法多,这也是event驱动框架和nginx处理用户请求的核心思想(可能)。

进一步分析第二种方案,理论上提升redis连接数量(以redis操作的网络I/O速度为上限)或者worker数量就会增加性能,当然worker数量大于redis连接数量(即允许并发访问redis的数量大于与redis idle连接数量,这可能是没有意义的,因为会导致新建连接)。还有就是如果worker中包含有业务数据,无限制的增大也不行,那么这里可能引入一个chan的消息队列有可能是一个比较好的方案。

下面逐步验证上面的推论是否正确。

  • 当引入worker并发操作redis时,发现redis库的idle连接50->500性能并没有明显提升,pprof显示大量的cpu花在了写网络io上。这是redis库内部瓶颈,也是大多数涉及到io的系统需要面对的,例如:web+db的组合
  • 同样道理,增加worker 20->200,性能并没有明显提升,查看pprof,也是因为redis库本身瓶颈导致。

既然上述的推论同样有限制条件,那么怎么进一步优化那?大的优化不可能了,除非将重io的操作用消息队列(kafka)和主业务解耦。

  • 后续也考虑过增加chan buffer来缓存消息,但这么做在高并发下完全是形同虚设,因为io的瓶颈导致消耗消息的速度是一定的,buffer很快会被打满,不过内存不是问题buffer无限大,相当于把问题简化为每次req都多写一次内存而已,只是cpu会被worker占用部分用于写io。
  • 完全异步,即无节制的使用goroutine也会导致内存疯长,负载过高影响整体性能。
goroutine实现并发worker
Share this