1 背景
我们在一个中间层的应用中使用了Servlet 3.0新增的异步特性。期望能提高应用的并发处理能力。不过在压力测试中发现,并发能力并没有相应的提高。
查看目标机器CPU,内存使用率都不高。看起来并没有发掘出机器的潜力。从现象来看,应该是连接数的限制,请求并没有到达处理线程。于是查看应用
容器Tomcat手册及配置说明,几个有趣的参数影响着系统的吞吐量。
2原理
2.1. 概要
Tomcat大致分为两个部分,Connector组件及Container组件。Connector组件负责控制入口连接,并关联着一个Executor。Container负责Servlet容器的实现,Executor负责具体的业务逻辑,如Servlet的执行。一个请求到达服务器后,经过以下关键几步,参见图1:
OS与客户端握手并建立连接,并将建立的连接放入完成队列,不妨叫Acceptor Queque。这个队列的长度就是Connector的acceptCount值。
Tomcat中的acceptor线程,不断从Acceptor Queque中获取连接。
Acceptor Queque队列中没有连接,Acceptor线程继续监视
Acceptor Queque队列中有新连接,Acceptor线程将检查当前的连接数是否超过了maxConnections
如果超过maxConnections,则阻塞。直到连接数小于maxConnections,acceptor线程将请求交由Executor负责执行。
Executor将分配worker线程来处理请求数据的读取,处理(servlet的执行)以及响应。
2.2. 参数
-
acceptCount
acceptCount 实际上是Bind Socket时候传递的backlog值,在linux平台下含义是已经建立连接还没有被应用获取的连接队列最大长度。此时,如果请求个数达到了acceptCount,新进的请求将抛出refuse connection.
-
maxConnections:
顾名思义,即Tomcat允许的同时存在的最大连接数。默认BIO模式下,这个数值等于maxThreads,即最大线程数。超过这个值后,acceptor线程被Block。新进入的连接将由acceptCount控制。当当前连接数小于这个数值时,acceptor线程被唤醒,新的连接才能进入Executor执行。
-
executor
JDK缺省的ThreadPoolExecutor的逻辑是:
如果线程数小于coreThreadSize, 优先建立新线程。
如果线程数大于coreThreadSize小于maxThreadSize,将请求入队列等待。
队列满的时候,才扩充线程数直到maxThreadSize.
当队列满,同时线程数大于maxThreadSize时,抛出异常。
Tomcat对JDK的ThreadPoolExecutor默认行为做了修改。使用继承自***队列的LinkedBlockingQueue的TaskQueue,并改写了其入队策略,是的tomcat的Executor具有以下特性。只要线程不足,并且小于maxThreads, 优先建立新线程。而不是超过coreSize,先入队列。
Executor如果发生reject,则将任务继续加入队列。而默认队列的size是Integer.MAX_VALUE,因此不会Reject.
在配置中,Executor可以单独配置,并被整个Tomcat共享。如果不配置,则Connector组件将使用自己默认的实现。
-
maxThreads
Worker线程的最大值。每当一个请求进来时,如果当前线程数小于这个值,则创建新的线程。如果大于这个值,则查找空闲线程。如果没有空闲线程,则被Block住。
-
protocol
可以有三个取值,BIO,NIO, APR。BIO使用阻塞Socket实现,一个连接一个线程的模型。NIO使用的时候非阻塞socket实现。APR是apache实现的一个夸平台的库,也是apache http服务以来的核心网络组件。
2.3. 总结
以上介绍的几个参数之间存在着微妙的关系。一方面可以限制过多的请求来保护系统资源,另一方面提供缓冲队列来提高系统的吞吐量。在低并发条件下,默认值基本满足要求。而在高并发的情况下,就需要根据具体的计算资源,评估以上参数的设置,来充分调动计算资源。
3 应用
3.1. 默认值的效果
背景中的case除了使用异步Servlet,后端依赖的服务也全部采用异步调用。即在tomcat的woker线程中,没有任何阻塞,只是做纯粹的本地CPU计算, 为了模拟服务失败的情况,后端服务被mock住,并随机sleep 0~3s。初次使用了tomcat的默认设置(具体值参见后表中第1条)。由于后端woker线程很快(发送异步请求后就结束了),一个线程在1s内可以处理多于一个的请求。此时,如果maxConnections等于maxThreads 的值,很显然不能完全激活全部工作线程。如图2:
300个线程,只能达到140左右的吞吐量,tomcat worker线程只有17个上下,平均响应时间已经2s多了(理论上正常平均应该1.5s),继续增加线程,返回异常,说明已经达到了极限值。可以理解为,在异步情况下,20个worker线程每秒就能处理140个请求。
3.2. maxConnection的应用
为了将worker线程打满,同时对后端的异步服务有足够的信心,逐步将maxConnection调整到2000,使用2000个并发打压。此时200个worker线程都工作了。吞吐量已经达到了1308(如图3),此时应该是应用最大吞吐量了。至此,初步达到了预期效果。
3.3. NIO Connector的应用
还有个NIO Connector,看到名字就是支持异步的IO了,在其它参数不变的情况下,换成NIO Connector。如图4所示,在吞吐量基本不变的情况下,线程数基本减少了一半。
在启用NIO Connector,servlet及后端调用不异步的话,如图所示5,显然吞吐量上不去,而且还有所下降。
4 总结
为了充分发挥tomcat的潜能,需要综合评估这几个重要参数。当worker线程处理足够快的时候,可以适当提高maxConnections值,以便更多的请求得到处理。
反之,如果后端线程处理较慢,则可考虑减少maxConnections及QueueSize,避免请求堆积而造成请求超时。另外NIO能更高效处理网络连接及请求。在全栈异步的情况下,能有效减少Worker线程数。