当前主流的Web服务器如Tomcat、Jetty均已采用nio作为默认的I/O模型。通常nio的线程模型中会有一组Acceptor线程用于接收客户端连接,一组Selector线程用于监听和处理I/O事件。当Selector检测到I/O事件后,是用同一个线程执行业务逻辑,还是将事件提交到一个业务线程组执行呢?不同的应用服务器,不同的场景下有着不同的策略。
本文将介绍几种主流的应用服务器的业务线程策略。
对于Tomcat来说情况较为简单:
从下文代码中可以看出,对于OP_READ和OP_WRITE事件,dispatch都是true,SocketProcessor对象都会被提交到线程池Executor执行。
AbstractEndpoint.java
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
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 (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;
}
AbstractEndpoint.java
if (sk.isReadable()) {
if (socketWrapper.readOperation != null) {
if (!socketWrapper.readOperation.process()) {
closeSocket = true;
}
} else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
if (socketWrapper.writeOperation != null) {
if (!socketWrapper.writeOperation.process()) {
closeSocket = true;
}
} else if (!processSocket(socketWrapper, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
Jetty对于业务线程的处理则要复杂一些,它可以采用多种策略:
ReservedThreadExecutor是Jetty 9.4.x中对于QTP的增强,通过该Executor,Jetty可以在QTP中保留多个线程。
QueuedThreadPool.java
@Override
protected void doStart() throws Exception
{
// 默认情况下,_reservedThreads为-1
if (_reservedThreads == 0)
{
_tryExecutor = NO_TRY;
}
else
{
ReservedThreadExecutor reserved = new ReservedThreadExecutor(this, _reservedThreads);
reserved.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS);
_tryExecutor = reserved;
}
addBean(_tryExecutor);
super.doStart();
// The threads count set to MIN_VALUE is used to signal to Runners that the pool is stopped.
_counts.set(0, 0); // threads, idle
ensureThreads();
}
ReservedThreadExecutor.java
public ReservedThreadExecutor(Executor executor, int capacity)
{
_executor = executor;
_capacity = reservedThreads(executor, capacity);
// 此处使用了无锁队列,以CPU时间为代价,提高并发性能
_stack = new ConcurrentLinkedDeque<>();
LOG.debug("{}", this);
}
// QTP实现了ThreadPool.SizedThreadPool接口,因此保留线程数在cpu核数和线程池size/10中取小
private static int reservedThreads(Executor executor, int capacity)
{
if (capacity >= 0)
return capacity;
int cpus = ProcessorUtils.availableProcessors();
if (executor instanceof ThreadPool.SizedThreadPool)
{
int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
return Math.max(1, Math.min(cpus, threads / 10));
}
return cpus;
}
// tryExecute方法是,TryExecutor接口定义的方法,调用方可以尝试执行一个runnable
// 如果执行失败,可能是没有空闲保留线程,则返回false,任务不会提交给线程执行,也不会加入等待队列
@Override
public boolean tryExecute(Runnable task)
{
if (LOG.isDebugEnabled())
LOG.debug("{} tryExecute {}", this, task);
if (task == null)
return false;
ReservedThread thread = _stack.pollFirst();
if (thread == null)
{
if (task != STOP)
startReservedThread();
return false;
}
int size = _size.decrementAndGet();
thread.offer(task);
if (size == 0 && task != STOP)
startReservedThread();
return true;
}
综上所述,Jetty在业务线程模型上的设计更为精巧,充分利用了CPU缓存,减少了线程上下文切换。
Tomcat 9.0.26
Jetty 9.4.20