1. 简介
当前主流的Web服务器如Tomcat、Jetty均已采用nio作为默认的I/O模型。通常nio的线程模型中会有一组Acceptor线程用于接收客户端连接,一组Selector线程用于监听和处理I/O事件。当Selector检测到I/O事件后,是用同一个线程执行业务逻辑,还是将事件提交到一个业务线程组执行呢?不同的应用服务器,不同的场景下有着不同的策略。
本文将介绍几种主流的应用服务器的业务线程策略。
2. 原理分析
2.1 Tomcat
对于Tomcat来说情况较为简单:
- LimitLatch作为连接信号量,负责控制最大连接数,达到阈值后,连接请求将被拒绝
- Acceptor是一组线程,负责accept客户端请求,当客户端请求到达时,accept方法构建一个Channel对象,并将Channel对象交给Poller处理
- Poller中维护了一个Selector线程,当检测到OP_READ或者OP_WRITE事件时,生成一个SocketProcessor对象,提交给Executor执行
从下文代码中可以看出,对于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;
}
}
2.2 Jetty
Jetty对于业务线程的处理则要复杂一些,它可以采用多种策略:
- ProduceConsume,任务生产者自己生产和执行任务,当任务耗时较长时,会严重影响Poller的执行效率
- ProduceExecuteConsume,如果是非阻塞任务,则当前生产者执行;否则,开启提交Executor执行任务。该策略不能重复利用CPU缓存,且线程上下文切换代价较高
- ExecuteProduceConsume,由生产者执行任务;但是如果任务类型是非阻塞任务,且生产者不处于pending状态,则提交Executor执行
- EatWhatYouKill,EatWhatYouKill是Jetty对于ExecuteProduceConsume的优化,依赖于QTP在9.4.x新增的ReservedThreadExecutor能力。当任务为非阻塞时,使用ProduceConsume模式;当任务为阻塞时,且生产者处于pending状态,使用EXECUTE_PRODUCE_CONSUME模式;生产者非pending状态,且ReservedThreadExecutor有可用线程时,也是用EXECUTE_PRODUCE_CONSUME模式;其他情况下使用PRODUCE_EXECUTE_CONSUME模式。
2.2.1 ReservedThreadExecutor
ReservedThreadExecutor是Jetty 9.4.x中对于QTP的增强,通过该Executor,Jetty可以在QTP中保留多个线程。
Jetty启动时
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;
}
3. 总结
综上所述,Jetty在业务线程模型上的设计更为精巧,充分利用了CPU缓存,减少了线程上下文切换。