引用: https://yq.aliyun.com/articles/20177
引用:https://www.cnblogs.com/kismetv/p/7806063.html
Servlet 3.0
引入的若干重要新特性,包括异步处理
、新增的注解支持、可插性支持等等.
本文主要针对异步处理
进行分析。
在分析之前, 我们回顾下,tomcat请求处理过程.
tomcat处理请求
tomcat主要包含两部分内容:Connector(连接器)
和Container(容器)
。
- 首先:由Connector接收客户端请求,并将socket请求数据封装成
Request
和Response
,在封装完成之后交给Container
进行处理 - 然后:由Container:用于处理请求,并将处理结果返回给
Connector
- 最后:由Connector使用Socket将处理结果返回给客户端。
Connector 接收请求
Connector中具体是用ProtocolHandler
来处理请求的,不同的ProtocolHandler代表不同的连接类型。
Http11Protocol
: BIO,支持http1.1协议Http11NioProtocol
: NIO,支持http1.1协议Http11AprProtocol
: ARP(Apache portable runtime),支持http1.1协议
ProtocolHandler有三个非常重要的组件:Endpoint,Processor和Adapter.
Endpoint
: 用于处理底层Socket的网络协议。 Endpoint的抽象实现类AbstractEndpoint中定义了Acceptor,AsyncTimeout
两个内部类和一个接口HandlerAcceptor
: 用于监听请求。AsyncTimeout
: 用于异步检查request超时。Handler
: 用于处理接收到的Socket,在内部调用了Processor
Processor
: 用于将Endpoint接收到的Socket封装成RequestAdapter
: 用于将封装好的Request交给Containner进行具体处理。
BIO/NIO有何不同
引用:[https://www.cnblogs.com/kismetv/p/7806063.html]
无论是BIO,还是NIO,Connector处理请求的大致流程是一样的:
在accept队列中接收连接(当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列);在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response。
为了便于后面的说明,首先明确一下连接与请求的关系:
连接是TCP层面的(传输层),对应socket;
请求是HTTP层面的(应用层),必须依赖于TCP的连接实现;
一个TCP连接中可能传输多个HTTP请求。
BIO实现
在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了Acceptor和Worker:
Acceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。
其中Worker是Tomcat自带的线程池,如果通过<Executor>
配置了其他线程池,原理与Worker类似。
NIO实现
在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还使用了Poller,处理流程如下图所示:
Acceptor接收socket后,不是直接使用Worker中的线程处理请求,而是先将请求发送给了Poller,而Poller是实现NIO的关键。Acceptor向Poller发送请求通过队列实现,使用了典型的生产者-消费者模式。在Poller中,维护了一个Selector对象;当Poller从队列中取出socket后,注册到该Selector中;然后通过遍历Selector,找出其中可读的socket,并使用Worker中的线程处理相应请求。
与BIO类似,Worker也可以被自定义的线程池代替。
通过上述过程可以看出,在NIoEndpoint处理请求的过程中,无论是Acceptor接收socket,还是线程处理请求,使用的仍然是阻塞方式
;但在“读取socket并交给Worker中的线程”的这个过程中,使用非阻塞的NIO实现
,这是NIO模式与BIO模式的最主要区别。而这个区别,在并发量较大的情形下可以带来Tomcat效率的显著提升:
目前大多数HTTP请求使用的是长连接(HTTP/1.1默认keep-alive为true),而长连接意味着,一个TCP的socket在当前请求结束后,如果没有新的请求到来,socket不会立马释放,而是等timeout后再释放。
- 如果使用BIO,“读取socket并交给Worker中的线程”这个过程是阻塞的,也就意味着在socket等待下一个请求或等待释放的过程中,处理这个socket的工作线程会一直被占用,无法释放;因此Tomcat可以同时处理的socket数目不能超过最大线程数,性能受到了极大限制。
- 而使用NIO,“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程(selector选择可读的socket)
,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。
Containner处理请求
Containner处理请求的流程如下图:
3个参数:acceptCount、maxConnections、maxThreads
http://tomcat.apache.org/tomcat-7.0-doc/config/http.html
-
maxThreads(最大处理线程数): 用于处理用户请求的最大线程数,默认值是200。
如果配置了线程池,该配置则无效
. -
acceptCount(最大等待数): 当所有可能的请求处理线程都在使用时,传入连接请求的最大队列长度。 队列
已满时
收到的任何请求都将被拒绝。 默认值为100。 -
maxConnections(最大连接数):Tomcat在任意时刻
接收
和处理
的最大连接数。当Tomcat接收的连接数达到maxConnections
时,Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。般这个值要大于maxThreads+acceptCount。
minSpareThreads
:初始化时创建的线程数maxSpareThreads
:一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。
-
connectionTimeout (连接等待时间): 等待
Acceptor
接收请求的等待时间。默认20000ms
.
压力测试
BlockServlet
BlockServlet
每个业务请求会sleep(7s)
@WebServlet(urlPatterns = "/block")
public class BlockServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("进入Servlet的时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ".");
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
out.println("结束Servlet的时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + ".");
out.flush();
}
}
web.xml
使用metadata-complete="false"
开启servlet3.0对注解的支持。
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0" metadata-complete="false">
</web-app>
测试数据1
server.xml
:
<Connector port="8088" protocol="HTTP/1.1"
redirectPort="8443" URIEncoding="UTF-8"
acceptCount="5" maxThreads="4" connectionTimeout="2000"/>
jmeter
;
测试数据2
server.xml
调整acceptCount
和maxThreads
<Connector port="8088" protocol="HTTP/1.1"
redirectPort="8443" URIEncoding="UTF-8" acceptCount="10" maxThreads="8" connectionTimeout="2000"/>
jemeter
相应的将jmeter的线程调整值: