Tomcat工作原理

Tomcat 的总体结构:

Tomcat总体结构图

ConnectorContainer 是Tomcat 两个核心组件。Connector 主要负责对外交流Container 主要处理 Connector 接受的请求,主要是处理内部事务Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器

在使用tomcat时,经常会遇到连接数、线程数之类的配置问题 ,在此之前必须先了解Tomcat的连接器Connector

Connector的主要功能是接收客户端发送的TCP连接请求,创建Request和Response对象用于和请求端交换数据;然后产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

可以说,Servlet容器处理请求,是需要Connector进行调度和控制的,Connector是Tomcat处理请求的主干,因此Connector的配置和使用对Tomcat的性能有着重要的影响 。

Connector在处理HTTP请求时,会使用不同的protocol。 典型的protocol包括BIO、NIO和APR (Tomcat7中支持这3种,Tomcat8增加了对NIO2的支持,而到了Tomcat8.5和Tomcat9.0,则去掉了对BIO的支持)BIO是Blocking IO,顾名思义是阻塞的IO;NIO是Non-blocking IO,则是非阻塞的IO。而APR是Apache Portable Runtime,是Apache可移植运行库,利用本地库可以实现高可扩展性、高性能Apr是在Tomcat上运行高并发应用的首选模式,但是需要安装apr、apr-utils、tomcat-native等包

Connector使用哪种protocol,可以通过connector元素中的protocol属性进行指定 ,指定的protocol取值及对应的协议如下:

  • HTTP/1.1:默认值,使用的协议与Tomcat版本有关
  • org.apache.coyote.http11.Http11Protocol:BIO
  • org.apache.coyote.http11.Http11NioProtocol:NIO
  • org.apache.coyote.http11.Http11Nio2Protocol:NIO2
  • org.apache.coyote.http11.Http11AprProtocol:APR

Tomcat7自动选取使用BIO或APR(如果找到APR需要的本地库,则使用APR,否则使用BIO) ,Tomcat8自动选取使用NIO或APR(如果找到APR需要的本地库,则使用APR,否则使用NIO),在SpringBoot中可以通过如下方式制定Protocol:

1
2
3
4
5
6
7
8
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory =
new TomcatEmbeddedServletContainerFactory();
tomcatEmbeddedServletContainerFactory
.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
return tomcatEmbeddedServletContainerFactory;
}

BIO与NIO

无论是BIO,还是NIO,Connector处理请求的大致流程是一样的:

当客户端向服务器发送请求时,如果客户端与OS完成三次握手建立了连接,则OS将该连接放入accept队列Connector在accept队列中接收连接,在连接中获取请求的数据生成request;调用servlet容器处理请求;返回response。

在BIO实现的Connector中,处理请求的主要实体是JIoEndpoint对象。JIoEndpoint维护了AcceptorWorkerAcceptor接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞。其中Worker是Tomcat自带的线程池,如果通过Executor配置了其他线程池,原理与Worker类似。

在NIO实现的Connector中,处理请求的主要实体是NIoEndpoint对象。NIoEndpoint中除了包含Acceptor和Worker外,还是用了Poller,处理流程如下:

Tomcat NIO处理流程图

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模式的最主要区别。

关键参数acceptCount、maxConnections、maxThreads

acceptCount

accept队列的长度;当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。默认值是100。

maxConnections

Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时Acceptor线程不会读取accept队列中的连接;这时accept队列中的线程会一直阻塞着,直到Tomcat接收的连接数小于maxConnections。如果设置为-1,则连接数不受限制。

虽然tomcat同时可以处理的连接数目是maxConnections,但服务器中可以同时接收的连接数为maxConnections + acceptCount

默认值与连接器使用的协议有关:NIO的默认值是10000,APR/native的默认值是8192,而BIO的默认值为maxThreads(如果配置了Executor,则默认值是Executor的maxThreads)。

maxThreads

请求处理线程的最大数量,Tomcat7和8默认值都是200。如果该Connector绑定了Executor,这个值会被忽略,因为该Connector将使用绑定的Executor,而不是内置的线程池来执行任务。

实测数据

下面是对这几个参数的实测数据,测试的是同一个接口,接口固定sleep 10秒,最后两列是接口的平均响应时间(这里说所的同步异步是指在Controller层中接口是否使用Servlet3.0提供的异步HTTP请求):

acceptCount maxConnections maxThreads 并发 同时处理线程数 同步结果 异步结果
1 9 1 10 1 55263 11130
1 9 2 10 2 30044 11611
1 9 3 10 3 22329 11207
1 9 4 10 4 18027 11303
1 9 5 10 5 15412 11111
9 1 1 10 1线程1请求 55087 55324
9 1 2 10 2线程1请求 50261 55151
9 1 3 10 3线程1请求 55082 45471
9 1 4 10 4线程1请求 55276 55156
9 1 5 10 5线程1请求 55074 55314
9 3 1 10 同步1,异步1线程3请求 55289 22082
9 3 2 10 同步2,异步2线程3请求 30046 22266
9 3 3 10 3线程3请求 22235 22089
9 3 4 10 4线程3请求 22292 22034
9 3 5 10 5线程3请求 22249 22083

以上测试出的结果,可以很明显的看出当maxConnections设置为1时,accept队列中的线程会一直阻塞着,通过控制台也可以很明显得看出不论时同步还是异步请求,不论maxThreads设置为几个,处理请求的线程始终为一个。由此可看出tomcat中能够同时被处理的请求数是maxThreadsmaxConnections中的较小者

正在被处理的请求数量达到maxConnections时,再过来的请求会被接受(TCP连接建立成功),但是没有被处理,被阻塞到serversocket上,接受并被阻塞的socket请求数的最大值为acceptCount;当阻塞达到acceptCount的最大数目时,在发送过来的请求会建立连接refuse,对客户端而言,返回连接被拒绝的错误。

tomcat能接受的最大请求数量为maxConnections,加上acceptCount的数量,其中maxThreads和maxConnections中的较小者是正在被处理的请求数量,acceptCount为等待被处理的请求数量,超过这两者之和的请求会被拒绝。

对数据进行了Mbean监控发现,并发请求进来时先使用maxConnections缓冲队列,当maxConnections队列满了后,再使用acceptCount队列,当acceptCount队列满了后,则多的请求全部拒绝。

该使用的Tomcat配置如下,使用Jmeter做并发测试工具,并发量为300:

1
2
3
4
tomcat:
max-threads: 200
accept-count: 100
max-connections: 10000

300-sync-false-mbean