如何合理地估算线程池大小?
如何确定线程池的大小?
成为大厂offer收割机是怎样一种体验? 一、现状:市场红利正盛,人才短板暴露 计算机的发展历程已经走过了大半个世纪,在当前的互联网+时代下,中国开发者市场正在迎来三大红利:全民编程、行业升级、技术大生态。人人都会编程、家家都是技术公司,全行业数字化升级。 面对大量的需求,目前IT人才的能力却无法匹配企业,核心技术创新能力不足,腰部人才稀缺导致持续发展成为难题。技术人才在成长过程中常会出现学习碎片化、难以持续、缺少指导等问题。 举个例子,打开一线大厂的招聘网站,在招聘需求中往往都会明确要求有一定的项目经验。这就意味着如果没有实操经验,就...
http://ifeve.com/how-to-calculate-threadpool-size/
背景
在我们日常业务开发过程中,或多或少都会用到并发的功能。那么在用到并发功能的过程中,就肯定会碰到下面这个问题
并发线程池到底设置多大呢?
通常有点年纪的程序员或许都听说这样一个说法 (其中 N 代表 CPU 的个数)
- CPU 密集型应用,线程池大小设置为 N + 1
- IO 密集型应用,线程池大小设置为 2N
这个说法到底是不是正确的呢?
其实这是极不正确的。那为什么呢?
- 首先我们从反面来看,假设这个说法是成立的,那我们在一台服务器上部署多少个服务都无所谓了。因为线程池的大小只能服务器的核数有关,所以这个说法是不正确的。那具体应该怎么设置大小呢?
- 假设这个应用是两者混合型的,其中任务即有 CPU 密集,也有 IO 密集型的,那么我们改怎么设置呢?是不是只能抛硬盘来决定呢?
那么我们到底该怎么设置线程池大小呢?有没有一些具体实践方法来指导大家落地呢?让我们来深入地了解一下。
Little's Law(利特尔法则)
一个系统请求数等于请求的到达率与平均每个单独请求花费的时间之乘积
假设服务器单核的,对应业务需要保证请求量(QPS):10 ,真正处理一个请求需要 1 秒,那么服务器每个时刻都有 10 个请求在处理,即需要 10 个线程
同样,我们可以使用利特尔法则(Little’s law)来判定线程池大小。我们只需计算请求到达率和请求处理的平均时间。然后,将上述值放到利特尔法则(Little’s law)就可以算出系统平均请求数。估算公式如下
*线程池大小 = ((线程 IO time + 线程 CPU time )/线程 CPU time ) CPU数目**
具体实践
通过公式,我们了解到需要 3 个具体数值
- 一个请求所消耗的时间 (线程 IO time + 线程 CPU time)
- 该请求计算时间 (线程 CPU time)
- CPU 数目
请求消耗时间
Web 服务容器中,可以通过 Filter 来拦截获取该请求前后消耗的时间
- public class MoniterFilter implements Filter {
- private static final Logger logger = LoggerFactory.getLogger(MoniterFilter.class);
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
- ServletException {
- long start = System.currentTimeMillis();
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
- String uri = httpRequest.getRequestURI();
- String params = getQueryString(httpRequest);
- try {
- chain.doFilter(httpRequest, httpResponse);
- } finally {
- long cost = System.currentTimeMillis() - start;
- logger.info("access url [{}{}], cost time [{}] ms )", uri, params, cost);
- }
- private String getQueryString(HttpServletRequest req) {
- StringBuilder buffer = new StringBuilder("?");
- Enumeration<String> emParams = req.getParameterNames();
- try {
- while (emParams.hasMoreElements()) {
- String sParam = emParams.nextElement();
- String sValues = req.getParameter(sParam);
- buffer.append(sParam).append("=").append(sValues).append("&");
- }
- return buffer.substring(0, buffer.length() - 1);
- } catch (Exception e) {
- logger.error("get post arguments error", buffer.toString());
- }
- return "";
- }
- }
CPU 计算时间
CPU 计算时间 = 请求总耗时 - CPU IO time
假设该请求有一个查询 DB 的操作,只要知道这个查询 DB 的耗时(CPU IO time),计算的时间不就出来了嘛,我们看一下怎么才能简洁,明了的记录 DB 查询的耗时。通过(JDK 动态代理/ CGLIB)的方式添加 AOP 切面,来获取线程 IO 耗时。代码如下,请参考
- public class DaoInterceptor implements MethodInterceptor {
- private static final Logger logger = LoggerFactory.getLogger(DaoInterceptor.class);
- @Override
- public Object invoke(MethodInvocation invocation) throws Throwable {
- StopWatch watch = new StopWatch();
- watch.start();
- Object result = null;
- Throwable t = null;
- try {
- result = invocation.proceed();
- } catch (Throwable e) {
- t = e == null ? null : e.getCause();
- throw e;
- } finally {
- watch.stop();
- logger.info("({}ms)", watch.getTotalTimeMillis());
- }
- return result;
- }
- }
CPU 数目
逻辑 CPU 个数 ,设置线程池大小的时候参考的 CPU 个数
- cat /proc/cpuinfo| grep "processor"| wc -l
总结
合适的配置线程池大小其实很不容易,但是通过上述的公式和具体代码,我们就能快速、落地的算出这个线程池该设置的多大。不过最后的最后,我们还是需要通过压力测试来进行微调,只有经过压测测试的检验,我们才能最终保证的配置大小是准确的。
-----------------------------------------------------------------------------------------------------------
在高并发的情况下采用线程池,有效的降低了线程创建释放的时间花销及资源开销,如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。(在JVM中采用的处理机制为时间片轮转,减少了线程间的相互切换)
那么在高并发的情况下,我们怎么选择最优的线程数量呢?选择原则又是什么呢?。
第一种:任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。CPU密集型任务 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。IO密集型任务 可以使用稍大的线程池,一般为2*CPU核心数+1。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。混合型任务 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
第二种呢,在IO优化文档中,有这样地公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
即线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
优化线程池线程数量
有经验公式
Nthread=Ncpu*Ucpu*(1+W/C)
- 1
- W/C:等待时间与计算时间的比值
- Ncpu:CPU数量
- Ucpu:目标cpu的使用率
Java中下面方法获取CPU数目
int Ncpus=Runtime.getRuntime().availableProcessors();
System.out.println(Ncpus);
- 1
- 2
并发编程网上的一个问题 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
如何合理地估算线程池大小?
这个问题虽然看起来很小,却并不那么容易回答。大家如果有更好的方法欢迎赐教,先来一个天真的估算方法:假设要求一个系统的TPS(Transaction Per Second或者Task Per Second)至少为20,然后假设每个Transaction由一个线程完成,继续假设平均每个线程处理一个Transaction的时间为4s。那么问题转化为:
如何设计线程池大小,使得可以在1s内处理完20个Transaction?
计算过程很简单,每个线程的处理能力为0.25TPS,那么要达到20TPS,显然需要20/0.25=80个线程。
很显然这个估算方法很天真,因为它没有考虑到CPU数目。一般服务器的CPU核数为16或者32,如果有80个线程,那么肯定会带来太多不必要的线程上下文切换开销。
再来第二种简单的但不知是否可行的方法(N为CPU总核数):
- 如果是CPU密集型应用,则线程池大小设置为N+1
- 如果是IO密集型应用,则线程池大小设置为2N+1
如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。
接下来在这个文档:服务器性能IO优化 中发现一个估算公式:
| |
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
| |
可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
上一种估算方法也和这个结论相合。
一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手:
- 尽量提高短板操作的并行化比率,比如多线程下载技术
- 增强短板能力,比如用NIO替代IO
第一条可以联系到Amdahl定律,这条定律定义了串行系统并行化后的加速比计算公式:
| |
加速比越大,表明系统并行化的优化效果越好。Addahl定律还给出了系统并行度、CPU数目和加速比的关系,加速比为Speedup,系统串行化比率(指串行执行代码所占比率)为F,CPU数目为N:
| |
当N足够大时,串行化比率F越小,加速比Speedup越大。
写到这里,我突然冒出一个问题。
是否使用线程池就一定比使用单线程高效呢?
答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:
- 多线程带来线程上下文切换开销,单线程就没有这种开销
- 锁
当然“Redis很快”更本质的原因在于:Redis基本都是内存操作,这种情况下单线程可以很高效地利用CPU。而多线程适用场景一般是:存在相当比例的IO和网络操作。
所以即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值。