线程池(Thread Pool)
在Web应用中线程池的大小决定了在任何一个时间点应用可以处理请求的并发数。如果一个系统收到的请求数超过了线程池的大小,那么超出的请求要么进入等待队列要么被拒绝。
请注意,并发和并行是不同的。并发请求是指在任何一个时间点,所有被处理的请求中只有只有很少一部分占用CPU(译者注:轮流使用CPU)。并行是指在任何一个时间点,所有被处理的请求同时在CPU上运行。
在非阻塞式(NO-Blocking)应用中(如NodeJs),一个单独的线程或进程可以并发处理多个请求。而在多核CPU中则可以通过增加线程或进程数来实现并行处理。
在阻塞式IO应用中(如java的SringMVC,一个线程只能同时处理一个并发请求。如果想要并发处理多个请求只能通过增加线程数来实现。
CPU消耗型应用
对于CPU消耗型应用来说,线程池的大小应该和单台服务器的CPU个数相同。对于这类应用由于线程上下文切换增加线程数反而会妨碍对请求的处理,同时还会增加响应时间。
非阻塞式IO应用由于在请求被处理时并不需要等待请求处理完成,因此属于CPU消耗型的应用。
IO消耗型应用
由于IO消耗型应用依赖于下行流量所在系统的响应时间,而且一个线程在其他系统响应完成之前将一直阻塞,所以决定IO消耗型应用的线程池大小变得更加困难。对于这类型应用,我们就像在阻塞式IO应用文章中讲的,通过增加线程数来提高CPU利用率。
科特尔法则(Little’s Law)
科特尔法则通常被用在非技术领域,例如告诉银行柜台出纳员还有多少客户在等待请求处理。
下面是维基百科对科特尔法的说明,英文原文如下:
The average number of threads in a system (Threads) is equal average web request arrival rate (WebRequests per sec), multiplied by the average response time (ResponseTime)
译文:一个系统的平均线程数(线程数)等于平均请求的到达率(每秒请求数)乘以平均响应时间(响应时间)。
公式:线程数=每秒请求数 X 响应时间
公式说明:
线程数系统所能处理的线程数量
每秒请求数每秒钟所能处理的请求数
响应时间处理一个请求所花费的时间
当然,上边的公式给出了处理多少请求需要多少线程,但是并没有考虑线程对CPU的占用率等情况,也没有说明对于多核的单台机器应该分配多少线程。
通过测试决定线程池大小
要分配合适大小的线程池就需要在吞吐量和响应时间这两个要素之间寻求平衡点。从每个CPU最少线程数开始(即线程数=cpu数),系统线程数和平均响应时间成正比直到CPU使用率达到最大或者响应时间不再减少为止。
下图说明了请求数、CPU和响应时间之间的关系。
CPU和请求数的图中展示了随着Web系统负载量不断增加时CPU的使用情况。
响应时间和请求数的图中展示了Web系统负载量的增加对响应时间的影响。
绿色的点表示吞吐量和响应时间的最优点。
线程池大小=CPU核心数
上图展示的是阻塞式IO消耗型应用在线程池大小等于CPU核心数量时的情况。线程由于要等待下行流量的IO处理所以会阻塞,而由于线程的阻塞使响应时间进一步增加,而且即使CPU的占用率非常低,但是线程池中所有线程都处于阻塞状态,那么应用还是会拒绝请求。
大的线程池
上图展示的是阻塞式IO消耗型应用在大的线程池下的使用情况。由于线城池数量大,线程上下文切换也变得非常频繁,而正是这些没必要的上下文切换使得应用还没有达到最大吞吐量时CPU就已经达到最大占用率了。请求响应时间也由于频繁的上下文切换而快速增长。
最优线程池大小
上图展示的是阻塞式IO消耗型应用在最优线程池下的情况。在高吞吐量和更少线程上文切换的情况下CPU得到了高效的利用。同时我们注意到,好的响应时间取决于在线程更少被阻断(上下文切换)的情况下对请求的高效处理。
线程池隔离
在大多数应用中,只有少数类型的请求会比其他请求更耗时,但这少数的耗时请求会影响整个系统的性能。有两个办法可以解决这个问题:
1)将比较耗时的请求隔离开来专门处理
2)在同一个应用中为耗时的web请求单独分配一个线程池
决定一个阻塞式IO消耗型应用的最优线程池大小是一件困难的事情,这通常需要通过多个性能测试来决定。如果在一个应用中使用多个线程池,会使对线程池的优化进一步复杂化。