注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….
上一篇文章中,我们分析了Connector的启动流程,本文将来分析tomcat对连接请求的处理。
1. 处理服务端连接:Acceptor#run
通过上一篇文章中,我们了解到,tomcat会启动一个单独的Acceptor线程来处理服务端的连接请求,Acceptor#run如下:
public class Acceptor<U> implements Runnable {
/** 类型是 NioEndpoint */
private final AbstractEndpoint<?,U> endpoint;
...
public Acceptor(AbstractEndpoint<?,U> endpoint) {
this.endpoint = endpoint;
}
@Override
public void run() {
int errorDelay = 0;
try {
while (!stopCalled) {
...
try {
...
U socket = null;
try {
// 接收服务端连接
socket = endpoint.serverSocketAccept();
} catch (Exception ioe) {
endpoint.countDownConnection();
if (endpoint.isRunning()) {
errorDelay = handleExceptionWithDelay(errorDelay);
throw ioe;
} else {
break;
}
}
errorDelay = 0;
if (!stopCalled && !endpoint.isPaused()) {
// 手动设置socket属性
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
} else {
endpoint.destroySocket(socket);
}
} catch (Throwable t) {
...
}
}
} finally {
stopLatch.countDown();
}
state = AcceptorState.ENDED;
}
...
}
这个方法有点长,不过大多逻辑是在处理一些判断,关键的操作就两个:
endpoint.serverSocketAccept():接收服务端连接endpoint.setSocketOptions(socket):手动设置socket属性
1.1 接收服务端连接
我们来看看它是怎么获取到一个连接的,进入NioEndpoint#serverSocketAccept方法:
protected SocketChannel serverSocketAccept() throws Exception {
return serverSock.accept();
}
这里调用的是ServerSocketChannel#accept方法来获取服务端连接,这个方法是jdk提供的,需要注意的是,这个方法是阻塞的,也就是说,tomcat会一直在这等着,直到有新的服务端连接进来才会被唤醒,因此Acceptor是阻塞的。
1.2 设置socket属性
我们再来看看手动设置socket属性的操作,也就是NioEndpoint#setSocketOptions方法:
protected boolean setSocketOptions(SocketChannel socket) {
// 将 socketChannel 包装为 NioSocketWrapper
NioSocketWrapper socketWrapper = null;
try {
// 包装socket为NioSocketWrapper的过程,具体过程我们并不关心
// 热爱 nio 网络编程的小伙伴可对照源码自行分析
...
// 手动注册事件
poller.register(socketWrapper);
return true;
} catch (Throwable t) {
...
}
return false;
}
这个方法所做的工作就是将SocketChannel包装为NioSocketWrapper,然后将其注册到poller上,我们继续进入其注册方法NioEndpoint.Poller#register,一探究竟。
1.3 处理socketWrapper注册:NioEndpoint.Poller#register
public void register(final NioSocketWrapper socketWrapper) {
// 表示 NioSocketWrapper 是读取操作
socketWrapper.interestOps(SelectionKey.OP_READ);
// 将 NioSocketWrapper 包装为 PollerEvent
PollerEvent event = null;
if (eventCache != null) {
event = eventCache.pop();
}
// 表示事件需要注册
if (event == null) {
event = new PollerEvent(socketWrapper, OP_REGISTER);
} else {
event.reset(socketWrapper, OP_REGISTER);
}
// 添加事件
addEvent(event);
}
这个方法会将传入的NioSocketWrapper包装为PollerEvent,然后进行添加事件操作,我们继续进入NioEndpoint.Poller#addEvent方法:
// 存放 PollerEvent 的结构
private final SynchronizedQueue<PollerEvent> events =
new SynchronizedQueue<>();
private void addEvent(PollerEvent event) {
events.offer(event);
if (wakeupCounter.incrementAndGet() == 0) {
// 唤醒 selector
selector.wakeup();
}
}
最终,只是把PollerEvent添加到了一个SynchronizedQueue结构中,然后就唤醒了selector。
至此,Acceptor#run所做的工作就结束了。
2. NioEndpoint.Poller#run
在Acceptor#run方法的最后,PollerEvent被添加到了一个SynchronizedQueue结构中,并且还唤醒了selector,这个selector是个啥呢?这就得说起Poller线程了,它的NioEndpoint.Poller#run方法如下:
public class Poller implements Runnable {
private Selector selector;
public Poller() throws IOException {
// 得到一个 selector 对象
this.selector = Selector.open();
}
@Override
public void run() {
while (true) {
boolean hasEvents = false;
try {
if (!close) {
// 1. 调用 events() 方法
hasEvents = events();
// 2. selector 操作
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
// 这里会阻塞,当 selector 在这里阻塞时,selector.wakeup() 会唤醒它
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
...
if (keyCount == 0) {
// 这里也会调用 events() 方法
hasEvents = (hasEvents | events());
}
} catch (Throwable x) {
...
}
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// selector 上存在key,进入处理流程
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
// 3. 处理请求
processKey(sk, socketWrapper);
}
}
}
...
}
...
}
Poller会定期阻塞selector的执行,并且会调用events()方法,当Acceptor线程完成"PollerEvent被添加到了一个SynchronizedQueue结构中,并且还唤醒了selector"操作后,在NioEndpoint.Poller#run调用events()方法会发生什么情况呢?
2.1 NioEndpoint.Poller#events
NioEndpoint.Poller#events方法如下:
public boolean events() {
boolean result = false;
PollerEvent pe = null;
// 遍历所有的事件,events.poll()操作的是SynchronizedQueue<PollerEvent>,元素在addEvent()添加的
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
result = true;
NioSocketWrapper socketWrapper = pe.getSocketWrapper();
SocketChannel sc = socketWrapper.getSocket().getIOChannel();
int interestOps = pe.getInterestOps();
if (sc == null) {
log.warn(sm.getString("endpoint.nio.nullSocketChannel"));
socketWrapper.close();
} else if (interestOps == OP_REGISTER) {
try {
// 手动注册读事件到 selector 上,getSelector():返回与Poller绑定的selector
sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {
log.error(sm.getString("endpoint.nio.registerFail"), x);
}
} else {
...
}
...
}
return result;
}
在events()方法中,会调用events.poll()方法获取events中的所有元素,events类型是SynchronizedQueue<PollerEvent>,在NioEndpoint.Poller#addEvent方法中添加的PollerEvent会在这里被取出来处理,处理时有一个关键操作:
sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
这里由jdk nio提供的方法AbstractSelectableChannel#register,该方法的说明如下:
Registers this channel with the given selector, returning a selection key.
注册当前 channel 到指定的 select,并返回 selection
也就是说,selector上的事件是在AbstractSelectableChannel#register方法中手动注册的!
2.2 NioEndpoint.Poller#processKey
让我们再回到 NioEndpoint.Poller#run 方法:
@Override
public void run() {
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
// selector 操作
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
...
...
} catch (Throwable x) {
...
}
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// selector 上存在key,进入处理流程
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
// 处理请求
processKey(sk, socketWrapper);
}
}
}
...
}
执行完events()方法后,selector上就有数据了,调用selector.select(...)或selector.selectNow()方法就能获取到SelectionKey了。经观察发现,最终调用的是processKey(sk, socketWrapper)方法来处理请求:
protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
try {
if (close) {
cancelledKey(sk, socketWrapper);
} else if (sk.isValid() && socketWrapper != null) {
// 在这里处理请求
if (sk.isReadable() || sk.isWritable()) {
if (socketWrapper.getSendfileData() != null) {
// 处理文件数据
processSendfile(sk, socketWrapper, false);
} else {
unreg(sk, socketWrapper, sk.readyOps());
boolean closeSocket = false;
// 读事件
if (sk.isReadable()) {
if (socketWrapper.readOperation != null) {
if (!socketWrapper.readOperation.process()) {
closeSocket = true;
}
// processSocket(...):处理是 socketChannel 的读事件
} else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
// 写事件
if (!closeSocket && sk.isWritable()) {
...
}
if (closeSocket) {
cancelledKey(sk, socketWrapper);
}
}
}
} else {
// Invalid key
cancelledKey(sk, socketWrapper);
}
} catch (...) {
...
}
}
这个方法还是比较长的,这个方法会处理读、写事件,以及文件发送处理,我们主要关注读事件,方法为AbstractEndpoint#processSocket,代码如下:
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
// 获取一个 SocketProcessorBase
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
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 (...) {
...
}
return true;
}
最终socketWrapper与event会包装成SocketProcessorBase,然后丢到线程池中去处理了。SocketProcessorBase相关代码如下:
/**
* 内部持有两个成员变量:socketWrapper,event
* 实现了Runnable接口
*/
public abstract class SocketProcessorBase<S> implements Runnable {
protected SocketWrapperBase<S> socketWrapper;
protected SocketEvent event;
...
@Override
public final void run() {
synchronized (socketWrapper) {
...
if (socketWrapper.isClosed()) {
return;
}
// 实际干活的方法
doRun();
}
}
/**
* 真正干活的方法留给子类实现
*/
protected abstract void doRun();
}
这个类的内部持有两个成员变量:socketWrapper,event,它也实现了Runnable接口,线程的执行逻辑在run()方法中,不过从代码上来看,SocketProcessorBase#run方法基本没干啥事,真正干活的是doRun()方法,这个方法由子类实现。
当前SocketProcessorBase实际类型为SocketProcessor,因此真正的处理逻辑就是在SocketProcessor#doRun(),关于这个方法的分析,我们留到下篇文章进行。
2.3 tomcat 线程池
请求过来后,最终是丢到tomcat线程池中运行的,这里我们看看tomcat 线程池有啥特点。
1. 创建
线程池的创建是在NioEndpoint#startInternal方法:
public void startInternal() throws Exception {
// 创建处理业务逻辑的线程池
if (getExecutor() == null) {
createExecutor();
}
}
继续进入createExecutor()方法,来到AbstractEndpoint#createExecutor:
public void createExecutor() {
internalExecutor = true;
// 任务队列,没有设置大小,默认大小为Integer.MAX_VALUE
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(
getName() + "-exec-", daemon, getThreadPriority());
// 线程池的创建,很熟悉的方法
executor = new ThreadPoolExecutor(
getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS, taskqueue, tf);
// 绑定线程池与任务队列的关系
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
getMinSpareThreads():核心线程数,默认为10getMaxThreads():最大线程数,默认为20060, TimeUnit.SECONDS:非核心线程最大存活时间,即超时核心线程数后,若有线程空闲时间超过60秒,那么该非核心线程就会关闭taskqueue:任务队列,处理不过来的任务放队列里,注意:这里的任务队列没有设置大小,默认大小为Integer.MAX_VALUEth:线程工厂,用来生成线程
那么队列满了,它会采取什么拒绝策略呢?我们继续进入ThreadPoolExecutor的构造方法:
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
/**
* 构造方法
*/
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
// 调用父类的构造方法
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
new RejectHandler());
prestartAllCoreThreads();
}
...
/**
* 这里拒绝策略
*/
private static class RejectHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r,
java.util.concurrent.ThreadPoolExecutor executor) {
throw new RejectedExecutionException();
}
}
}
需要注意的是,这里的ThreadPoolExecutor是tomcat提供的org.apache.tomcat.util.threads.ThreadPoolExecutor,它继承了jdk的ThreadPoolExecutor,从代码来看,它的拒绝策略为抛异常。
2. 任务的执行
还是org.apache.tomcat.util.threads.ThreadPoolExecutor类,我们来看看它是怎么执行任务的:
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
/** 正在执行的任务数 */
private final AtomicInteger submittedCount = new AtomicInteger(0);
/**
* 执行
*/
@Override
public void execute(Runnable command) {
execute(command,0,TimeUnit.MILLISECONDS);
}
public void execute(Runnable command, long timeout, TimeUnit unit) {
// 执行前,将submittedCount值加1
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
...
}
}
/**
* 执行完成
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
// 执行结束后,将submittedCount值减1
if (!(t instanceof StopPooledThreadException)) {
submittedCount.decrementAndGet();
}
if (t == null) {
stopCurrentThreadIfNeeded();
}
}
...
从源码上来看,ThreadPoolExecutor的执行还是调用java.util.concurrent.ThreadPoolExecutor#execute来做的,只不过ThreadPoolExecutor中维护了一个成员变量submittedCount,执行前将这个值加1,执行完成后再将这个值减1.从这里我们可以大致推断出,submittedCount表示的是当前正在执行的任务数。
那么submittedCount在哪里用到呢?这里我们需要来到TaskQueue#offer方法:
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
@Override
public boolean offer(Runnable o) {
// parent 不存在,直接添加到队列
if (parent==null) return super.offer(o);
// 线程池的核心线程数等于最大线程数,放到队列里,不能再创建线程了
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
// 执行到这里,表明当前线程数小于最大线程数。
// 表明是可以创建新线程的,那到底要不要创建呢?分两种情况:
// 已提交的任务数小于等于核心线程数,说明当前线程还有空闲,不必创建线程,添加到队列中
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
// 线程池的核心线程数小于最大线程数,表明需要创建新线程处理,不添加到队列中
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
return super.offer(o);
}
...
}
在设置线程池时,我们设置了核心线程数与最大线程数,当核心线程数来不急处理时,就再创建线程,直到达到最大线程数,此时再有任务进来,就放进队列里。
3. 关于 tomcat nio 模型的几个问题
3.1 tomcat 的 nio 模型
tomcat 中,线程模型如下:
- 处理连接的组件为
Acceptor,一个线程处理,用来接收服务端请求,然后丢给Poller处理; - 处理读事件的线程为
Poller,一个线程处理,处理Acceptor传递过来的请求,然后丢给线程池处理;
整个处理流程如下图所示:
3.2 Poller线程的作用?
看完nio的线程模型后,不知道大家有没有一个疑问:
Acceptor线程获取到一个连接后,要经过这么些步骤的处理:
Acceptor线程将连接丢到队列里Poller线程扫描队列,得到连接,将其注册到selector上Poller线程调用selector#select(...)/selector#selectNow(...)方法拿到连接Poller线程将连接丢到线程池中
看完后,有没有发现Poller似乎做了一些额外的工作,比如,
Poller线程在队列中获取到连接后,直接丢到线程池中就好了,为何还要注册到selector中?Acceptor线程获取到连接后,直接丢到线程池中不更好,为何要经过Poller线程?
个人猜测,注册到selector上,主要是为了复用连接,在http 1.1的时候,http请求是可以复用的,当注册到selector后,下一次请求过来时,就不会再走Acceptor线程了,而是直接处理读写请求了。
3.3 selector空轮询问题
关于selector空轮询问题,不了解的小伙伴可以看看JDK Epoll空轮询bug,个人感觉解释得还不错,这里引用文章中的一张图:
对于空轮询问题,图上已经标记得很清楚了,简单来说,就是关注的select轮询事件返回数量为0时,NIO照样不断的从select本应该阻塞的Selector.select()/Selector.select(timeout)中wake up出来,导致CPU 100%问题,
在 tomcat的Poller线程中,一样有Selector.select(timeout)操作,那它会有空轮询问题吗?注意到tomcat9支持的最低版本是jdk8,而代码中又没任何关于空轮询的处理,这是说jdk8已经没有了空轮询问题吗?大家对此有何见解呢,欢迎留言讨论。
4. 总结
本文主要分析了tomcat处理服务端连接的过程,总结如下:
Acceptor线程将连接丢到队列里Poller线程扫描队列,得到连接,将其注册到selector上Poller线程调用selector#select(...)/selector#selectNow(...)方法拿到连接Poller线程将连接丢到线程池中
连接丢到线程池后,tomcat又是如何处理的呢?我们下一篇揭晓。
限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。
本文首发于微信公众号 Java技术探秘,链接:mp.weixin.qq.com/s/2SRV_bppK…,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!