天天看点

一图搞懂Tomcat接收连接的处理过程

Tomcat由Connector和Container两部分组成,Connector负责接收连接,Container负责进行处理。其中管理连接的配置选项参数都属于Connector。本文结合Connector的三个配置参数                                                                                                             acceptCount, maxConnections, maxThreads来说明请求所经过的路径。

一图搞懂Tomcat接收连接的处理过程

请求经过路径图。(重要)

一图搞懂Tomcat接收连接的处理过程

我们从ServerSokcet说起,大家对下面这行代码都不陌生,服务器新建一个ServerSokcet。

ServerSocket server = new ServerSocket(8080,100);//绑定8888端口,设置等待队列长度为100
           

ServerSocket的第二个参数名为backlog,它的含义是建立连接但没有被accept()方法接受的请求个数,这个参数在tomcat里叫做acceptCount,对应上图中的等待队列的长度。下面是Tomcat初始化一个ServerSokcet的步骤,可以看见bind方法的第二个参数为getAcceptCount(),这个值默认为100.

public class EndPoint{ 
    ...
    protected void initServerSocket() throws Exception {
            serverSock = ServerSocketChannel.open();
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = new InetSocketAddress(getAddress(), 
             getPortWithOffset());
            serverSock.socket().bind(addr,getAcceptCount());//设置acceptCount的值默认为100
     }
     ...
} 
           

 如果没有被accept()方法接收的请求达到100的话,后续请求会被拒绝。如果等待队列未满,请求来到LimtLatch类,它跟java自带的Semaphore类似,他限制了accept()方法的接收数量,如下图所示,对应的配置变量为maxConnections,NIO模式默认为10000,只有经过它,请求才能到达ServerSocket.accept()方法。

一图搞懂Tomcat接收连接的处理过程

一个请求经过LimitLatch之后,就可以调用ServerSokcet.accept()方法了,Tomcat专门定义了一个Acceptor类来调用Socket的accept()方法。如下图所示,Acceptor默认线程数量为1。同时,如果一个请求经过了Accept,那么等待队列的个数就会减一。

public class Acceptor<U> implements Runnable {
    @Override
    public void run() {
        endpoint.countUpOrAwaitConnection();//LimitLatch,连接数超的话会阻塞.就走不到下面的accpet()了
        ...
        socket = endpoint.serverSocketAccept();
    }
   
    @Override
    protected SocketChannel serverSocketAccept() throws Exception {
        return serverSock.accept();
    }
    protected void countUpOrAwaitConnection() throws InterruptedException {
        if (maxConnections==-1) return;
        LimitLatch latch = connectionLimitLatch;
        if (latch!=null) latch.countUpOrAwait();  //可能阻塞,底层为java并发包的AQS
    }
}
           

NIO模式,需要选择器来选择就绪的事件。Tomcat为此定义了一个Poller类。run方法拿到就绪事件后传递给processKey方法,接着再调用processSocket方法,我们可以看到processSocket方法内部调用使用executor来处理这一任务的。这里的executor就是线程池了。

public class Poller implements Runnable {
        public void run() {             
             Iterator<SelectionKey> iterator =
                    keyCount > 0 ? selector.selectedKeys().iterator() : null;
             while (iterator != null && iterator.hasNext()) {//轮询就绪事件
                       ...
                       processKey(sk, attachment);//传递就绪事件
             }
         }

        protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
           ...
           processSocket(attachment, SocketEvent.OPEN_WRITE, true)//处理就绪事件
        }

    public boolean processSocket(SocketWrapperBase<S> socketWrapper,
       SocketEvent event, boolean dispatch) {
          executor.execute(sc);//线程池接收任务并处理
          ...
       }
}
           

Tomcat构建标准线程池语句如下,其中getMaxThreads的默认值为200,对应配置参数maxThreads。

executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 
               maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
           

请求走到这里还会经过很长的一段代码,由Adaptor传入容器进行处理,再返回给客户端,本文不做后续分析。我们来模拟一下Tomact是如何处理连接请求的。 假设acceptCount的值为100,maxConnections的值为1000,maxThreads个数为150。服务器启动,因为线程池还没有压力,请求一路顺畅经过等待队列,经过栅栏,经过accept到达执行线程池。此时等待队列的值为0,Socket.accept()方法每接收一个请求,连接数就会加一(该请求处理完会减一)。随着服务器压力增大,线程池就会满负荷工作,这时未处理请求都放在线程池的任务队列里,造成连接数持续增加,当连接数达到1000的时候,LimitLatch就会阻塞Acceptor线程,那么请求就会填入ServerSocket的等待队列。当等待队列达到100时,Acceptor仍然阻塞,服务器就会拒绝请求。线程池没有压力,LimitLatch就没有压力,LimitLatch没有压力,等待队列就没有压力。Tomcat就是这样将服务器压力缓慢上传,直到网络连接都满的情况下才拒绝连接,提高了服务器的可用性。建议大家多看文章开头的图,方便理解。

最后,本文截取的代码省略了很多内容,如果想看tomcat源码的同学可以直接搜索类名或者方法名。IDEA中使用CTRL+SHIFT+N可以直接定位类名。双击SHIFT可以搜索所有文件。