项目-线程池优化
April 3, 2025About 8 min
背景
一期优化背景:
- 系统运行一段时间后,service 线程数可以达到 8000+;
- 大部分线程池的核心线程是 50 - 100;
- 服务启动时,所有线程池已创建核心线程数都为 0,所以要等全部核心线程创建完,service 才可以稳定提供服务,创建核心线程过程,可能会导致上层请求超时;
二期优化背景:
之前在使用线程池时发现随着业务的迭代调整,有些业务的请求量下降,有些业务的请求量上升,还有一些旧的业务不再使用,导致现有现有部分线程池资源分配不合理,需要对线程池资源重新分配。
优化目标
- 较少业务调用超时;
- 让线程池当前的参数匹配当前的业务请求量;
问题分析思路/过程
一期优化方向:
- 服务启动时,自动创建线程池所有核心线程;
- 优化线程池的数据的日志输出打印;
- 让运维采集数据,将所有线程池,全部加入到监控中,后续将日志在监控 grafana 中展示;
注:for 循环查询,要适当增加线程池核心线程大小。瞬间请求量较大
二期优化方向:
- 根据线程池的统计日志分析各个线程池的各个指标,判断是否需要对参数进行调整;
- 针对业务:是否需要对同类型的业务按照优先级分为多个不同参数的线程池去执行;
方案设计
ThreadPoolExecutor 统计相关的 API
关于 ThreadPoolExecutor 线程池统计相关的 API:
- getPoolSize():获取线程池中工作的线程的个数;
- getActiveCount():获取线程池中活跃线程数;(近似值,执行任务加锁的原因)
- getLargestPoolSize():获取线程池的历史最大的线程个数;
- getCorePoolSize():获取核心线程数;
- getMaximumPoolSize():获取允许的最大的线程数;
- getQueue().size():获取线程池堆积的任务个数;
- getTaskCount():获取总任务数;(近似值,已执行的任务数 + 阻塞队列中的任务个数)
- getCompletedTaskCount():已执行任务数;(近似值,因为累加只会在线程退出时才累加)
对于任务堆积数:
- 对于线程池来说就是阻塞队列中实时长度;
- 实际使用中的意义:
- 如果任务堆积长度不为 0 , 说明线程池的核心线程全部在执行任务中,且任务已经出现堆积;
- 如果任务堆积长度不长有可能是瞬时请求量增加,如果长度过长说明需要考虑是否进行优化了;
- 优化思路:
- 时延要求高的线程和定时任务线程执行要求不一样,前者要求实时执行任务应减少堆积。定时任务线程实时行要求不高可以有一定堆积。如果是时延要求高的线程任务堆积不能太长, 如果是定时任务任务可以堆积;
- 如果全天只有一段时间长度长需要增加最大线程数量;
- 如果很长时间都有堆积核心线程数和最大线程数都需要调整;
对于完成任务数:
- 当前存活的所有线程执行过的任务数量;
- 已销毁线程执行过的任务数量;
对于总任务数:
- 所有线程执行过的任务数
- 已销毁线程执行过的任务数
- 执行执行的任务
- 队列长度
对于活跃线程数:正在执行任务的线程数
对于历史最大的线程个数:统计线程池阶段最大的线程数量;
- 如果线程池需要缩减核心线程数,历史最大活跃线程可以作为参考值;
具体统计的代码实现
所有线程池使用统一的入口注册到日志打印类 StatLog.class 中;
例如在具体使用线程的地方这样定义:
private static final ThreadPoolExecutor executor =
new ThreadPoolExecutorTraceId(3, 50, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.DiscardOldestPolicy());
static {
StatLog.registerExecutor("xxx_pool_name", executor);
}通过 registerExecutor 方法将线程池注册到 StatLog 类内部的 Map;
StatLog 类的核心打印逻辑,日志是每 10 秒打印一次
private String statExecutor(String name, ThreadPoolExecutor executor, long time2) {
// 队列任务堆积
int size = executor.getQueue().size();
// 完成任务数
long completedTaskCount = executor.getCompletedTaskCount();
// 总任务数
long taskCount = executor.getTaskCount();
// 活跃线程数
int activeCount = executor.getActiveCount();
// 历史最大线程数
int largestPoolSize = executor.getLargestPoolSize();
// 核心线程数
int corePoolSize = executor.getCorePoolSize();
// 记录每个线程池,最大活跃线程数
Integer integer = MAX_ACTIVE_COUNT.get(name);
if(Objects.isNull(integer)){
MAX_ACTIVE_COUNT.put(name, activeCount);
}else {
MAX_ACTIVE_COUNT.put(name,Math.max(integer.intValue(),activeCount));
}
StringBuilder strBuf = new StringBuilder(32);
strBuf.append(name).append("{").append(size).append(",")
.append(completedTaskCount).append(",")
.append(taskCount).append(",")
.append(activeCount).append(",")
.append(corePoolSize).append("}\n");
if (largestPoolSize > 0) {
StringBuilder errorBuffer = new StringBuilder();
errorBuffer.append(name).append("{").append("队列任务堆积:").append(size).append(";")
.append("完成任务数:").append(completedTaskCount).append(";")
.append("总任务数:").append(taskCount).append(";")
.append("活跃线程数:").append(activeCount).append(";")
.append("历史最大活跃线程数:").append(MAX_ACTIVE_COUNT.get(name)).append(";")
.append("历史最大线程数:").append(largestPoolSize).append(";")
.append("核心线程数:").append(corePoolSize).append("}\n");
log.error(errorBuffer.toString());
}
return strBuf.toString();
}优化案例:
线程池 1:
- 优化前参数:core : 20 max : 20 queue : 50 alive : 2m
- 优化后参数:core : 2 max : 10 queue : 500 alive : 1m
- 优化方式:减少核心线程数;
- 优化原因:没有任务,看代码大部分代码已经注释掉了, 或者没有调用;
线程池 2:
- 优化前参数:core : 100 max : 1000 queue : 1000 alive : 0.1 s
- 优化后参数: core : 10 max : 100 queue : 2000 alive : 2m
- 优化方式:减少核心线程数
- 优化原因:任务不多,线程存活时间太短
线程池 3:
- 优化前参数: core : 20 max : 500 queue : 1000 alive : 10m
- 优化后参数: core : 5 max : 100 queue : 1000 alive : 2m
- 优化方式:减少核心线程数;
- 优化原因:任务不多,减少核心线程;提高线程利用率;降低线程存活时间;
线程池 4:
- 优化前参数:core : 20 max : 500 queue : 2000 alive : 10m
- 优化后参数: core : 50 max : 500 queue : 2000 alive : 10m
- 优化方式:增加核心线程数;
- 优化原因:任务堆积情况多,且历史最大线程数比较大;
效果分析
- 接口成功率没有明显的提升(接口超时导致部分接口调用失败),线程池的任务丢弃数变少了;
- 服务器 CPU 使用率也没有明显的变化;
优化总结
总结
- 监控线程池历史最大的线程数:通过这个可以作为是否增大核心线程的一个指标;
- 阻塞队列是否有任务堆积?如果有说明已经到达线程池的最大处理能力,还有任务提交,如果任务比较重要,需要考虑增大线程数;
- 对于完成任务数较少的那些线程池,可以考虑减少线程数;
线程池线程设置的依据
首先是核心线程数和最大线程池数,我们需要考虑任务的性质,分为 CPU 密集型任务和 IO 密集型任务。
① CPU 密集型任务:(视频进行高清解码、转码,文件压缩解压缩)
- 也叫计算密集型,系统中大部份时间用来做计算、逻辑判断等,比如对视频进行高清解码、转码,文件压缩解压缩等,一般而言 CPU 占用率相当高。多线程跑的时候,可以充分利用起所有的 CPU 核心。但是如果线程远远超出 CPU 核心数量反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。
- CPU 密集型任务:核心线程数参考值可以设置为 NCPU + 1;
② IO 密集型任务:(文件读写、DB读写、网络请求)
- 指任务需要执行大量的 IO 操作,涉及到网络 IO、磁盘 IO 操作。当线程因为 IO 阻塞而进入阻塞状态后,该线程的调度被操作系统内核立即停止,不再占用 CPU 时间片段,而其他 IO 线程能立即被操作系统内核调度,等 IO 阻塞操作完成后,原来阻塞状态的线程重新变成就绪状态,而可以被操作系统调度。这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多, CPU 效率越高,但也有一个限度。常见的大部分任务都是 IO 密集型任务;
- IO 密集型任务:核心线程数参考值可以设置为 2 * NCPU;
上面的经验归经验,最终的数据大小需要通过压测来验证,比如预估多少流量的请求,多大的线程数参数和队里长度才能抗住
Contributors
Dylan Kwok