如何科学的设置线程池
线上高并发的服务就像默默的屹立在大江大河旁边的大堤一样,随时准备着应对洪水带来了冲击,线上高并发服务的线程池导致的问题也颇多,例如:线程池涨满、CPU 利用率高、服务线程挂死等,这些都是因为线程池的使用不当,或者没有做好保护、降级的工作而导致的。
当然,有些小伙伴是有保护线程池的想法的,但是,大家是不是有过这样的经验和印象,线程池的线程有时候设置多了性能低,设置少了还是性能低,到底应该怎么设置线程池呢?
在经历过这些年对小伙伴的设计评审,得知小伙伴们都是凭经验、凭直觉来设置线程池的线程数的,然后根据线上的情况调整数量多少,最后找到一个最合适的值,这是通过经验的,有时候管用,有时候不管用,有时候虽然管用但是牺牲了很大的代价才找到最佳的设置数量。
其实,线程池的设置是有据可依的,可以根据理论计算来设置的。
首先,我们看一下理想的情况,也就是所有要处理的任务都是计算任务,这时,线程数应该等于 CPU 核数,让每个 CPU 运行一个线程,不需要线程切换,效率是最高的,当然这是理想情况。
这种情况下,如果我们要达到某个数量的 QPS,我们使用如下的计算公式。
设置的线程数 = 目标 QPS/(1/任务实际处理时间)
举例说明,假设目标 QPS=100,任务实际处理时间 0.2s,100 * 0.2 = 20个线程,这里的20个线程必须对应物理的20个 CPU 核心,否则将不能达到预估的 QPS 指标。
但实际上我们的线上服务除了做内存计算,更多的是访问数据库、缓存和外部服务,大部分的时间都是在等待 IO 任务。
如果 IO 任务较多,我们使用阿姆达尔定律来计算。
设置的线程数 = CPU 核数 * (1 + io/computing)
举例说明,假设4核 CPU,每个任务中的 IO 任务占总任务的80%,4 * (1 + 4) = 20个线程,这里的20个线程对应的是4核心的 CPU。
线程中除了线程数的设置,线程队列大小的设置也很重要,这也是可以通过理论计算得出,规则为按照目标响应时间计算队列大小。
队列大小 = 线程数 * (目标相应时间/任务实际处理时间)
举例说明,假设目标相应时间为0.4s,计算阻塞队列的长度为20 * (0.4 / 0.2) = 40。
另外,在设置线程池数量的时候,我们有如下最佳实践。
-
线程池的使用要考虑线程最大数量和最小数最小数量。
-
对于单部的服务,线程的最大数量应该等于线程的最小数量,而混布的服务,适当的拉开最大最小数量的差距,能够整体调整 CPU 内核的利用率。
-
线程队列大小一定要设置有界队列,否则压力过大就会拖垮整个服务。
-
必要时才使用线程池,须进行设计性能评估和压测。
-
须考虑线程池的失败策略,失败后的补偿。
-
后台批处理服务须与线上面向用户的服务进行分离。