背景
组内有好几个线上服务,除了业务逻辑不一样,请求处理过程基本上都是一致的。这些服务的执行逻辑都非常简单,但是有几个问题:
- 单机QPS很低,需要很多台机器
- 99分位耗时比理论上的长很多
- 在服务的负载达到上限时,服务器的负载却非常低
上面的这些问题在财大气粗的公司面前都不是问题啊,性能不够加机器!性能不够加机器! 上游再一次过来反映超时问题时,为了壮年程序员的尊严,这次刚好有时间决定不再单纯的加机器,要把这个程序优化一下,用一次屠龙技。
技术栈:
语言:java
通信方式:thrift,server模式为THsHaServer
redis客户端:jedis
模型:xgboost
执行环境:docker,8核 8G
缓存集群:通信协议为redis,但实现方式和redis不一样
优化过程
想要提升程序的性能,首先需要找到程序的瓶颈所在,然后有针对的去进行优化。
流程分析
优化前流程图
这个流程图省略了业务处理逻辑,只展示了各个线程之间的交互关系。构造redis请求的那部分逻辑,其实使用了两个forkjoin线程池,但它们两个的逻辑非常类似,为了简化把它们合并到一个流程中了。流程图中有两个深红色的步骤,很明显的看出两个明显能影响性能的地方。
结合服务的执行环境,可以总结出以下问题:
线程数过多
由于操作系统对线程的抢占式调度,线程频繁的上下文切换会带来几个问题:
- 系统指令执行时间增长,对应的指标值为
cpu.sys
,造成cpu执行时间的浪费 - 单位时间内分配给执行用户态指令的时间减少,对应的指标为
cpu.user
- 综上可看出,其不仅使机器负载升高,也会使执行单次任务耗时增长
两个阻塞
流程图中标红的两个步骤在执行时都会阻塞线程。其中thrift-server是在阻塞等待所有的redis结果,forkjoin中是在阻塞等待redis返回结果,网络通信使用的是同步io模式。
线程从运行状态切换为阻塞状态时,会发生一次线程上下文切换并且线程需要等待被重新调度。这是在操作系统层面的影响。
假设服务同时接收到40个请求,从流程图中可以看出,此时服务最多同时能发送86个redis请求,而要想让服务能通时执行40个任务,则必须要同时发送2000 = 40 * 20个redis请求。如果将f