在部署 web 应用到生产环境,或者在对 web 应用进行性能测试的时候,经常会有人问:如何决定 web 应用线程池大小?决定一个 IO 阻塞型 web 应用的线程池大小是一项很艰巨的任务。通常是通过进行大量的性能测试来完成。在一个 web 应用中同时拥有多个线程池会让决定最优线程池大小的过程变得更加复杂。本文将就这个常见的问题进行一些讨论和建议。
请注意并发和并行不是一个概念。并发请求指的是正在处理中的请求数量,在某个时间点,只有其中的一小部分能够得到 CPU 执行。而并行请求指的是正在处理的请求数量,在某个时间点,所有请求都在被 CPU 执行。
在非阻塞型 IO 应用中,比如 NodeJS,单个线程(进程)能够同时处理多个请求。多核 CPU 处理器下,通过增加线程或进程数能够处理并行请求。
在阻塞型 IO 应用中,比如 SpringMVC,单个线程只能同时处理一个请求。要同时处理多个并发请求的话,我们必须增加线程数量。
非阻塞型 IO 应用将会是 CPU 密集型的,因为在请求得到处理的时候没有线程等待时间。
利特尔法则: 在一个稳定的系统中,长时间观察到的平均顾客数量 L,等于长时间观察到的有效到达速率,λ,与平均每个顾客在系统中花费的时间之乘积:L = λW。
适用于 web 应用的利特尔法则: 一个系统中线程的平均数量(Threads),等于 web 请求的到达速率(WebRequests per sec),与平均每个处理的响应时间(ResponseTime)的乘积。
Threads = 线程的数量
WebRequests per sec = 一秒内能够处理的 web 请求数
ResponseTime = 处理一次 web 请求所需要的时间
Threads = (WebRequests/sec) X ResponseTime
尽管上边这个公式提供了处理进入请求的线程个数,它并没有提供线程数和 CPU 核心数之间的比率信息,比如一个 x 个 CPU 的主机需要分配多少个线程。
下图指出了请求数、CPU 以及响应时间等指标之间的关联关系。
CPU Vs 请求数演示了在增加 web 应用负载时的 CPU 利用率。
响应时间 Vs 请求数图演示了增加 web 应用负载对响应时间的影响。
绿点指出了最佳吞吐量和响应时间。
线程池大小 = CPU 个数
上图描述的是 IO 等待型应用在线程数等于 CPU 数时的情况。应用的线程在等待下游系统响应时发生了阻塞。由于线程都阻塞住了,系统响应时间因请求进入等待队列而被拉长。由于所有线程都处于阻塞状态,应用开始拒绝请求,尽管 CPU 使用率还很低。
线程池很大
上图描述的是 IO 等待型应用在 web 应用中创建了很多线程的情况。由于有很多数量的线程,线程的上下文切换将会很频繁。由于不必要的线程上下文切换,尽管吞吐量还没升上去的时候应用的 CPU 使用率就已经很高了。响应时间由于被请求的处理被线程的上下文切换所打断而被拉长。
最佳线程池大小
上图描述的是 IO 等待型应用在 web 应用中创建了合理数量的线程的情况。CPU 得到了有效利用,具备良好的吞吐量和较少的线程上下文切换。我们可以看到由于更少的打断(上下文切换),请求处理更加有效,应用有一个良好的响应时间。
处理这种问题的两个方案是:
线程池
web 应用中的线程池大小决定了在指定时间内能够处理的并发请求数。如果一个 web 应用接收到的请求数高于线程池大小,多出来的请求将进入队列等待,或被拒绝。请注意并发和并行不是一个概念。并发请求指的是正在处理中的请求数量,在某个时间点,只有其中的一小部分能够得到 CPU 执行。而并行请求指的是正在处理的请求数量,在某个时间点,所有请求都在被 CPU 执行。
在非阻塞型 IO 应用中,比如 NodeJS,单个线程(进程)能够同时处理多个请求。多核 CPU 处理器下,通过增加线程或进程数能够处理并行请求。
在阻塞型 IO 应用中,比如 SpringMVC,单个线程只能同时处理一个请求。要同时处理多个并发请求的话,我们必须增加线程数量。
计算密集型应用
在计算密集型应用中,线程池的大小应该等同于主机中 CPU 的数量。再添加更多线程将会打断请求的处理,因为线程的上下文切换也会延迟响应时间。非阻塞型 IO 应用将会是 CPU 密集型的,因为在请求得到处理的时候没有线程等待时间。
IO 等待应用
决定 IO 等待应用的线程池大小会由于依赖于下游系统的响应时间而变得更加复杂,因为一个线程在其他系统响应之前始终是阻塞的。我们不得不像《 应答者模式:I/O 阻塞型应用》中讨论的那样去增加线程的数量以提高 CPU 利用率。利特尔法则
利特尔法则应用于非技术领域,比如银行,以估算处理进入银行客户所需要的银行出纳柜台的数量。利特尔法则: 在一个稳定的系统中,长时间观察到的平均顾客数量 L,等于长时间观察到的有效到达速率,λ,与平均每个顾客在系统中花费的时间之乘积:L = λW。
适用于 web 应用的利特尔法则: 一个系统中线程的平均数量(Threads),等于 web 请求的到达速率(WebRequests per sec),与平均每个处理的响应时间(ResponseTime)的乘积。
Threads = 线程的数量
WebRequests per sec = 一秒内能够处理的 web 请求数
ResponseTime = 处理一次 web 请求所需要的时间
Threads = (WebRequests/sec) X ResponseTime
尽管上边这个公式提供了处理进入请求的线程个数,它并没有提供线程数和 CPU 核心数之间的比率信息,比如一个 x 个 CPU 的主机需要分配多少个线程。
测试决定线程池大小
要找出合适的线程池大小,需要在吞吐量和响应时间之间进行权衡。先以一个最小值开始测试:一个 CPU 一个线程(也就是线程池大小 = CPU 个数),应用线程池大小与下游系统平均响应时间成正比增长,直到 CPU 使用率饱和或者响应时间开始退化为止。下图指出了请求数、CPU 以及响应时间等指标之间的关联关系。
CPU Vs 请求数演示了在增加 web 应用负载时的 CPU 利用率。
响应时间 Vs 请求数图演示了增加 web 应用负载对响应时间的影响。
绿点指出了最佳吞吐量和响应时间。
线程池大小 = CPU 个数
上图描述的是 IO 等待型应用在线程数等于 CPU 数时的情况。应用的线程在等待下游系统响应时发生了阻塞。由于线程都阻塞住了,系统响应时间因请求进入等待队列而被拉长。由于所有线程都处于阻塞状态,应用开始拒绝请求,尽管 CPU 使用率还很低。
线程池很大
上图描述的是 IO 等待型应用在 web 应用中创建了很多线程的情况。由于有很多数量的线程,线程的上下文切换将会很频繁。由于不必要的线程上下文切换,尽管吞吐量还没升上去的时候应用的 CPU 使用率就已经很高了。响应时间由于被请求的处理被线程的上下文切换所打断而被拉长。
最佳线程池大小
上图描述的是 IO 等待型应用在 web 应用中创建了合理数量的线程的情况。CPU 得到了有效利用,具备良好的吞吐量和较少的线程上下文切换。我们可以看到由于更少的打断(上下文切换),请求处理更加有效,应用有一个良好的响应时间。
线程池隔离
对于大多数 web 应用而言,只有少数几种类型的 web 请求会花费比较长的处理时间。这些慢的请求处理可能会拖累所有线程,并降低整个应用的性能。处理这种问题的两个方案是:
- 为慢处理的 web 请求设置在一台独立的主机;
- 在同一个应用中为慢处理的 web 请求分配一个独立的线程池;