1. 业务背景
- 场景一: 快速响应用户请求
场景描述:比如说⽤户要查看⼀个商品
的信息,那么我们需要将商品维度
的⼀系列信息如商品的价格
、优惠
、库存
、图⽚
等等聚合起来,展示给⽤户。
分析:从用户角度来看,要求响应越快越还,但其实这些面向用户功能的聚合通常会伴随着调用之间的级联、,这种情况下使用线程池,将调用封装成任务并行执行,缩短总体的响应时间,这种场景其实最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务。调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。 - 场景二: 快速处理批量任务
场景描述:比如说有一个离线的计算任务
,计算量很大,像大型的统计报表,其实我们希望的是来快速生成报表
分析:这种需要执行大量任务,我们希望任务执行的越快越好,使用多线程。但这种与响应速度的区别在于:这类场景任务量巨大,并不需要瞬时完成
,而是关注如何使用有限的资源尽可能在单位时间处理更多的任务,也就是强调吞吐量。这种情况下应该设置队列去缓冲并发任务,调整合适的corePoolSize,这里,设置的线程数过多可能会引发线程上下文切换频繁,也会降低处理任务的速度,减低吞吐量。
2. 实际问题及方案思考
线程池使用面临的核心问题在于:线程池的参数不好配置:线程池执行的情况和任务类型相关性很大,IO、cpu密集型任务执行起来的情况差异非常大。
事故描述一:xxx 页面展示接口大量调用降级:队列设置正常,核心线程过小
。
事故原因:该服务展示接口内部逻辑使用线程池并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降低。
事故描述二:服务不可用:任务堆积
,线程池队列⻓度设置过⻓、corePoolSize设置过⼩导致任务执⾏速度低;线程来不及处理任务造成大量任务堆积,导致任务执行时间过长,会导致下游服务的大量调用超时失败。
思考:那线程池参数有计算呢公式吗?
其实很难有一个准确的公式来计算出核心线程、最大线程数的,往往实际场景是根据压测、tps等来大概估算的,但实际中有时候流量是正常的,有时候流量往往是随机的,其实也不符合实际场景,那既然不能一下子算出来,那可以通过动态线程池参数,实现参数的动态化。
那问题来了:怎样将修改线程池参数的成本降下来?
在成本可控的情况下,在发生故障时可以快速调整从而缩短故障恢复的时间?如何缩短时间?线程池参数的动态感知------> 分布式配置中心,实现线程池参数动态配置即时生效。
线程池参数动态化:分布式配置中心,参数的更新。
3. 动态线程池
动态调参:JDK提供的原生的ThreadPoolExecutor提供了对核心线程数、最大线程数、拒绝策略等的set方法,在运行期使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且会基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程的情况,说明有多余的工作线程,这时会向工作线程发起中断请求以实现回收;对于当前值大于原始值并且队列中有待执行任务,则线程池会创建新的工作线程来执行队列任务。
那怎样实现动态线程池:获取到当前程序中的ThreadPoolExecutor实例------> 进而获取到当前程序的线程池参数-----> 之后上报到redis中,基于redis发布订阅,实现动态参数更新。
对于JDK提供的这些set方法,线程池内部会处理好当前状态做到平滑处理。只要维护ThreadPoolExecutor实例,在需要修改的时候修改参数即可。
3.1 遇到的问题
问题一:实际投入项目使用时,使用redis作为线程池的注册中心,项目本身是不是还需要额外定义redis客户端实例?要和starter里面的实例 区分开来使用?
- @ConditionalOnMissingBean。
问题二:为什么redisson 定义的时候 不用默认的配置,而是自己定义的redis的配置类
- 自己定义可以更好控制和扩展,不会受到升级影响。
问题三:如果想这个starter 既要支持redis 又要支持nacos?
- 组件中,如果有多个,之后有需要部分不启动。可以用 class.forName 进行检测,如果类不存在就不检测了。还可以使用@ConditionalOnMissingBean的方式,注入的对象也可以控制。
4. 代码
首先要将本地采集到的线程池数据进行上报,采用redis作为注册中心,将采集到的线程池配置信息上传到redis(这是是用spring定时任务,定时采集线程池配置信息进行上报的);动态更新线程池配置参数,是根据redis发布订阅来实现的,将需要动态更新的消息发布到topic,在消费者这一端(订阅了该topic)就会监听到这个消息,然后将变更的消息设置到当前应用的对应的ThreadPoolExecutor实例上(应用本地有一个Map<String, ThreadPoolExecutor>, key为线程池实例bean的名字,value为线程池实例,拿到的消息包含哪个应用啊、哪个线程池实例啊、以及要设置的参数:核心线程、最大线程;用消息中线程池实例bean的名字拿到对应的实例,然后直接更新这个实例中的核心线程、最大线程、队列长度等信息,实现动态更新,不需要重启应用)。
问:spring采集线程池配置信息怎么做的?
Map<String, ThreadPoolExecutor> threadPoolExecutorMap;就拿到了当前应用中的所有线程池实例。
上报到redis中的信息包括什么:包括线程池的名字、标识、配置参数。