线程池ThreadPoolTaskExecutor使⽤不当的惨痛教训
问题现场:
配置:⽣产环境nginx做负载均衡,后端三台服务器,这样⼀个传统的集架构。
现象:平时系统正常时,没怎么发现问题。最近随着业务量增⼤,我们以及依赖的⼀些第三⽅服务接连出现各种各样的服务超时,不可⽤的情况。最终我们也没有幸免,因为我们的业务依赖第三⽅接⼝的成分较⼤,属于⼀个⽤户接收,⽤户渠道的⾓⾊。这也就意味着如果第三⽅服务不可⽤,我们的相应服务也将不可⽤了。没错,最终我们的系统挂了!
原因:原因是服务⾥⾯的⼀个接⼝不可⽤了,调⽤总是超时。⽽接⼝调⽤是在⼀个异步线程池中进⾏的。
<bean id="taskExecutor"class="urrent.ThreadPoolExecutor" p:corePoolSize="2"
p:maxPoolSize="5"/>
嗯,这是我们的⼀个线程池配置。是以前的⼀个⽼代码,因为缺少定期的代码评审 + 繁忙的业务逻辑任务,代码疏于管理,就这样了。
问题分析
熟悉线程池的同学可以知道,上⾯的线程池就是Executors创建的线程池了,可以参考:
1、它的阻塞队列是⼀个⽆界队列,队列⾥⾯最多可⽤容纳的任务可达Integer.MAX_VALUE个,当阻塞的任务过多,势必会产⽣oom异常。
2、线程池中线程数的配置。我前⾯说了,我们的业务基本都是依赖了调⽤第三⽅接⼝的这样⼀个场景,在这⾥,如果该线程池中的线程需要调⽤第三⽅接⼝,这是⼀个IO密集型的线程池(⽹络IO甚⾄⽐磁盘IO还要慢上⼏⼗倍),设置2个线程⼤⼩显然是不合理的。同时因为阻塞队列的长度为Integer.MAX_VALUE,即使任务很多,线程池⼤⼩也不会扩容到p:maxPoolSize=“5”个。在这种场景下,两个较慢的请求就会占⽤整个线程池,后⾯的请求都将排队了。
3、还有⼀个致命的问题(前⾯没说),就是多个业务逻辑公⽤了这个线程池。更加加剧的线程池的压⼒,且因为⼀个服务接⼝不可⽤,导致调⽤其他可⽤接⼝的服务也同时不可⽤,⼏乎全⾯崩盘。
4、没有使⽤⼀个有效的拒绝策略。当巨⼤的流量过来时,在线程池这⼀层也可以做⼀层拒绝策略。如提⽰⽤户 “系统过于⽕爆,请稍后再试!”。外层也进⾏请求限流,甚⾄服务熔断以保证服务能存活下来。这些我们都没有。
java线程池创建的四种
问题⼩结
⾸先服务不可⽤起初是出现在第三⽅系统,但最终蔓延到我们系统。同时⼜因为我们使⽤线程池的不合理(多个服务公⽤⼀个线程池,线程池线程数、阻塞队列、拒绝策略的等问题),导致了整个服务不可⽤。
解决⽅案
1、线程池应做到尽量隔离,独⽴。即每个需要异步执⾏的业务逻辑应独⽴配置⼀个线程池,这样不⾄于业务服务之间不影响。这样,⼀个接⼝不可⽤,也只是这个线程池处于阻塞状态,其他线程池的对应服务还能继续正常执⾏。
2、线程池参数做到合理配置。配置规则为:
线程数:
计算密集型(⽐如都是做数据运算,不涉及IO操作的),线程数可以配置为和cpu核数差不多。(配置过多即使cpu处于空闲状态,过多的线程数只会增加线程切换,降低效率)
IO密集型:这个就不好说了,我觉得需要看任务中IO操作与计算操作的⽐例。假如都是IO操作,需要c
pu,那么你可以设置的尽量⼤(但⼜不能过⼤,因为系统的线程数是有限制的)。如果计算与IO操作各⼀半,那么就类似于计算密集型的个数*2,我相信你也理解了。
阻塞队列:
阻塞队列可以按照⾃⾝系统的⽤户使⽤量,业务的容许延迟程度,内存的宽裕程度等考虑。可以设置1000-⼏万不等
3、线程池要配置合理的拒绝策略。⼈⽣就是这样,有舍才有得。作为⼀名软件设计者,你不能让有限的硬件软件资源能够处理⽆限的请求吧。所以设置合理的拒绝策略是有必要的。urrent.ThreadPoolExecutor中提供的四种拒绝策略有在线程直接执⾏,抛出异常,直接抛弃任务、抛弃阻塞队列中最⽼的任务。在真实场景中差强⼈意,你可以实现urrent.RejectedExecutionHandler 接⼝实现⼀个⾃定义拒绝策略,抛出⼀个更友好的提⽰等。
⼩结
做到以上⼏点以后,即使某个第三⽅接⼝服务不可⽤,在⼤部分情况下,能保证本系统的安全。当然因为第三⽅接⼝服务不可⽤,当前功能的不可⽤也是⽆奈之举,有数据不⼀致问题也只能后期运维。谁叫服务没有很好地做到⾼可⽤呢!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论