Tomcat NIO线程模型深入分析

0.566字数 2403阅读 3729

1.Tomcat总体架构


Tomcat有Connector和Container两大核心组件,Connector组件负责网络请求接入,Connector目前支持BIO、NIO、APR三种模式,后续文章会再重点对比下NIO和APR,Tomcat5以后的版本开始支持NIO了;Container组件实现了对servlet的容器管理功能;service服务将Connector和Container又包了一层,包装成外部可以获取的服务;多有service都运行在Tomcat这个大Server服务上,Server有所有service的实例,并实现了LifeCycle接口可以控制所有service的生命周期。

2.Tomcat NIO相关类

Tomcat的NIO实现主要是在Connector组件内,Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。


整个Connector组件包含三部分:Http11NioProtocol、Mapper、CoyoteAdapter。Http11NioProtocol包含NioEndpoint和Http11ConnectionHandler,NioEndpoint是Http11NioProtocol中负责接收处理socket的主要模块;Http11ConnectionHandler是连接处理器。NioEndpoint主要是实现了socket请求监听线程Acceptor、socket NIO poller线程、以及请求处理线程池。
NioEndpoint的内部处理流程为:


Acceptor 接收socket线程,这里虽然是基于NIO的connector,但是在接收socket方面还是传统的serverSocket.accept()方式,获得SocketChannel对象,然后封装在一个tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中。然后将NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入events queue里。这里是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过queue通信,Acceptor是events queue的生产者,Poller是events queue的消费者。
Poller Poller线程中维护了一个Selector对象,NIO就是基于Selector来完成逻辑的。在connector中并不止一个Selector,在socket的读写数据时,为了控制timeout也有一个Selector,在后面的BlockSelector中介绍。可以先把Poller线程中维护的这个Selector标为主Selector。 Poller是NIO实现的主要线程。首先作为events queue的消费者,从queue中取出PollerEvent对象,然后将此对象中的channel以OP_READ事件注册到主Selector中,然后主Selector执行select操作,遍历出可以读数据的socket,并从Worker线程池中拿到可用的Worker线程,然后将socket传递给Worker。整个过程是典型的NIO实现。
Worker Worker线程拿到Poller传过来的socket后,将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑,跟BIO实现一样。在Worker线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。在从socket中读数据和往socket中写数据的过程,并没有像典型的非阻塞的NIO的那样,注册OP_READ或OP_WRITE事件到主Selector,而是直接通过socket完成读写,这时是阻塞完成的,但是在timeout控制上,使用了NIO的Selector机制,但是这个Selector并不是Poller线程维护的主Selector,而是BlockPoller线程中维护的Selector,称之为辅Selector。
NioSelectorPool NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法。

3.请求处理流程

上面介绍了Tomcat的总体架构和涉及到NIO的相关工作类,下面从一个网络请求到Tomcat处理的过程直到业务servlet处理的过程,整体上说下一个网络请求的处理流程,下面借用网上的一张流程图,如果图片作者看到觉得侵权请下面留言,马上删掉:)



对于Acceptor监听到的Socket请求,经过NioEndpoint内部的NIO 线程模型处理后,会转变为SocketProcessor在Executor中运行,其在Run过程中会交给Http11ConnectionHandler处理,Http11ConnectionHandler会从ConcurrentHashMap<NioChannel,Http11NioProcessor>缓存中获取相应的Http11NioProcessor来继续处理,Http11NioProcessor主要是负责解析socket请求Header,解析完成后,会将Request、Response(这里的请求、响应在tomcat中看成是coyote的请求、响应,意思是还需要CoyoteAdaper处理)交给CoyoteAdaper继续处理,CoyoteAdaper这里的工作主要将socket解析的Request、Response转化为HttpServletRequest、HttpServletResponse,而这里的请求响应就是最后交给Container去处理。
同时我们可以看到Acceptor线程会将接受到的SocketChannel(一个socket请求)封装为PollerEvent放到Poller线程中的ConcurrentLinkedQueue<PollerEvent>缓存中,注意到这里的缓存是ConcurrentLinkedQueue是支持并发的,那么在Poller线程的内部,它只需要从这个缓存中不停地获取PollerEvent然后处理就可以了。最后Poller线程处理完成后会封装成SocketProcessor交给NioEndpoint内的线程池Executor去处理。线程池中的Work thread线程在处理SocketProcessor过程中,会调用Http11ConnectionHandler处理,而Http11ConnectionHandler则从ConcurrentHashMap<NioChannel,Http11NioProcessor>缓存中获取相应的Http11NioProcessor来继续处理,这里要注意的ConcurrentHashMap也是支持并发的。

