跳到主要内容

最佳实践:深入理解线程池参数设置

· 阅读需 14 分钟
Kindling-OriginX
故障根因推理引擎

最佳实践:深入理解线程池参数设置

在现代编程中,线程池已经成为了不可或缺的一部分,特别是在Java编程开发中,线程池更是绕不开技术点。然而,要想取得优秀的性能表现,需要对线程池的参数进行调优。本文将深入讲解 Java 线程池的调优方法和技巧,帮你提高编程技能和优化系统性能,并介绍如何使用 Kindling-OriginX 来深入理解线程池参数设置。

最佳实践:深入理解线程池参数设置

什么是线程池

线程池是一种管理和重用线程资源的机制,是利用池化思想设置和管理多线程的工具。线程池维护一定数量的空闲线程,当有任务需要时,就从中选择一个空闲的线程用来执行任务,当使用完成后该线程就会被重新放回线程池中,通过这样循环使用的方式来节省创建线程和销毁线程的各项资源开销。

线程池重要参数解析

线程池中有多个关键参数,需要在创建线程池时对其进行设置,合理的参数设置能够达到最佳的性能,适应任务场景。这里以ThreadPoolExecutor为例,对几个重要的参数进行解析说明。

corePoolSize

核心线程池中线程的数量。当提交一个新任务时,如果当前线程池中的线程数量少于corePoolSize,就会创建新的线程。即使此时有空闲的非核心线程可使用,也会创建线程,直到达到corePoolSize配置数量。

maximumPoolSize

线程池中最大的线程数量。包括核心线程池和非核心线程池,即在任务队列已满的情况下,可以创建的最大线程数。当线程数量超过maximumPoolSize时会执行配置的拒绝策略。

keepAliveTime

线程存活时间。当线程池中的线程数量大于corePoolSize时,超出的空闲线程最大能存活的时间,超过这个时间,线程就会被回收,直到线程数等于corePoolSize。

unit

时间单位

workQueue

任务队列实现。用于存储已提交未被执行的任务。线程池根据任务队列的策略来进行等待任务的调度。常见的队列有:

  • ArrayBlockingQueue:数组实现的有限队列,可以指定队列长度。

  • LinkedBlockingQueue:基于链表的无限队列,长度可以无限扩展。

  • PriorityBlockingQueue:优先级队列,可以设定队列里任务的优先级。

参数设置原理

最佳实践:深入理解线程池参数设置

为了最大程度利用线程池的资源,充分发挥线程池的执行效率,需要对线程池的主要参数进行合理的设置,对于不同的业务和场景,也需要根据实际情况来进行调整。

  • 核心线程池大小corePoolSize和最大线程池大小maximumPoolSize一般需要根据实际场景设置,主要与执行任务的类型和数量相关。一般最佳实践建议是将核心线程池设置为CPU核心数 + 1,最大线程池大小设置为CPU核心数 x 2。

  • KeepAliveTime线程存活时间,一般根据任务处理的耗时配置。如果任务密集且耗时长,则可以适当增加空闲线程的存活时间,根本目的是尽可能减少线程的创建和销毁操作,原则上不超过60s。

  • workQueue阻塞队列的类型及大小需要根据具体场景来设置。通常来讲任务数量多或并发高,选择无界队列,避免任务被拒绝。任务数量可控选择有界队列。


虽然参数设置原理看似简单,但实际使用中仍存在一些问题:

  • 人员经验和能力不同,经常以个人习惯或理解进行设置,没有标准或者数据依据。

  • 执行情况和任务类型、并发情况、机器配置都有关系,导致同样参数也可能运行起来情况有差异。

  • 同一个应用中可能存在多个不同业务类型的线程池。

常见线程池参数配置方案及其问题

上面参数设置大多基于经验,是否有科学的方式能够根据场景对其进行计算或者评估?

常见理论方案

这里以美团技术团队调研的业界一些线程池参数配置方案为例:

最佳实践:深入理解线程池参数设置

  • 第一种方案过于理论化,偏离任务场景。

  • 第二种方案也不符合实际情况,应用中往往不可能只存在一个线程池。

  • 第三种方案过于理想,正常情况下流量存在高峰低谷,同时大促、秒杀等运营活动期间流量更不可能是均衡的。

其他方案

在《linux多线程服务器端编程》中有一个思路,CPU计算和IO的阻抗匹配原则,根据这个原则可以推出估算公式:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

