所有的性能优化大体脱离不了以下几个字:降、疏、缓、堵、调、冗。而这几个性能优化的方法中以疏字用的最多。
从最小的多线程,到多进程,到服务的水平,横向的拆分等等。都是针对请求量增大后的疏导方式,而这些方法中以多线程使用最广,也是使用门槛最低的方式。各种语言也在最新的版本中尽量的降低开发人员使用多线程的门槛,例如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
0 条评论。