线程池调优

调优指标

线程池的大小依赖于所执行任务的特性以及程序运行的环境,线程池的大小应该应采取可配置的方式(写入配置文件)或者根据可用的 CPU 数量 Runtime.availableProcessors() 来进行设置,其中 Ncpu 表示可用 CPU 数量,Nthreads 表示线程池工作线程数量,Ucpu 表示 CPU 的利用率 0≤ Ucpu ≤1;W 表示资源等待时间,C 表示任务计算时间;Rtotal 表示有限资源的总量,Rper 表示每个任务需要的资源数量。

  • 对于对于纯 CPU 计算的任务-即不依赖阻塞资源(外部接口调用)以及有限资源(线程池)的 CPU 密集型(compute-intensive)任务线程池的大小可以设置为:Nthreads = Ncpu+1

  • 如果执行的任务除了 cpu 计算还包括一些外部接口调用或其他会阻塞的计算,那么线程池的大小可以设置为 Nthreads = Ncpu - Ucpu -(1 + W / C)。可以看出对于 IO 等待时间长于任务计算时间的情况,W/C 大于 1,假设 cpu 利用率是 100%,那么 W/C 结果越大,需要的工作线程也越多,因为如果没有足够的线程则会造成任务队列迅速膨胀。

  • 如果任务依赖于一些有限的资源比如内存,文件句柄,数据库连接等等,那么线程池最大可以设置为 Nthreads ≤ Rtotal/Rper

单线程

单线程情况下,服务接收到请求后开始初始化,资源准备,计算,返回结果,时间主要花在 CPU 计算和 CPU 外的 IO 等待时间,多个请求来也只能排队一个一个来,那么 RT 计算如下

RT = T(cpu) + T(io)
QPS = 1000ms / RT

多线程

单线程情况很好计算,多线程情况就复杂了,我们目标是计算出最佳并发量,也就是线程数 N:

  • 单核情况:N = [T(cpu) + T(io)] / T(cpu)
  • M 核情况:N = [T(cpu) + T(io)] / T(cpu) * M

由于多核情况 CPU 未必能全部使用,存在一个资源利用百分比 P,那么并发的最佳线程数 N = [T(cpu) + T(io)] / T(cpu) * M * P

吞吐量

我们知道单线程的 QPS 很容易算出来,那么多线程的 QPS:

QPS = 1000ms / RT * N = 1000ms / [T(cpu) + T(io)] - [T(cpu) + T(io)] / T(cpu) * M * P= 1000ms / T(cpu) * M * P

在机器核数固定情况下,也即是并发模式下最大的吞吐量跟服务的 CPU 处理时间和 CPU 利用率有关。CPU 利用率不高,就是通常我们听到最多的抱怨,压测时候 qps 都打满了,但是 cpu 的 load 就是上不去。并发模型中多半个共享资源有关,而共享资源又跟锁息息相关,那么大部分时候我们想对节点服务做性能调优时就是对锁的优化,这个下一节会提到。

前面我们是假设机器核数固定的情况下做优化的,那假如我们把缓存,IO,锁都优化了,剩下的还有啥空间去突破呢?回想一下我们谈基础理论的时候提到的 Amdahl 定律,公式之前已经给出,该定律想表达的结论是随着核数或者处理器个数的增加,可以增加优化加速比,但是会达到上限,而且增加趋势愈发不明显。