本文前半部分结论存在严重错误,请看最后2015-1-20更新部分

最近一直在解决线上一个问题,表现是:

服务器性能很好,Tomcat版本是7.0.54,配置如下:

 <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="3000" minSpareThreads="800"/> <Connector executor="tomcatThreadPool" port="8084" protocol="org.apache.coyote.http11.Http11AprProtocol"
connectionTimeout="60000"
keepAliveTimeout="30000"
maxKeepAliveRequests="8000"
maxHttpHeaderSize="8192"
URIEncoding="UTF-8"
enableLookups="false"
acceptCount="1000"
disableUploadTimeout="true"
redirectPort="8443" />

事后thread dump看其实真正处于RUNNABLE状态的线程很少,绝大部分线程都处于TIMED_WAITING状态:

聊下并发和Tomcat线程数(错误更正)-LMLPHP

于是大伙都开始纠结为什么线程会涨到3000,而且发现即使峰值过了线程数并不会降下来。

我们首先想到的是:

但是优化一个版本上线后发现虽然涨的情况有所好转,但是最终线程池还是会达到3000这个最大值。

==================================分割线=========================================

以上是大背景,中间的过程省略,直接跟各位说下目前我得到的结论:

1、首先是为什么线程不释放的问题?

简单说下我验证的Tomcat(7.0.54)线程池大概的工作机制

  • Tomcat启动时如果没有请求过来,那么线程数(都是指线程池的)为0;
  • 一旦有请求,Tomcat会初始化minSapreThreads设置的线程数;
  • Tomcat不会主动对线程池进行收缩,除非确定没有任何请求的时候,Tomcat才会将线程池收缩到minSpareThreads设置的大小;
  • Tomcat6之前的版本有一个maxSpareThreads参数,但是在7中已经移除了,所以只要前面哪怕只有一个请求,Tomcat也不会释放多于空闲的线程。

至于Tomcat为什么移除maxSpareThreads这个参数,我想也是出于性能的考虑,不停的收缩线程池性能肯定不高,而多余的线程处于等待状态的好处是一有新请求过来立刻可以处理。

  • 而且大量的Tomcat线程处于等待状态不会消耗CPU,但是会消耗一些JVM存储。

补充:上面标红的一句有点问题,进一步验证发现只有使用Keep-Alive(客户端和服务端都支持)时才是这种表现,如果客户端没有使用Keep-Alive那么线程会随着TCP连接的释放而回收。

Tomcat中Keep-Alive相关的参数:

maxKeepAliveRequests:

keepAliveTimeout:

2、为什么线程池会满?

这是我现在纠结的核心。到底是不是应用的性能慢导致的,我现在的结论是有关系,但关键是并发。

  • Tomcat的线程池的线程数跟你的瞬间并发有关系,比如maxThreads设置为1000,当瞬间并发达到1000那么Tomcat就会起1000个线程来处理,这时候跟你应用的快慢关系不大。

那么是不是并发多少Tomcat就会起多少个线程呢?这里还跟Tomcat的这几个参数设置有关系,看官方的解释是最靠谱的:

maxThreads:

maxConnections:

acceptCount:

minSpareThreads:

我简单理解就是:

maxThreads:Tomcat线程池最多能起的线程数;

maxConnections:Tomcat最多能并发处理的请求(连接);

acceptCount:Tomcat维护最大的对列数;

minSpareThreads:Tomcat初始化的线程池大小或者说Tomcat线程池最少会有这么多线程。

比较容易弄混的是maxThreads和maxConnections这两个参数:

maxThreads是指Tomcat线程池做多能起的线程数,而maxConnections则是Tomcat一瞬间做多能够处理的并发连接数。比如maxThreads=1000,maxConnections=800,假设某一瞬间的并发时1000,那么最终Tomcat的线程数将会是800,即同时处理800个请求,剩余200进入队列“排队”,如果acceptCount=100,那么有100个请求会被拒掉。
注意:根据前面所说,只是并发那一瞬间Tomcat会起800个线程处理请求,但是稳定后,某一瞬间可能只有很少的线程处于RUNNABLE状态,大部分线程是TIMED_WAITING,如果你的应用处理时间够快的话。所以真正决定Tomcat最大可能达到的线程数是maxConnections这个参数和并发数,当并发数超过这个参数则请求会排队,这时响应的快慢就看你的程序性能了。

