Tomcat由Connector和Container两部分组成,Connector负责接收连接,Container负责进行处理。其中管理连接的配置选项参数都属于Connector。本文结合Connector的三个配置参数 acceptCount, maxConnections, maxThreads来说明请求所经过的路径。
请求经过路径图。(重要)
我们从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()方法。
一个请求经过LimitLatch之后,就可以调用ServerSokcet.accept()方法了,Tomcat专门定义了一个Acceptor类来调用Socket的accept()方法。如下图所示,Acceptor默认线程数量为1。同时,如果一个请求经过了Accept,那么等待队列的个数就会减一。
public class Acceptor 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 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 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可以搜索所有文件。