tomcat在处理每个连接时,Acceptor
角色负责将socket上下文封装为一个任务SocketProcessor
然后提交给线程池处理。在BIO和APR模式下,每次有新请求时,会创建一个新的SocketProcessor
实例(在之前的tomcat对keep-alive的实现逻辑里也介绍过可以简单的通过SocketProcessor
与SocketWrapper
实例数对比socket的复用情况);而在NIO里,为了追求性能,对SocketProcessor
也做了cache,用完后将对象状态清空然后放入cache,下次有新的请求过来先从cache里获取对象,获取不到再创建一个新的。
这个cache是一个ConcurrentLinkedQueue
,默认最多可缓存500个对象(见SocketProperties
)。可以通过socket.processorCache
来设置这个缓存的大小,注意这个参数是NIO特有的。
接下来在SocketProcessor
执行过程中,真正的业务逻辑是通过一个org.apache.coyote.Processor
的接口来封装的,默认这个Processor
的实现是org.apache.coyote.http11.Http11Processor
。我们看一下SocketProcessor.process(...)
方法的大致逻辑:
public SocketState process(SocketWrapper<S> wrapper, SocketStatus status) {
...
// 针对长轮询或upgrade情况
Processor<S> processor = connections.get(socket);
...
if (processor == null) {
// 1) 尝试从回收队列里获取对象
processor = recycledProcessors.poll();
}
if (processor == null) {
// 2) 没有再创建新的
processor = createProcessor();
}
...
state = processor.process(wrapper);
...
release(wrapper, processor, ...);
...
return SocketState.CLOSED;
}
上面的方法是在AbstractProtocol
模板类里,所以BIO/APR/NIO都走这段逻辑,这里使用了一个回收队列来缓存Processor
,这个回收队列是ConcurrentLinkedQueue
的一个子类,队列的长度可通过server.xml里connector节点的processorCache
属性来设置,默认值是200,如果不做限制的话可以设置为-1,这样cache的上限将是最大连接数maxConnections
的大小。
在原有的一张ppt上加工了一下把这两个缓存队列所在位置标示了一下,图有点乱,重点是两个绿颜色的cache队列:
图中位于上面的socket.processorCache
队列是NIO独有的,下面的processorCache
是三种连接器都可以设置的。processorCache
这个参数在并发量比较大的情况下也蛮重要的,如果设置的太小,可能引起瓶颈。我们模拟一下,看看这个瓶颈是怎么回事。先修改server.xml里的connector节点,把processorCache
设置为0:
<Connector port="7001"
protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
processorCache="0"/>
启动tomcat后,使用ab模拟并发请求:
$ ab -n100000 -c10 http://localhost:7001/main
然后在ab的执行过程中立刻执行jstack观察堆栈信息,会发现一大半线程阻塞在AbstractConnectionHandler.register
或AbstractConnectionHandler.unregister
方法上:
"http-nio-7001-exec-11" #34 daemon prio=5 os_prio=31 tid=0x00007fd05ab05000 nid=0x8903 waiting for monitor entry [0x000000012b3b7000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.register(AbstractProtocol.java:746)
- waiting to lock <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:277)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:139)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679)
...
"http-nio-7001-exec-4" #27 daemon prio=5 os_prio=31 tid=0x00007fd0593e3000 nid=0x7b03 waiting for monitor entry [0x000000012aca2000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.unregister(AbstractProtocol.java:773)
- locked <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler)
at org.apache.coyote.AbstractProtocol$RecycledProcessors.offer(AbstractProtocol.java:820)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.release(Http11NioProtocol.java:219)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:690)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)
register
和unregister
分别是在创建和回收processor的时候调用的;看一下createProcessor
方法里的大致逻辑:
public Http11NioProcessor createProcessor() {
Http11NioProcessor processor = new Http11NioProcessor(...);
processor.setXXX(...);
...
// 这里,注册到jmx
register(processor);
return processor;
}
tomcat对jmx支持的非常好,运行时信息也有很多可以通过jmx获取,所以在每个新连接处理的时候,会在创建processor对象的时候注册一把,然后在processor处理完回收的时候再反注册一把;但这两个方法的实现都是同步的,同步的锁是一个全局的ConnectionHandler
对象,造成了多个线程会在这里串行。
绝大部分应用没有特别高的访问量,通常并不需要调整processorCache
参数,但对于网关或代理一类的应用(尤其是使用servlet3的情况)这个地方可以设置的大一些,比如调到1000或者-1。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/140489.html