前提
Tomcat中Connector實現主要有3種,NIO、NIO2、APR,其中NIO是預設方式。
NIO基於ServerSocketChannel
NIO2基於AsynchronousServerSocketChannel
後面都是基於NIO模式來進行闡述。
Tomcat執行緒池
tomcat中的執行緒池實現為ThreadPoolExecutor
,基本上是複製了JDK中的ThreadPoolExecutor
,與JDK中的執行緒池主要區別在於阻塞佇列實現,而execute方法並沒有區別。
JDK中的執行緒池提交邏輯是,小於核心執行緒數量時,新建執行緒來執行任務,超過核心執行緒數量後,放入阻塞佇列中,阻塞佇列滿了,新建執行緒來執行任務,當執行緒數量超過最大執行緒數時,則執行拒絕策略。
tomcat中的執行緒池提交邏輯是,小於核心執行緒數量時,新建執行緒來執行任務,超過核心執行緒數量後,還是新建執行緒來執行任務,當執行緒數量超過最大執行緒數時,將任務放入阻塞佇列中,阻塞佇列滿了,則執行拒絕策略。
tomcat的目標是要儘可能的多開執行緒來處理請求,而不是優先放入阻塞佇列。
Tomcat執行緒池阻塞佇列
tomcat中的阻塞佇列為TaskQueue
,它繼承了LinkedBlockingQueue
,主要看offer方法實現。
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
private transient volatile ThreadPoolExecutor parent = null;
@Override
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) {
return super.offer(o);
}
// 執行緒池的數量已經達到最大值, 走原邏輯
if (parent.getPoolSizeNoLock() == parent.getMaximumPoolSize()) {
return super.offer(o);
}
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount() <= parent.getPoolSizeNoLock()) {
return super.offer(o);
}
// 執行緒值中的數量小於最大值, 返回false, 代表佇列滿了,於是執行緒池則會新建執行緒來執行任務
if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) {
return false;
}
//if we reached here, we need to add it to the queue
return super.offer(o);
}
}
tomcat connector的幾個重要引數
- maxConnections,預設值為8192,允許的最大socket連線數,當連線數達到最大值,acceptor執行緒將會阻塞,不再呼叫
ServerSocketChannel.accept()
方法接收新的socket連線。 - acceptCount,預設值為100,作業系統掛起socket連線的最大值,等同於
ServerSocketChannel.bind()
方法的backlog
引數。當客戶端連線到服務端時,如果服務端沒有呼叫accept獲取連線,這個連線將會處於一個佇列中,當佇列中的連線數超過backlog
引數後,客戶端將會報錯connection refuse。而呼叫accept方法則會從佇列中取走一個連線。 - minSpareThreads,預設值10,執行緒池的核心執行緒數量。
- maxThreads,預設值200,執行緒池的最大執行緒數量。
總結:
因此tomcat預設可以處理的連線總數是maxConnections + acceptCount,超出後則會直接報錯。
tomcat處理請求時,都會使用一個執行緒來處理,如果需要處理的連線數量(已經發起請求資料了,空閒的連線不算)大於執行緒池數量時,多餘的連線請求則會被加入到阻塞佇列中。
對應的Spring Boot配置
- server.tomcat.max-connections
- server.tomcat.accept-count
- server.tomcat.threads.min-spare
- server.tomcat.threads.max
可以根據這些配置順藤摸瓜找到TomcatWebServerFactoryCustomizer
配置類,可以看這個類怎麼將這些配置給設定到tomcat中。
tomcat連線處理機制
NIO模式核心的類為NioEndpoint
、Acceptor
、Poller
。
NioEndpoint.start()
方法會建立ServerSocketChannel
監聽連線,同時開啟Acceptor
、Poller
兩個執行緒。
Acceptor執行緒無線迴圈呼叫ServerSocketChannel.accept()
獲取新的SocketChannel連線,然後將其設定為非阻塞模式,註冊讀事件,供Poller執行緒進行事件迴圈。在每次呼叫accept之前,都會檢查連線數量有沒有達到maxConnections,如果達到了則會先阻塞,直到已有的連線關閉。accept是阻塞式的,讀取請求資料是非阻塞式的。
poller執行緒也是一個無限迴圈,主要是呼叫Selector.select()
方法獲取可以讀寫的連線,然後分發給執行緒池來執行。每次迴圈主要做3件事件。
- 將Acceptor註冊的
PollerEvent
事件轉換成SelectionKey事件
,這裡為啥要折中下,因為Selector中的方法存在很多鎖,因此在單執行緒中處理是比較好的選擇,否則容易出現死鎖現象。比如Selector.select
方法和SocketChannel.register(selector)
就會死鎖。 - 呼叫
Selector.select
方法獲取就緒的SelectionKey
,分發給執行緒池來處理請求邏輯。 - 處理超時的socket連線,tomcat在http1.1協議下,因為keepAlive預設開啟,是不會主動關閉socket連線的,如果已經建立的socket連線沒有主動關閉,在20s(預設值)沒有傳送請求時,tomcat會主動關閉該socket連線。
關於acceptCount
NioEndpoint.java
public void bind() throws Exception {
initServerSocket();
setStopLatch(new CountDownLatch(1));
// Initialize SSL if needed
initialiseSsl();
}
protected void initServerSocket() throws Exception {
serverSock = ServerSocketChannel.open();
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// 關鍵點
serverSock.bind(addr, getAcceptCount());
// 這裡是阻塞模式哦,也就是接收連線是阻塞的
serverSock.configureBlocking(true);
}
關於maxConnections
Acceptor.java
/**
* 簡寫形式,虛擬碼
*/
public void run() {
while (true) {
endpoint.countUpOrAwaitConnection();
SocketChannel result = serverSock.accept();
// 向Poller中註冊PollerEvent
}
}
protected void countUpOrAwaitConnection() throws InterruptedException {
// -1不控制連線數量
if (maxConnections==-1) {
return;
}
// 控制連線數量, 作用和jdk中的Semaphore類似
LimitLatch latch = connectionLimitLatch;
if (latch!=null) {
// 達到maxConnections執行緒會阻塞
latch.countUpOrAwait();
}
}
關於minSpareThreads、maxThreads
NioEndpoint.java
@Override
public void startInternal() throws Exception {
if (getExecutor() == null) {
createExecutor();
}
// 開啟Poller執行緒
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
// 開啟Acceptor執行緒
startAcceptorThread();
}
public void createExecutor() {
internalExecutor = true;
// 使用虛擬執行緒(JDK21)
if (getUseVirtualThreads()) {
executor = new VirtualThreadExecutor(getName() + "-virt-");
} else {
// 使用tomcat定製的阻塞佇列,改變執行緒池提交行為,阻塞佇列是無界的,容量為Integer.MAX
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);
}
}
socket連線關閉時機
兩種正常關閉情況
- 客戶端主動關閉socket,此時會觸發READ事件,Poller執行緒會分發任務給執行緒池,讀取資料時發現已到達流末尾,返回-1,發生EOFExcetion,關閉連線。
- Poller執行緒每一次迴圈的最後,只要滿足清理條件(清理時間滿足,執行緒當前比較空閒,沒有就緒的事件和要轉換的PollerEvent),都會去關閉已經超時的socket連線(超時判定,超過20s沒有傳送請求資料,20s是預設時間)
簡易版tomcat連線實現
核心機制與tomcat一樣,簡化了處理請求邏輯,只能處理以\n結尾的請求資料,並簡單列印到控制檯。
TomcatServer
TomcatClient