以上的结论都是我个人验证和总结,如有不对,跪求指正!!!

==========================更新(2015-1-20)===========================

以上的得出结论有严重的问题,特此更正下,如果误导了某些同学十分抱歉。

主要错误的结论是:

  • Tomcat不会主动对线程池进行收缩,除非确定没有任何请求的时候,Tomcat才会将线程池收缩到minSpareThreads设置的大小;
  • Tomcat6之前的版本有一个maxSpareThreads参数,但是在7中已经移除了,所以只要前面哪怕只有一个请求,Tomcat也不会释放多于空闲的线程。

Tomcat会停止长时间闲置的线程。Tomcat还有一个参数叫maxIdleTime

其实从这个参数解释也能看出来Tomcat会停止闲置了超过一定时间的线程的,这个时间就是maxIdleTime。但我之前的测试中确实没有发现线程释放的现象,这是为什么呢?我发现除了这个参数线程池线程是否释放?释放多少?还跟当前Tomcat每秒处理的请求数(从Jmeter或LoadRunner来看可以理解为TPS)有关系。通过下表可以清晰的看出来线程数TPSmaxIdleTime之间的关系:

TPS maxIdleTime(ms)Thread Count
1060,000600    
560,000300
160,00060

依次类推,当然Thread Count这一列是一个大约数,上下相差几个,但基本符合这样一个规则:

当然这个Thread Count不会小于minSpareThreads,这个跟之前的结论还是一样的。我现在大胆猜测下(回头看源码验证下,或者哪位同学知道告诉我下,谢谢):

Tomcat线程池每次从队列头部取线程去处理请求,请求完结束后再放到队列尾部,也就是说前后两次请求处理不会用同一个线程。某个线程闲置超过maxIdleTime就释放掉。

聊下并发和Tomcat线程数(错误更正)-LMLPHP

假设首先线程池在高峰时期暴涨到1000,高峰过后Tomcat处理一次请求需要1s(从Jmeter看TPS大约就为1),那么在maxIdleTime默认的60s内会用到线程池中60个线程,那么最后理论上线程池会收缩到60(假设minSpareThreads大于60)。另外:这个跟用不用Keep-Alive没关系(之前测试结论是因为用了Keep-Alive导致程序性能下降,TPS降低了很多导致的)

这就是为什么我之前的测试中、还有我们生产环境中线程数只增不减的原因,因为就算峰值过后我们的业务每秒请求次数仍然有100多,100*60=6000,也就是3000个线程每个线程在被回收之前肯定会被重用。

那么现在有另外一个问题,那么正常情况下为什么每秒100次的请求不会导致线程数暴增呢?也就是说线程暴增到3000的瓶颈到底在哪?这个我上面的结论其实也不是很准确。

真正决定Tomcat最大可能达到的线程数是maxConnections这个参数和并发数,当并发数超过这个参数则请求会排队,这时响应的快慢就看你的程序性能了。

这里没说清楚的是并发的概念,不管什么并发肯定是有一个时间单位的(一般是1s),准确的来讲应该是当时Tomcat处理一个请求的时间内并发数,比如当时Tomcat处理某一个请求花费了1s,那么如果这1s过来的请求数达到了3000,那么Tomcat的线程数就会为3000,maxConnections只是Tomcat做的一个限制。

欢迎斧正!

补充:

使用Jmeter可以很容易的控制请求的频率。

聊下并发和Tomcat线程数(错误更正)-LMLPHP

聊下并发和Tomcat线程数(错误更正)-LMLPHP

05-07 13:46
查看更多