Hystrix
线程池导致线程数耗尽的问题
1.背景
一次在生产环境中,服务报错了java.lang.OutOfMemoryError: unable to create new native thread
,查看了系统的内存是足够的,操作系统设置的最大线程数是5000,以为是代码中有使用线程池或者异步调用后线程没有回收又或者是一些http调用、rpc调用后链接没有释放。于是,重启服务,排查代码。修改完代码中可能会出现问题的部分,结果过了两天,同样的问题再次出现,直接心态爆炸。通过工具分析,发现大量的线程都是wait
状态,并且这些线程基本都是hystrix-
开头的。这才明白,是项目用使用的hystrix
组件配置存在问题。
2.问题排查
2.1 Hystrix
配置
这里先说一下,hystrix
组件有两种隔离策略,信号量隔离和线程池隔离,默认使用的是线程池隔离。我们这里只说线程池隔离策略。
线程池隔离:使用该方式,HystrixCommand
将会在单独的线程上执行,并发请求受线程池中线程数量的限制。
hystrix
详细配置项信息可以参考https://github.com/Netflix/Hystrix/wiki/Configuration#keepAliveTimeMinutes。
hystrix:
command:
default:
execution:
isolation:
# 信号量隔离,不加默认线程池隔离
# strategy: SEMAPHORE
# semaphore:
# maxConcurrentRequests: 3000
thread:
timeoutInMilliseconds: 300000
interruptOnTimeout: true
threadpool:
default:
# 核心线程数
coreSize: 5
# 最大线程数
maximumSize: 1500
# 释放线程时间 min为单位 默认为1min,当最大线程数大于核心线程数的时
keepAliveTimeMinutes: 1
# 是否允许maximumSize生效,默认false只有coreSize会生效
allowMaximumSizeToDivergeFromCoreSize: true
# BlockingQueue的最大队列数,默认值-1代表使用SynchronousQueue队列
maxQueueSize: -1
# 即便没达到maxQueueSize阈值,但达到queueSizeRejectionThreshold阈值,请求也会被拒绝,默认值5
# maxQueueSize为-1时,此参数不生效
queueSizeRejectionThreshold: 200
coreSize
:核心线程数,默认值10。maximumSize
:最大线程数,默认值10。keepAliveTimeMinutes
:超过这个时间多于coreSize
数量的线程会被回收,只有maximumsize
大于coreSize
,这个值才有意义,默认值1min
。allowMaximumSizeToDivergeFromCoreSize
:是否允许maximumSize
生效,默认false只有coreSize
会生效,默认值false
。maxQueueSize
:BlockingQueue
的最大队列数,默认值-1,代表使用SynchronousQueue
同步队列。queueSizeRejectionThreshold
:任务队列中存储的任务数量超过这个值,线程池拒绝新的任务。即便没达到maxQueueSize
阈值,但达到queueSizeRejectionThreshold
阈值,请求也会被拒绝,默认值5
,maxQueueSize
为-1
时,此参数不生效。
再来看一下我们项目中hystrix
的相关配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 600000
threadpool:
default:
coreSize: 200
我们项目中最初值配置了核心线程数200。hystrix会让相同组名groupKey使用统一线程池。所以不同分组的hystrix会根据不同的分组创建多个隔离的线程池。
2.2Hystrix
线程池原理
说起Hystrix就离不开核心注解:@HystrixCommand,@HystrixCommand注解可以配置的除了常用的groupKey、commandKey、fallbackMethod等,还有一个很关键的就是threadPoolKey,就是使用Hystrix线程隔离策略时的线程池Key。
@HystrixCommand注解源码如下:
/**
* This annotation used to specify some methods which should be processes as hystrix commands.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HystrixCommand {
/**
* The command group key is used for grouping together commands such as for reporting,
* alerting, dashboards or team/library ownership.
* <p/>
* default => the runtime class name of annotated method
*
* @return group key
*/
String groupKey() default "";
/**
* Hystrix command key.
* <p/>
* default => the name of annotated method. for example:
* <code>
* ...
* @HystrixCommand
* public User getUserById(...)
* ...
* the command name will be: 'getUserById'
* </code>
*
* @return command key
*/
String commandKey() default "";
/**
* The thread-pool key is used to represent a
* HystrixThreadPool for monitoring, metrics publishing, caching and other such uses.
*
* @return thread pool key
*/
String threadPoolKey() default "";
......
从@HystrixCommand的源码注释来看:
- groupKey的默认值是使用@HystrixCommand标注的方法所在的类名
- commandKey的默认值是@HystrixCommand标注的方法名,即每个方法会被当做一个HystrixCommand
- threadPoolKey没有默认值,其实是和groupKey保持一致
HystrixThreadPool
public HystrixThreadPoolDefault(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties.Setter propertiesDefaults) {
this.properties = HystrixPropertiesFactory.getThreadPoolProperties(threadPoolKey, propertiesDefaults);
HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
this.queueSize = properties.maxQueueSize().get();
this.metrics = HystrixThreadPoolMetrics.getInstance(threadPoolKey,
concurrencyStrategy.getThreadPool(threadPoolKey, properties),
properties);
this.threadPool = this.metrics.getThreadPool();
this.queue = this.threadPool.getQueue();
/* strategy: HystrixMetricsPublisherThreadPool */
HystrixMetricsPublisherFactory.createOrRetrievePublisherForThreadPool(threadPoolKey, this.metrics, this.properties);
}
HystrixConcurrencyStrategy
public ThreadPoolExecutor getThreadPool(final HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) {
final ThreadFactory threadFactory = getThreadFactory(threadPoolKey);
final boolean allowMaximumSizeToDivergeFromCoreSize = threadPoolProperties.getAllowMaximumSizeToDivergeFromCoreSize().get();
final int dynamicCoreSize = threadPoolProperties.coreSize().get();
final int keepAliveTime = threadPoolProperties.keepAliveTimeMinutes().get();
final int maxQueueSize = threadPoolProperties.maxQueueSize().get();
final BlockingQueue<Runnable> workQueue = getBlockingQueue(maxQueueSize);
if (allowMaximumSizeToDivergeFromCoreSize) {
final int dynamicMaximumSize = threadPoolProperties.maximumSize().get();
if (dynamicCoreSize > dynamicMaximumSize) {
logger.error("Hystrix ThreadPool configuration at startup for : " + threadPoolKey.name() + " is trying to set coreSize = " +
dynamicCoreSize + " and maximumSize = " + dynamicMaximumSize + ". Maximum size will be set to " +
dynamicCoreSize + ", the coreSize value, since it must be equal to or greater than the coreSize value");
return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
} else {
return new ThreadPoolExecutor(dynamicCoreSize, dynamicMaximumSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
}
} else {
return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
}
}
通过源码我们发现hystrix线程池底层使用的是JDK线程池。
2.3问题分析
- 我们使用的是hystrix的线程池隔离策略,会根据不同的分组,创建不同的线程池。
- 我们的项目中的hystrix的配置只设置了核心线程数,核心线程数没有设置回收。
- hystrix线程池底层是使用的JDK线程池。
结论:当线程池设置的分组过多,且线程池的核心线程数设置的过大,我们知道核心线程数的没有设置回收的所以会一直存在,当执行完任务之后,会进入等待状态,不会回收,所以导致了线程数耗尽。
3.解决方案
3.1Hystrix
线程池配置参数相关源码分析
HystrixContextScheduler
@Override
public Subscription schedule(Action0 action) {
if (threadPool != null) {
if (!threadPool.isQueueSpaceAvailable()) {
throw new RejectedExecutionException("Rejected command because thread-pool queueSize is at rejection threshold.");
}
}
return worker.schedule(new HystrixContexSchedulerAction(concurrencyStrategy, action));
}
HystrixThreadPool
@Override
public boolean isQueueSpaceAvailable() {
if (queueSize <= 0) {
// we don't have a queue so we won't look for space but instead
// let the thread-pool reject or not
return true;
} else {
return threadPool.getQueue().size() < properties.queueSizeRejectionThreshold().get();
}
}
通过代码我们发现,
- 配置线程池队列大小参数为-1时,任务的执行与否交给java线程池决定,此时队列是同步队列,那么当并发任务数量大于核心线程数小于最大线程数的时候,是应该会创建新的线程来执行此任务。那么
maximumSize
的配置是有效的 - 配置线程池队列的
maxQueueSize
大于等于queueSizeRejectionThreshold
配置时。若此时并发数达到了核心线程数和maxQueueSize
配置之和,再有任务需要执行时,根据此逻辑,会返回false
,拒绝任务的执行,并不会交给线程池处理。从而使得maximumSize
的配置是无效的。
由此,我们追溯到了maximumSize
配置无效的原因。
让maximumSize
变得有效
- 不使用线程池的队列,直接将maxQueueSize配置设为 -1
queueSizeRejectionThreshold
配置大于maxQueueSize
也可以让线程池中线程的数量达到maximumSize
数量,但是此时queueSizeRejectionThreshold
配置并没有起到它应该承担的意义,因为线程池中队列的大小永远不可能达到queueSizeRejectionThreshold
配置的数量
3.2修改项目Hystrix
相关配置
根据上面提到的线程池的相关配置信息,我们对hysrix的线程池相关配置如下
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
interruptOnTimeout: true
threadpool:
default:
# 核心线程数
coreSize: 5
# 最大线程数
maximumSize: 500
# 释放线程时间 min为单位 默认为1min,当最大线程数大于核心线程数的时
keepAliveTimeMinutes: 1
# 是否允许maximumSize生效,默认false只有coreSize会生效
allowMaximumSizeToDivergeFromCoreSize: true
# BlockingQueue的最大队列数,默认值-1代表使用SynchronousQueue队列
maxQueueSize: -1
# 即便没达到maxQueueSize阈值,但达到queueSizeRejectionThreshold阈值,请求也会被拒绝,默认值5
# maxQueueSize为-1时,此参数不生效
queueSizeRejectionThreshold: 200