tomcat連線處理機制和執行緒模型

wastonl發表於2024-08-11

前提

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模式核心的類為NioEndpointAcceptorPoller

NioEndpoint.start()方法會建立ServerSocketChannel監聽連線,同時開啟AcceptorPoller兩個執行緒。

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

相關文章