深入浅出Tomcat网络通信的高并发处理机制
深入浅出Tomcat网络通信的高并发处理机制
随着互联网应用的快速发展,Web服务器面临的访问压力日益增大,如何高效处理高并发的网络请求成为关键。Tomcat作为Java世界中最受欢迎的Web容器之一,可以灵活选择不同的IO模型来处理网络通信,确保面对高并发的网络请求时能够快速处理。本文将深入探讨Tomcat中AbstractEndpoint的三种实现类(NioEndPoint、Nio2EndPoint、AprEndPoint)是如何处理网络通信的。
NioEndPoint
NioEndPoint将处理网络通信分离为三个步骤,分别使用三个组件进行执行:接收连接、检测IO事件、处理请求。
Acceptor
Acceptor用于接收连接(循环执行):使用LimitLatch限制最大连接数量,等待客户端完成TCP三次握手连接后,将连接交给Poller。
Poller
Poller用于检测IO事件是否就绪(循环执行):将连接注册到Selector上,使用Selector监听IO事件,当事件发生(读就绪)时交给Executor进行处理。
Executor
Executor池化管理线程,使用线程执行后续流程(解析请求、封装适配、交给容器处理...)。
Acceptor.run
Acceptor接收连接:
为了简化流程,只保留了较重要的流程:
- 使用LimitLatch限制连接数量,如果达到最大值则等待
- 等待获取客户端socket channel
- 把socket channel交给Poller处理
public void run() {
while (!stopCalled) {
//1.使用limit latch限制连接数量,如果达到最大值则等待
endpoint.countUpOrAwaitConnection();
U socket = null;
//2.等待获取客户端socket channel
socket = endpoint.serverSocketAccept();
//3.交给Poller处理
endpoint.setSocketOptions(socket);
}
}
Poller.run
Poller主要循环检测是否有IO事件发生,主要流程为:
- 轮询处理队列中的事件PollerEvent,比如将通道注册到Selector上
- Selector阻塞到事件发生或超时
- 迭代遍历处理事件,交给线程池处理
public void run() {
while (true) {
//1.轮询处理队列中的事件
events();
//2.select 阻塞直到事件发生
selector.select(selectorTimeout);
//3.迭代遍历处理事件
Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
//从附件中拿到连接的包装
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
//交给线程池处理
processKey(sk, socketWrapper);
}
}
}
}
Executor
线程池中的线程执行SocketProcessor时会去交给ProtocolHandler处理:
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
//封装SocketProcessor
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
//交给线程池执行
executor.execute(sc);
} else {
//当前线程执行(使用AIO时走这里)
sc.run();
}
}
Nio2EndPoint
Nio2EndPoint使用异步IO模型(AIO)来处理网络通信。AIO的特点就是异步,使用回调函数,当数据就绪时使用异步线程调用回调函数。
Nio2Acceptor
Nio2EndPoint中使用Nio2Acceptor接收连接,Nio2Acceptor继承Acceptor并实现回调接口CompletionHandler。
class Nio2Acceptor
extends Acceptor<AsynchronousSocketChannel>
implements CompletionHandler<AsynchronousSocketChannel,Void>
Nio2Acceptor.run
由于使用AIO,Nio2Acceptor在执行任务时不再需要循环,只需要携带回调函数,当客户端连接完成时触发回调。
public void run() {
if (!isPaused()) {
try {
//1.使用LimitLatch限制连接数
countUpOrAwaitConnection();
} catch (InterruptedException e) {
}
if (!isPaused()) {
//2.接收连接
serverSock.accept(null, this);
} else {
state = AcceptorState.PAUSED;
}
} else {
state = AcceptorState.PAUSED;
}
}
Nio2Acceptor.completed
回调成功的方法中主要做几件事:
- 是否限制连接数量
- 调用accept,方便接收下次连接
- 调用后续处理
public void completed(AsynchronousSocketChannel socket,Void attachment) {
errorDelay = 0;
if (isRunning() && !isPaused()) {
//1.是否限制连接数量
if (getMaxConnections() == -1) {
//不限制连接数量,方便接收下一次连接
serverSock.accept(null, this);
} else if (getConnectionCount() < getMaxConnections()) {
try {
//当前连接数小于最大限制连接数,不阻塞,主要是去自增计数
countUpOrAwaitConnection();
} catch (InterruptedException e) {
// Ignore
}
//方便接收下次连接
serverSock.accept(null, this);
} else {
//当前连接数大于等于最大限制连接数,再调用limitlatch会阻塞,为了避免阻塞使用线程池去执行(排队)
getExecutor().execute(this);
}
//setSocketOptions后续处理
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
if (isRunning()) {
state = AcceptorState.PAUSED;
}
destroySocket(socket);
}
}
Nio2SocketWrapper
当前线程是连接完成执行异步回调的线程,去执行SocketProcessor也就是会使用Processor解析数据,但此时数据可能还未准备好。
this.readCompletionHandler = new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer nBytes, ByteBuffer attachment) {
//...
getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_READ, false);
}
};
Http11Processor
在Processor处理HTTP协议的实现类Http11Processor中,执行service解析请求时,会先解析请求头parseRequestLine。
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException {
//...
inputBuffer.parseRequestLine(keptAlive, protocol.getConnectionTimeout(),protocol.getKeepAliveTimeout())
//...
}
AprEndPoint
APR(Apache Portable Runtime)是Apache提供的可移植运行库,是为了早期的Tomcat的提供高性能的。早期NIO还不成熟,使用APR通过JNI调用本地C语言实现的库,能够使用操作系统的epoll来实现多路复用模型,旨在提高高性能。
AprEndPoint在通道上使用的缓冲区是基于直接内存的(DirectByteBuffer),而NioEndPoint与Nio2EndPoint都是使用堆内存的(HeapByteBuffer)。使用直接内存的好处是能够减少数据拷贝带来的开销,但无法使用JVM来进行管理内存。并且AprEndPoint还能使用零拷贝sendfile,将数据从磁盘读到网卡发送时减少各种拷贝开销。
但在后来NIO、AIO逐渐成熟,AprEndPoint带来的好处逐渐被追平,在Tomcat 10时被遗弃。
总结
NioEndPoint将处理网络通信分为接收连接、监听事件、处理请求三个步骤。其中Acceptor负责接收连接,使用LimitLatch限制连接数量(若超过上限则等待),获取客户端连接NioChannel,包装为NioSocketWrapper,再封装为PollerEvent放入Poller的队列中。
Poller会轮询处理PollerEvent,通过PollerEvent拿到NioSocketWrapper将连接注册到Selector上,使用Selector监听事件,有事件触发时,从附件中获取连接的包装NioSocketWrapper,将其封装为SocketProcessor交给线程池处理。
线程池的线程处理SocketProcessor时,则会使用Processor解析协议,后续再封装请求/响应调用容器处理。
Nio2EndPoint 使用AIO,由内核监听事件(数据就绪)后使用异步线程执行回调。其中Nio2Acceptor继承Acceptor,接收连接不再循环处理,而是使用异步回调:当连接完成后再使用LimitLatch判断是否限制连接,调用非阻塞accept便于接收下次连接(回调),然后将客户端连接Nio2Channel封装为Nio2SocketWrapper再封装为SocketProcessor处理(后续调用processor无法解析,因为当前是连接完成的回调线程,数据还未就绪)。
当数据就绪时,通过Nio2SocketWrapper的回调继续封装为SocketProcessor向后处理(后续调用processor可以解析,因为当前为读数据就绪的回调线程,第二次读)。
早期的APR通过本地库、直接内存、零拷贝等多种方式进行性能优化。