一、NIO模型
Tomcat7以后,默认的IO模型就是NIO了,但是它的实现跟常用的Reactor模型略有不同,具体设计如下:
可以看出多了一个BlockPoller 的设计,这是因为在Servlet规范中ServletInputStream和ServletOutputStream是阻塞的,所以请求体和响应体的读取和发送需要阻塞处理。请求行读取和SSL握手使用非阻塞的Poller处理。具体的架构如下图所示:
从上面图我们可以发现整体架构运行着4种线程:
1、Acceptor线程
这个线程主要监听端口,当有请求过来的时候,完成 tcp 三次握手,将 accept 过来的 socket 注册OP_REGISTER 事件,并将该事件提交到 Poller 线程的事件队列 PollerEventQueue中。
2、Poller线程
1)在 tomcat NIO 架构中会有 poller 线程,在 tomcat8 及以前的版本之中,可以通过 pollerThreadCount 配置 poller thread 的数目,但是在 tomcat 9.0.21 中 poller thread 数目始终会为 1。
2)poller thread 的核心功能主要由Poller 类来实现,对于每一个 poller 实例都有一个 NIO selector 实例,同时也有一个事件队列SynchronizedQueue<PollerEvent>。
3)每一个 poller thread,都会去轮询队列SynchronizedQueue<PollerEvent>,该队列的事件由 acceptor 线程(监听到新连接时)或者 tomcat io 线程(处理完请求之后保持长连接,添加读事件)放入,然后根据不同的事件对原始socket注册相应的读写事件。
4)每一个poller thread,会调用 java NIO 对象 selector,发起系统调用,来监测原始 scoket 是否有读写事件发生,如果有则将原始 scoket 的封装对象交由 tomcat io 线程处理。
3、Tomcat IO线程
1)tomcat io 线程是一个线程池,线程池大小可由 tomcat 相关参数配置。
2)tomcat io 线程会接受 poller thread 传入的 scoket 封装对象后,依次调用SocketProcessor/ConnectionHanlder(global instance)/Http11Processor/CoyoteAdapter,最后交由 servlet container 完成servlet API 的调用。
3)对于request header 的解析,request body 的获取,servlet API 的调用,response 数据的写入发送,这一系列过程都是在 tomcat io 线程中完成的。
4)对于request header,request body 有时候需要多次读取操作,这个时候 tomcat io 线程会将原始 socket 再次进行读事件的注册,并交由 BlockPoller 线程处理,然后在readLatch (CountDownLatch) 对象上等待。
5)对于 response data 有时候不可写,比如原始 scoket 的发送缓冲区满了,这个时候 tomcat io 线程同样会将原始 socket 再次注册写事件,并交由 BlockPoller 线程处理,然后在writeLatch 对象上等待。
4、BlockPoller线程
1)tomcat NIO 架构中会有 block poller 线程,其核心功能由BlockPoller 类来实现,BlockPoller 实例会有一个 NIO selector 实例,同时也会拥有一个自己的事件队列实例SynchronizedQueue<Runnable>。
2)对于BlockPoller thread来说, 会去轮询队列 SynchronizedQueue<Runnable>,该队列的对象(RunnableAdd类型)由 tomcat io 线程放入,然后根据不同的对象对原始 socket 注册相应的读写事件。
3)对于BlockPoller thread来说, 会调用 java NIO 对象 selector,发起系统调用,来监测原始 scoket 是否有读写事件发生。如果有,则在 readLatch 或者 writeLatch 对象上等待 tomcat io 线程唤醒,然后 tomcat io 线程继续完成读写操作。
二、请求处理流程
以tomcat6为例简化类的包装过程:
1、Acceptor 接收 TCP 连接,并将其注册到 Poller 上
2、Poller 发现通道有就绪的 I/O 事件,将事件分配给线程池中的线程处理
3、线程池线程首先在 Poller 上非阻塞完成请求行和 SSL 握手的处理,然后通过容器调用 Servlet,生成响应,最后如果需要读取请求体或者发送响应,那就会将通道注册到 BlockPoller 上模拟阻塞完成。
相关组件的功能如下:
- Acceptor: 阻塞监听和接收通道连接
- Poller: 事件多路复用器,通知 I/O 事件的发生并分配合适的处理器
- PollerEvent: 是对通道、SelectionKey 的附加对象和通道关注事件的封装
- SocketProcessor: 线程池调度单元,它处理 SSL 握手,调用 Handler 解析协议
- Handler: 通道处理接口,用于适配多种协议,如 HTTP、AJP
- NioEndpoint: 服务启停初始化的入口
- NioSelectorPool: 提供一个阻塞读写使用的 Selector 池和一个单例 Selector
- NioBlockingSelector: 提供阻塞读和写的方法
- BlockPoller: 与 NioBlockingSelector 配合完成模拟阻塞
- NioChannel: 封装 SocketChannel 和读写使用的 ByteBuffer
- KeyAttachment: Key 的附加对象,它包含通道最后访问时间和用于模拟阻塞使用的读写闭锁
三、源码分析
1、 Acceptor注册通道到Poller上
通过启动流程源码分析,我们知道Acceptor线程会阻塞在serverSock.accept()方法上,当有客户端发送请求连接时,会接着往下执行:
public void run() {
int errorDelay = 0;
// Loop until we receive a shutdown command
while (running) {
···
state = AcceptorState.RUNNING;
try {
···
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
if (running && !paused) {
//关键代码
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
closeSocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
setSocketOptions(SocketChannel socket)
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
//设置为非阻塞
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);
//第一次为null
NioChannel channel = nioChannels.pop();
if (channel == null) {
//buffer处理器
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
//Https的请求会创建SecureNioChannel
if (isSSLEnabled()) {
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
//将socketchannel包装成niochannel
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset();
}
//往poller线程提交NioChannel
getPoller0().register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error("",t);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
// Tell to close the socket
return false;
}
return true;
}
public void register(final NioChannel socket) {
//关联此 Poller
socket.setPoller(this);
NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
socket.setSocketWrapper(ka);
ka.setPoller(this);
ka.setReadTimeout(getSocketProperties().getSoTimeout());
ka.setWriteTimeout(getSocketProperties().getSoTimeout());
ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
ka.setReadTimeout(getConnectionTimeout());
ka.setWriteTimeout(getConnectionTimeout());
PollerEvent r = eventCache.pop();
// 声明此通道关注的事件
ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
if ( r==null) {
r = new PollerEvent(socket,ka,OP_REGISTER);
} else {
r.reset(socket,ka,OP_REGISTER);
}
// 加入到 Poller 的 events 队列中
addEvent(r);
}
private void addEvent(PollerEvent event) {
// 插入队列
events.offer(event);
if (wakeupCounter.incrementAndGet() == 0) {
// 唤醒 Selector
selector.wakeup();
}
}
Poller线程的唤醒
Poler线程在初始化时就已经启动了,run方法里有个死循环一直在监听是否有新的事件。
//Poller线程
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
try {
if (!close) {
//遍历事件处理
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// wakeupCounter 大于0,意味着 Acceptor 接收了大量连接,产生大量 PollerEvent 急
// 需 Poller 消费处理,此时进行一次非阻塞调用
keyCount = selector.selectNow();
} else {
// wakeupCounter 等于0,阻塞等待 IO 事件发生或被唤醒
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error("",x);
continue;
}
// Either we timed out or we woke up, process events first
if (keyCount == 0) {
hasEvents = (hasEvents | events());
}
//
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// Walk through the collection of ready keys and dispatch
// any active event.
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (socketWrapper != null) {
//socket
processKey(sk, socketWrapper);
}
}
// Process timeouts
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}
wakeupCounter 计数器变量值的初始为 0,wakeupCounter.getAndSet(-1) 调用之后其实际内存中值为 -1,方法的返回值依然为原始 0,所以进入到 else 分支中。
在 else 分支中 selector.select(timeout) 会被调用,因为没有为队列中的原始 socket 注册可读可写事件,所以 poller 线程会阻塞,放弃对 cpu 的使用,一直到超时。
wakeupCounter.set(0) 被调用,将指设置回原始 0。后面如果还是没有对原始socket 注册可读可写事件,依然循环上面的步骤。
selectorTimeout 的默认值为 1000 毫秒,即会阻塞 poller 线程 1 秒钟,然后进入下一个循环。
poller 线程的唤醒由 poller 实例的 addEvent() 方法实现,其间接的被 acceptor 线程通过 poller.register() 调用 trigger 的,所以是 acceptor 线程完成对 poller 线程的唤醒。
对于 acceptor 线程:
1. 间接调用该方法把事件放入 poller 队列里,并会调用wakeupCounter.incrementAndGet() 方法。
2. 根据上面"poller线程的阻塞"部分的分析,当 poller 阻塞的时候,wakeupCounter 的值为-1。这里通过调用 incrementAndGet() 方法加1,使其值变为 0,然后调用 selector.wakeup() 唤醒处于阻塞状态的 poller 线程。
对于 poller 线程:
1. poller 线程被 acceptor 线程唤醒之后继续执行 run() 方法,run() 方法会间接调用 events() 方法。根据上一篇文章,event() 方法会对事件队列中关联的所有原始 socket 对象注册读事件。然后根据调用 wakeupCounter.getAndSet() 的返回值来调用 selector 实例的 select() 方法来看是否有可读事件发生,这样就完成了 poller 的唤醒。
2. 如果同时并发多个连接,acceptor 多次把事件放入队列,那么wakeupCounter 的值一定大于 0,wakeupCounter.getAndSet(-1) 的返回值也大于 0,就说明一定有连接了,poller 会在下一个循环里调用select.selectNow() 非阻塞方法来检查可读事件是否发生。
3. 对于 poller 来说,如果被唤醒了调用 selector的 selectNow() 或select(timeout) 也不一定获得事件。因为注册的是可读事件,事件的发生还是靠 client 端把数据发送过来。之所以要唤醒 poller 线程,是因为有 client 的连接建立好了,正常情况下 client 端在建立好连接之后应该会发送数据,就有数据可读的可能性。所以马上唤醒 poller 线程,来用 selector 监测是否有读事件发生。
Tomcat 正是通过以上 poller 线程的阻塞与唤醒的设计,最大程度的避免了 poller 线程对 cpu 的占用,同时又在有 client 连接 ready 的时候唤醒 poller 线程去监测 client 连接数据的可读性。
2、对原始socket注册事件
public boolean events() {
boolean result = false;
PollerEvent pe = null;
// 遍历事件队列
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
result = true;
try {
// 本质调用的是 PollerEvent.run()
pe.run();
// 重置并缓存 PollerEvent 对象
pe.reset();
if (running && !paused) {
eventCache.push(pe);
}
} catch ( Throwable x ) {
log.error("",x);
}
}
return result;
}
public static class PollerEvent implements Runnable {
private NioChannel socket;
private int interestOps;
private NioSocketWrapper socketWrapper;
@Override
public void run() {
if (interestOps == OP_REGISTER) {
try {
// 将 SocketChannel 注册到 Poller 的 Selector 上并指定关注的事件和附加对象
socket.getIOChannel().register(
socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {
log.error(sm.getString("endpoint.nio.registerFail"), x);
}
} else {
final SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
if (key == null) {
// The key was cancelled (e.g. due to socket closure)
// and removed from the selector while it was being
// processed. Count down the connections at this point
// since it won't have been counted down when the socket
// closed.
socket.socketWrapper.getEndpoint().countDownConnection();
((NioSocketWrapper) socket.socketWrapper).closed = true;
} else {
final NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment();
if (socketWrapper != null) {
//we are registering the key to start with, reset the fairness counter.
int ops = key.interestOps() | interestOps;
socketWrapper.interestOps(ops);
key.interestOps(ops);
} else {
socket.getPoller().cancelledKey(key);
}
}
} catch (CancelledKeyException ckx) {
try {
socket.getPoller().cancelledKey(key);
} catch (Exception ignore) {}
}
}
}
}
PollerEvent 的 run() 方法会根据不同的 event 类型对原始 socket 去注册事件,当 event 为 OP_REGISTER 时,会利用 java NIO API 对原始 socket 去注册 OP_READ 事件,如果不是 OP_REGISTER 则追加当前的 event 事件。在 acceptor 线程中建立连接之后注册到 Poller 线程中的事件就是OP_REGISTER , 所以是 acceptor 线程建立原始 scoket 连接,然后 Poller 线程对原始 socket 注册 OP_READ 事件。
3、processKey(sk, socketWrapper);
protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
try {
if (close) {
cancelledKey(sk);
} else if ( sk.isValid() && attachment != null ) {
if (sk.isReadable() || sk.isWritable() ) {
if ( attachment.getSendfileData() != null ) {
processSendfile(sk,attachment, false);
} else {
unreg(sk, attachment, sk.readyOps());
boolean closeSocket = false;
// Read goes before write
if (sk.isReadable()) {
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
if (closeSocket) {
cancelledKey(sk);
}
}
}
} else {
// Invalid key
cancelledKey(sk);
}
} catch (CancelledKeyException ckx) {
cancelledKey(sk);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("",t);
}
}
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
return false;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This means we got an OOM or similar creating a thread, or that
// the pool and its queue are full
getLog().error(sm.getString("endpoint.process.fail"), t);
return false;
}
return true;
}
public abstract class SocketProcessorBase<S> implements Runnable {
@Override
public final void run() {
synchronized (socketWrapper) {
// It is possible that processing may be triggered for read and
// write at the same time. The sync above makes sure that processing
// does not occur in parallel. The test below ensures that if the
// first event to be processed results in the socket being closed,
// the subsequent events are not processed.
if (socketWrapper.isClosed()) {
return;
}
doRun();
}
}
}
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
@Override
protected void doRun() {
NioChannel socket = socketWrapper.getSocket();
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
int handshake = -1;
try {
if (key != null) {
if (socket.isHandshakeComplete()) {
// No TLS handshaking required. Let the handler
// process this socket / event combination.
handshake = 0;
} else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
event == SocketEvent.ERROR) {
// Unable to complete the TLS handshake. Treat it as
// if the handshake failed.
handshake = -1;
} else {
handshake = socket.handshake(key.isReadable(), key.isWritable());
// The handshake process reads/writes from/to the
// socket. status may therefore be OPEN_WRITE once
// the handshake completes. However, the handshake
// happens when the socket is opened so the status
// must always be OPEN_READ after it completes. It
// is OK to always set this as it is only used if
// the handshake completes.
event = SocketEvent.OPEN_READ;
}
}
} catch (IOException x) {
handshake = -1;
if (log.isDebugEnabled()) {
log.debug("Error during SSL handshake",x);
}
} catch (CancelledKeyException ckx) {
handshake = -1;
}
if (handshake == 0) {
SocketState state = SocketState.OPEN;
// Process the request from this socket
if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
state = getHandler().process(socketWrapper, event);
}
if (state == SocketState.CLOSED) {
close(socket, key);
}
} else if (handshake == -1 ) {
getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
close(socket, key);
} else if (handshake == SelectionKey.OP_READ){
socketWrapper.registerReadInterest();
} else if (handshake == SelectionKey.OP_WRITE){
socketWrapper.registerWriteInterest();
}
} catch (CancelledKeyException cx) {
socket.getPoller().cancelledKey(key);
} catch (VirtualMachineError vme) {
ExceptionUtils.handleThrowable(vme);
} catch (Throwable t) {
log.error("", t);
socket.getPoller().cancelledKey(key);
} finally {
socketWrapper = null;
event = null;
//return to cache
if (running && !paused) {
processorCache.push(this);
}
}
}
}
}
如果 selector 有检测到事件发生,则对所有发生的事件调用 poller 的 processKey() 方法。
在每次循环的最后调用 poller 的 timeout() 方法处理是否超时。
对于 poller 实例的 processKey() 方法会去检查 SelectionKey 的合理性,在可读可写的基础上去调用 AbstractEndpoint 的 processSocket() 方法。
对于 AbstractEndpoint 具体实现类对象之中的 processSocket() 方法,会利用 tomcat 的 io 线程池(通过调用getExecutor方法得到线程池)来运行SocketProcessorBase对象实例的 run() 方法。
也就是说把SocketWrapper 对象实例委托给 SocketProcessorBase的run() 方法,最终调用SocketProcessor的doRun()方法。