这也是网络上流传的比较多的方法之一,包括其衍生出的案例:

假如一个程序平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么最佳的线程数应该是?

根据上面这个公式估算得到最佳的线程数:((0.5+1.5)/0.5)*8=32。

这个方法看似严谨,但也存在很大问题,因为其结论可以简单等价为线程等待时间所占比例越高,需要越多线程,忽略了线程切换开销和锁,同时也忽略应用CPU密集型、IO密集型、内存型区别,以及硬件环境不同带来的差异性。

上面的这些方案看似合理,但是在实际场景下却未必合理,实际情况下都需要结合系统实际情况和硬件环境,通过合适的工具尝试达到一个符合实际场景需求的合理估算值。

使用 Kindling-OriginX 进行参数调优

最佳实践:深入理解线程池参数设置

这里以 Kindling-OriginX 为例,说明如何使用其提供的北极星指标体系进行线程参数配置的优化。

北极星指标

cpu

程序代码执行所消耗的CPU cycles

runq

线程的状态是Ready,如果CPU资源是充分,线程应该被调度到CPU上执行,但是由于各种原因,线程并未调度到CPU执行,从而产生的等待时间。

net

网络时间,主要包括DNS,TCP建连,常规网络调用

futex

通常指的是一个线程在尝试获取一个futex锁时因为锁已经被其他线程占用而进入等待状态的时间。在这段时间内,线程不会执行任何操作,它会被内核挂起。

file

存储操作时间

通过上述指标的具体时间,我们就可以知道每一次调用程序具体耗时在哪些地方,该从哪些方向进行优化,cpu资源是否被充分使用,还是时间都被消耗在了线程切换上等等。

调优案例解析

下面以使用 Kindling-OriginX 为例,说明如何对线程池进行参数设置与优化,并找出系统链路中的真实性能瓶颈。对于单一线程池可以通过 Kindling-OriginX 确定其是cpu密集型还是说IO密集型任务,对于多线程池可以通过 Kindling-OriginX 以数据为基础,对多个线程池综合调优,使应用达到最佳状态。

案例一

最佳实践:深入理解线程池参数设置

从北极星指标中可以看到,该次调用futex时间很长,可能是存在Full GC导致,也可能是程序中产生了锁等待,锁的竞争非常激烈,此时增大线程池也并不可能提高性能,可以考虑从优化任务执行代码入手。如果该服务是上游服务,则可以考虑加大下游服务的线程池尝试增强处理能力。

案例二

最佳实践:深入理解线程池参数设置

runq是一个表示cpu等待的概念,它是一个系统活动的队列,用于存储正在等待cpu资源的进程,本例中runq数值很高,说明cpu资源紧张,没有资源分配给线程使用,可以认为该线程池处理的任务为cpu密集型任务,一方面配置参考Ncpu + 1的方式,尽可能提高利用率,减少上下文切换,同时考虑减少目前配置大小,合理配置线程池队列长度,设置合理的拒绝策略,避免导致上游方法或服务产生大量锁等等。另一方面需要考虑扩充资源或查看机器监控等指标,分析是否出现了异常的资源抢占。

案例三

最佳实践:深入理解线程池参数设置

在北极星指标中,file一般指代存储相关操作。该例中,主要操作耗时是磁盘存储操作,在不考虑存储设备异常的前提下,该线程池可被认为是一个负责处理IO密集型任务的线程池,这种情况下可以考虑对该线程池采用Ncpu * 2的方式进行配置,并酌情增大。

对于单个或多个线程池的参数调优,亦可以Trace的角度出发,通过链路分析的方式,对单一节点的调用耗时进行分析来判断该服务中线程池的优化方向,单个线程池可以根据任务类型参考业内最佳实践,多个线程池可以根据北极星指标分别针对性的调整后综合分析,以求达到多个线程池的最佳资源利用状态。

小结

对于业务中的线程池问题,需要对线程池的工作原理及各参数含义有深入理解,同时也需要能合理根据实际场景选用合理的工具对其参数进行调优,不能一味生搬硬套业内经验。可以通过 Kindling-OriginX 等工具对程序执行的各项指标进行分析,以数据为导向,合理调配,才能真正提高线程的复用和效率,适用不同的业务场景,提供系统性能,结合实际情况和真实数据才是最佳实践。