动态线程池思想学习及实践
相关文档
美团线程池实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 线程池思想解析:https://www.javadoop.com/post/java-thread-pool
引言
在后台项目开发过程中,我们常常借助线程池来实现多线程任务,以此提升系统的吞吐率和响应性;而线程池的参数配置却是一个难以合理评估的值,虽然业界也针对cpu密集型,IO密集型等场景给出了一些参数配置的经验与方案,但是实际业务场景中通常会因为流量的随机性,业务的更迭性等情况出现预计和实际运行情况偏差较大的情况;而不合理的线程池参数,可能导致服务器负载升高,服务不可用,内存溢出等严重问题;一旦遇到参数不合理的问题,还需要重新上线修改,并且存在反复修改的情况,而这期间花费的时间可能带来更大的风险,甚至导致严重业务事故;那么有没有一种方式能有效感知上述问题并及时避免以上问题呢?或许动态线程池可以。
什么是动态线程池
简单来说,动态线程池就是能在不重新部署应用的情况下动态实时变更其核心参数,并且能对其核心参数及运行状态进行监控及告警;以便开发人员可以及时感知到实际业务中因为各种随机情况导致线程池异常的场景,并依据动态变更能力快速调整并验证参数的合理性。
为什么需要动态线程池,存在什么痛点
线程池在给我们业务带来性能和吞吐提升的同时,也存在诸多风险和问题,其中主要原因就在于我们难以设置出合理的线程池参数,一方面线程池的运行机制不是很好理解,配置合理强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,同时实际场景中流量的随机性,业务的更迭性也导致业界难以有一套成熟或开箱即用的经验策略来帮助开发人员参考。而线程池参数难以合理设置的特性又不得不让我们关注以下三个痛点问题:
1.运行情况难感知:在业务使用线程池的过程中,线程池的运行情况对于开发人员来说很难感知,我们难以知道每个线程池创建了多少个线程,是否有队列积压,线程池运行状态怎么样,线程池是否已经耗尽... 直到出现线上问题或收到客诉才后知后觉;(我们能否对系统中用到的线程池进行一个整体的把控,在线程池任务积压,任务拒绝等问题发生时,甚至问题发生前进行及时感知,让开发人员能未雨绸缪,尽早发现和解决问题呢?-线程池监控,异常告警)
流量突增导致预估和实际情况偏差较大,同时由于未能及时感知并解决积压情况,最终引发客诉 case1:广告主大批量删除物料后异步清理附属表出现任务积压 问题描述:广告主批量删除计划物料后,对应物料附属表数据未及时删除,导致广告主关键词等物料数上限得不到释放而影响创建新物料,引发线上客诉。 问题原因:广告主删除计划物料后,系统会同步删除计划物料主表信息,然后通过线程池的方式异步删除计划物料附属表数据。临近大促广告主物料增删频率及单次批量操作的物料数量都有明显增加,由于核心线程设置较小同时队列设置过长,导致计划主表同步删除后异步删除附属表的任务出现队列积压,对应的关键词等物料数上限得不到释放而影响新物料创建,引发线上客诉。
2.线程拒绝难定位:当拒绝发生后,即使我们迅速感知到了线程池运行异常,也经常会因为拒绝持续时间较短而拿不到问题发生时的线程堆栈,因此通常难以快速定位甚至无法定位到是哪里的原因导致的拒绝,比如是流量的突增将线程池打满,还是某个业务逻辑耗时较长将线程池中的线程拖住;(我们有没有一种方式能在线程池拒绝后去更容易的定位到问题呢?-自动触发线程池堆栈打印,分析工具)
case2: 线程池拒绝具有随机性,当拒绝时长较短时,难以定位问题原因 问题描述:某业务接口内部计算逻辑较多,且存在多处外部接口调用逻辑,上线后不定时出现线程池拒绝异常,由于持续时间不长,问题发生后无法通过jstack去获取问题发生时现场的线程堆栈, 很难定位是什么原因导致了线程池拒绝;由于没有较好的排查手段,只能通过逐步搂日志的方式排查,而排查过程又可能因为日志较多或者日志不全出现问题定位时间长或者是根本无法定位的情况。 问题原因:某外部某接口不稳定,在性能较差且流量较大时就容易把调用线程池打满,导致可用率下降
3.参数问题难以快速调整:在定位到某个线程池参数设置不合理的情况后,我们需要根据情况随即进行调整,但是"修改->打包->审批->发布"的时间很可能会扩大问题的影响甚至是事故严重程度;同时因为线程池参数难以合理设置的原因,可能导致我们要重复进行上述"修改->打包->审批->发布"的流程...(有没有一种方法能快速修改并验证参数设置的合理性呢?-参数动态调整)
线程池参数设置不合理,难以快速调整参数,业务风险上升 case3:应用JSF接口修改为异步调用后出现可用率下降 问题描述:将应用中部分JSF接口切换为异步模式后,对应可用率有明显下降 问题原因:在修改为异步模式的JSF接口中,部分业务在拿到future对象后使用ThenApply做了一些耗时的操作,另外还有一部分在ThenApply里面又调用了另外一个异步方法;而thenApply的执行会使用jsf的callBack线程池,由于线程池线程配置较小,并且部分回调方法耗时较长,导致callBack线程池被打满,子任务请求线程时进入阻塞队列排队,出现接口超时可用率下降。
业界动态线程池动态线程池调研
当前业界已存在部分动态线程池组件,其主体功能及大体思想类似,但存在以下几个问题
1.与外部中间件耦合较多,难以二次开发加以使用;
2.使用灵活性受限,难以根据业务自身特点进行定制化(自动触发线程池堆栈打印,一键清空队列,callback线程池等)
综合考虑上述问题,决定结合公司中间件及自身业务特点实现一套集线程池监控,异常告警,线程栈自动获取,动态刷新为一体的动态线程池组件。
如何实现动态线程池
整体方案
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-05-22-20-32ptjr6mnH7EmCzpX.png)
线程池监控及告警
要实现线程池监控及告警,我们需要关注以下几个要点
1.如何获取到待监控的线程池信息
在实际业务中我们通常想要知道应用中有哪些线程池,每个线程池各个参数在每个时刻的运行情况是怎么样的;对于第一种场景,我们可以构建一个线程池管理器,用于管理应用中使用到的业务线程池,为此我们可以在应用初始化时将这些线程池按名称和实际对象注册到管理器;后续使用时就可以根据名称从管理中心拉取到对应线程池;
public class ThreadPoolManager {
// 线程池管理器
private static final ConcurrentHashMap<String, Executor> REGISTER_MAP_BY_NAME = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Executor, String> REGISTER_MAP_BY_EXECUTOR = new ConcurrentHashMap<>();
// 注册线程池
public static void registerExecutor(String threadPoolName, Executor executor) {
REGISTER_MAP_BY_NAME.putIfAbsent(threadPoolName, executor);
REGISTER_MAP_BY_EXECUTOR.putIfAbsent(executor, threadPoolName);
}
// 根据名称获取线程池
public static Executor getExecutorByName(String threadPoolName) {
return REGISTER_MAP_BY_NAME.get(threadPoolName);
}
// 根据线程池获取名称
public static