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