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緩存,減少了線程上下文切換。