4.NIO相关参数

一个或多个Acceptor线程,每个线程都有自己的Selector,Acceptor只负责accept新的连接,一旦连接建立之后就将连接注册到其他Worker线程中

多个Worker线程,有时候也叫IO线程,就是专门负责IO读写的。一种实现方式就是像Netty一样,每个Worker线程都有自己的Selector,可以负责多个连接的IO读写事件,每个连接归属于某个线程。另一种方式实现方式就是有专门的线程负责IO事件监听,这些线程有自己的Selector,一旦监听到有IO读写事件,并不是像第一种实现方式那样(自己去执行IO操作),而是将IO操作封装成一个Runnable交给Worker线程池来执行,这种情况每个连接可能会被多个线程同时操作,相比第一种并发性提高了,但是也可能引来多线程问题,在处理上要更加谨慎些。tomcat的NIO模型就是第二种。

所以一般参数就是Acceptor线程个数,Worker线程个数。
参考官方文档https://tomcat.apache.org/tomcat-8.5-doc/config/http.html?spm=5176.100239.blogcont39093.5.Vomyf0
参数主要有以下几个:
1)acceptCount
连接在被ServerSocketChannel accept之前就暂存在这个队列中,acceptCount就是这个队列的最大长度。ServerSocketChannel accept就是从这个队列中不断取出已经建立连接的的请求。所以当ServerSocketChannel accept取出不及时就有可能造成该队列积压,一旦满了连接就被拒绝了;
2)acceptorThreadCount
Acceptor线程只负责从上述队列中取出已经建立连接的请求。在启动的时候使用一个ServerSocketChannel监听一个连接端口如8080,可以有多个Acceptor线程并发不断调用上述ServerSocketChannel的accept方法来获取新的连接。参数acceptorThreadCount其实使用的Acceptor线程的个数;

  1. maxConnections
    这里就是tomcat对于连接数的一个控制,即最大连接数限制。一旦发现当前连接数已经超过了一定的数量(NIO默认是10000),上述的Acceptor线程就被阻塞了,即不再执行ServerSocketChannel的accept方法从队列中获取已经建立的连接。但是它并不阻止新的连接的建立,新的连接的建立过程不是Acceptor控制的,Acceptor仅仅是从队列中获取新建立的连接。所以当连接数已经超过maxConnections后,仍然是可以建立新的连接的,存放在上述acceptCount大小的队列中,这个队列里面的连接没有被Acceptor获取,就处于连接建立了但是不被处理的状态。当连接数低于maxConnections之后,Acceptor线程就不再阻塞,继续调用ServerSocketChannel的accept方法从acceptCount大小的队列中继续获取新的连接,之后就开始处理这些新的连接的IO事件了;
  2. maxThread
    专门处理IO的Worker数,默认是200;

这篇文章从tomcat的整体架构入手,分别介绍了tomcat中的NIO相关类,也介绍了一个网络请求在tomcat中的处理流程,最后介绍了一下tomcat中关键的几个参数对NIO线程模式的作用和影响,相信会对希望了解tomcat nio线程模型的同学会有所帮助。

推荐阅读更多精彩内容