RPC线程池与CPU和内存的相互关系

一、RPC服务端线程池的必要性

出于性能考虑, RPC框架服务端是一定要设计线程池的. 否则, 来一个请求, 再去创建/启动线程, 就相当低效了.

二、线程池队列中有等待任务的影响

我们的RPC框架中使用的是java线程池, 初始化时, 其核心线程数和最大线程数设置为相同, 都是取threadPoolsize的值.一旦同一时间的请求数高于设置的线程数(即threadPoolsize), 则请求会进等待队列. 此时服务端开始满负荷运行(线程池中所有线程均处理执行状态, 在执行完已有任务前, 不会再产生新的线程来执行请求).

那线程池等待队列有任务对服务有什么影响呢?

最直接的影响, 是导致请求延迟. 假设服务平均响应时间100ms, 服务线程池大小100. 假设当前并发为200, 则会有100个并发请求会进队列, 需要等待线程池中先处理完100个请求, 才能轮到队列中的请求执行. 此时响应时间直接变为200ms, 服务响应时间变长.

三、线程池大小设置对CPU/内存的影响

既然线程池队列有任务, 会导致服务响应时间变长, 那是否可以把线程池大小设置的足够大, 保证请求不会进等待队列, 进而产生延时?

答案是: 不可以.

jvm在创建线程时,要为线程分配栈内存, 当前默认是1024KB(64位linux机器, 且不设置-Xss), 也就是说在服务端满负荷运行的情况下, 服务端threadPoolsize从100提升到500, 将增加400MB内存的消耗. 一旦我们的代码执行过程中还涉及到新建线程, 那内存的消耗将进一步增加. 此外, 我们的服务还涉及到运算, 也要消耗CPU资源. 更不必说超过CPU核数的并发, 只是CPU分配时间片实现的并发, 本质上已经不是并发了. 由此可知: 线程池大小的调整, 要综合考虑机器实例的负载. 需要在线程池大小与内存/CPU之间寻找相对平衡的点, 实现在充分保证服务质量的情况下, 最大限度利用好资源.

四、我们服务的特点

相对而言, 我们的服务是计算量比较少的, 很少涉及到一定规模数据量的排序/查询/运算操作. 这一点从部署平台上当前各个服务的cpu/内存负载也能看出来.

因而在调整线程池大小时, 我们首先考虑的是对内存的影响

我们的运营同学会有一些push运营, 因而服务会存在一些流量突增的情况, 尤其是在发全量push的时候. 因而我们的服务正常情况下, 队列中不应该经常出现大量等待任务. 如果经常出现, 需要考虑扩容.

五、具体的调整操作

当前我们的RPC线程池队列打点已经有一段时间, 可以看到服务在生产运行下线程池队列情况. 结合我们观察到的现象, 我们可以对服务中的线程池队列做一些调整. 初步方案如下:

情况1: 服务内存使用率较低但线程池队列经常有任务

这种情况首先考虑调大threadPoolsize, 调整量可以根据当前内存使用率大小而定. 如果当前内存使用率仅20%, 可以考虑适当多加; 如果当前内存使用率到了40%, 则需要少加. 单次最高加100, threadPoolsize最大不要超过600.

情况2: 服务内存使用率较低且线程池队列没有任务

这种情况说明当前服务负载较低, 优先考虑缩容. 实例最少2, 不要少于2.

情况3: 服务内存使用率较高且线程池队列经常有任务

这种情况分两种:

  • 对于在突增流量下会受影响的服务, 如果队列里任务量较大(这个阈值可以定成100), 要考虑扩容
  • 对于在突增流量下不怎么受影响的服务, 容忍度要放宽(这就是利用率比较理想的情况)

情况4: 出现服务端调用超时报错的情况

这种情况下, 确认服务不是因为调用中间件超时报错(包括hbase/redis/mysql/其他服务), 直接扩容