OutOfMemoryError: Unable to create new native thread:Hystrix线程池导致线程数耗尽的问题

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
  • maxQueueSizeBlockingQueue的最大队列数,默认值-1,代表使用SynchronousQueue同步队列。
  • queueSizeRejectionThreshold:任务队列中存储的任务数量超过这个值,线程池拒绝新的任务。即便没达到maxQueueSize阈值,但达到queueSizeRejectionThreshold阈值,请求也会被拒绝,默认值5maxQueueSize-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问题分析

  1. 我们使用的是hystrix的线程池隔离策略,会根据不同的分组,创建不同的线程池。
  2. 我们的项目中的hystrix的配置只设置了核心线程数,核心线程数没有设置回收。
  3. 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
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值