线程池的一些使用经验

所有的性能优化大体脱离不了以下几个字:降、疏、缓、堵、调、冗。而这几个性能优化的方法中以疏字用的最多。

从最小的多线程,到多进程,到服务的水平,横向的拆分等等。都是针对请求量增大后的疏导方式,而这些方法中以多线程使用最广,也是使用门槛最低的方式。各种语言也在最新的版本中尽量的降低开发人员使用多线程的门槛,例如JDK从1.4到1.8,一直在降低写多线程的复杂度,golang和erlang这些语言在语言层面就建立了非常简单的线程模型。

在使用多线程时候,考虑系统资源的有限,加上建立和销毁线程有一定的陈本,所以对于多线程使用通用的做法是采用线程池。

即缓存一定的线程来重复的使用,避免过多的线程创建和销毁。同时建立一定的机制,快速,动态,高效的维护线程数目。这些具体可以通过线程池的几个参数可以了解。

这里以JDK中的线程池为例,其他jetty,tomcat大多类似:

 

  • coreSize:线程池中初始化的线程个数
  • maxSize:线程池中存活的最大线程个数
  • keepAliveTime:超过coreSize数时闲置工作者的存活时间
  • workQueue:用于缓存当前池中无空闲worker时,待处理的任务
  • 饱和策略:当线程池饱和后,对于新进来任务的处理策略

在JDK中提供了四种拒绝策略:

  • CallerRunsPolicy :调用者脱离线程池执行运行
  • AbortPolicy : 放弃任务,并抛出异常
  • DiscardPolicy:放弃任务,不抛出异常
  • DiscardOldestPolicy:抛弃排在队列前面的任务,并把自己加到队列中

以上的几个参数和策略基本覆盖了线程池中的几个方面:初始化,中间状态存储,任务算法,workers的增减算法,过载保护等方面。可以说已经相当的全面和完善。

其中的任务算法简单说,当有任务到来时候启用coreSize中的线程去处理,若是coreSize没有空闲时将任务放到队列中,若是队列也放满后,增加线程数到maxSize,若是maxSize的线程数也无法处理请求时,采用一定的饱和策略。

初始的几个参数中,其中coreSize和maxSize基本需要基于业务场景去压测得出一个合理的值,例如是CPU密集型还是IO密集型,或是复合型,很难有一个固定的数目。

或者说纯粹的CPU密集型会好定义一点,一般建议coreSize是 CPU+1, maxSize 是2*CPU.

workQueue这个一般不建议是设置为无界队列,因为无界很容易在大流量下造成内存的过度使用,进一步OOM。至于具体队列的大小需要根据以下几个因素进行设置

  • 每个请求对象的大小
  • 系统处理速度
  • 系统自身的内存大小
  • 处理的maxSize

 

假设每个对象大小是300字节,单CPU处理一个任务速度是0.001秒,客户端对于响应时间的要求是1秒,maxSize是20个 。那么可以建议的队列长度是(20×1)/0.001 = 20000.同时内存大小必须大于20000×300byte = 5.8M ,当然这个内存一般没什么问题。

剩下就是饱和策略的选择。

幂等,非事务的请求业务场景

例如说网易首页,每次请求其实是幂等,而且无事务,任何一次请求失败不会造成很严重的后果,所以选择AbortPolicy和DiscardPolicy,DiscardOldestPolicy都没什么问题,一般这种场景更建议选择DiscardPolicy。因为重试成本比较低。同时这种策略保护了线程池自身,能够持续的提供服务能力。

非幂等,事务型的请求业务场景

例如网易宝,对于每个支持请求都是非幂等的,且要有事务保证,对于事务保证从大架构的方面说有很多手段,例如定期校队,可靠消息模式,TCC,补偿模式等等。这里具体不展开,只简单说在线程池这个简单范围内可以如何操作。

同时满足自己的过载保护及事务的保证,可以采用的饱和策略只剩下一种:AbortPolicy,抛弃任务,并抛出异常,通过外部机制来处理,至于外部是补偿模式还是可靠消息模式可再去斟酌。

还有一种简单的处理办法就是自定义饱和策略,将有限,不可伸缩的存储队列空间外置。在单节点中线程池饱和后,将消息外置到一个分布式缓存中,可以是redis,也可以是HDFS。具体需要依赖于消息数据完整性的要求。

然后在系统有空闲能力时去读取这些数据,当然这样会带来响应时间的延迟,在系统设计时需要综合的去考虑。其实这个也雷同于AbortPolicy,不过AbortPolicy采用的是比较过激的方式告知系统。

这里有一个点需要关注,尽量不要触发线程池任务机制中定义的饱和策略,因为若是等到触发线程池中的饱和测试时,一些业务相关的数据你很难通过简单的方式去拿到。这个时候留给你的操作空间一般较小。

一种比较简单的做法是自己去检测队列中的数据大小,当队列堆积到一个阀值时候提前做自我保护,防止将系统撑死。这个做法需要将coreSize和maxSize设置为相同的值。因为任务不是按照原本的任务算法流转了。

一般CallerRunsPolicy策略很少使用,若是你内存足够大的话,大可将workQueue设置的更大点。

以上基本概述了线程池中各个参数设置的一些权衡点。

参考资料:

1.http://www.infoq.com/cn/articles/thread-pool-algorithm-realization

2.http://www.cnblogs.com/skywang12345/p/3512947.html

3.http://www.infoq.com/cn/articles/java-threadPool

作者: inter12

在这苦短的人生中,追求点自己的简单快乐

发表评论

电子邮件地址不会被公开。 必填项已用*标注