曹工說Tomcat:200個http-nio-8080-exec線程全都被第三方服務拖住了,這可如何是好(上:線程模型解析)


前言

這兩年,tomcat慢慢在新項目里不怎么接觸了,因為都被spring boot之類的框架封裝進了內部,成了內置server,不用像過去那樣打個war包,再放到tomcat里部署了。

但是,內部的機制我們還是有必要了解的,尤其是線程模型和classloader,這篇我們會聚焦線程模型。

其實我本打算將一個問題,即大家知道,我們平時最終寫的controller、service那些業務代碼,最終是由什么線程來執行的呢?

大家都是debug過的人,肯定知道,線程名稱大概如下:

http-nio-8080-exec-2@5076

這個線程是tomcat的線程,假設,我們在這個線程里,sleep個1分鍾,模擬調用第三方服務時,第三方服務異常卡住不返回的情況,此時客戶端每秒100個請求過來,此時整個程序會出現什么情況?

但是我發現,這個問題,一篇還是講不太清楚,因此,本篇只講一下線程模型。

主要線程模型簡介

大家可以思考下,一個服務端程序,有哪些是肯定需要的?

我們肯定需要開啟監聽對吧,大家看看下面的bio程序:

這個就是個線程,在while(true)死循環里,一直accept客戶端連接。

ok,這個線程肯定是需要的。接下來,再看看還是否需要其他的線程。

如果一切從簡,我們只用這1個線程也足夠了,就像redis一樣,redis都是內存操作,做啥都很快,還避免了線程切換的開銷;

但是我們的java后端,一般都要操作數據庫的,這個是比較慢,自然是希望把這部分工作能夠交給單獨的線程去做,在tomcat里,確實是這樣的,交給了一個線程池,線程池里的線程,就是我們平時看到的,名稱類似http-nio-8080-exec-2@5076這樣的,一般默認配置,最大200個線程。

但如果這樣的話,1個acceptor + 一個業務線程池,會導致一個問題,就是,該acceptor既要負責新連接的接入,還要負責已接入連接的socket的io讀寫。假設我們維護了10萬個連接,這10萬個連接都在不斷地給我們的服務端發數據,我們服務端也在不停地給客戶端返回數據,那這個工作還是很繁重的,可能會壓垮這個唯一的acceptor線程。

因此,理想情況下,我們會在單獨弄幾個線程出來,負責已經接入的連接的io讀寫。

大體流程:

acceptor--->poller線程(負責已接入連接的io讀寫)-->業務線程池(http-nio-8080-exec-2@5076)

這個大概就是tomcat中的流程了。

在netty中,其實是類似的:

boss eventloop--->worker eventloop-->一般在解碼完成后的最后一個handler,交給自定義業務線程池

tomcat如何接入新連接

大家可以看看下圖,這里面有幾個橙色的方塊,這幾個代表了線程,從左到右,分別就是acceptor、nio線程池、poller線程。

  • 1處,acceptor線程內部維護了一個endpoint對象,這個對象呢,就代表了1個服務端端點;該對象有幾個實現類,如下:

    我們spring boot程序里,默認是用的NioEndpoint。

  • 2處,將新連接交給NioEndpoint處理

    @Override
        protected boolean setSocketOptions(SocketChannel socket) {
            // Process the connection
            try {
                // Disable blocking, polling will be used
                socket.configureBlocking(false);
                Socket sock = socket.socket();
                socketProperties.setProperties(sock);
                // 進行一些socket的參數設置
                NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
                channel.setSocketWrapper(socketWrapper);
                socketWrapper.setReadTimeout(getConnectionTimeout());
                socketWrapper.setWriteTimeout(getConnectionTimeout());
                //3 交給poller處理
                poller.register(channel, socketWrapper);
                return true;
            }
     		...
            // Tell to close the socket
            return false;
        }
    
  • 3處,就是交給NioEndpoint內部的poller對象去進行處理。

            public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) {
                socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
                PollerEvent r = null;
                // 丟到poller的隊列里,poller線程會輪旋該隊列
                r = new PollerEvent(socket, OP_REGISTER);
                // 丟到隊列里
                addEvent(r);
            }
    

    上面的addEvent值得一看。

    private final SynchronizedQueue<PollerEvent> events =
                    new SynchronizedQueue<>();
            
    private void addEvent(PollerEvent event) {
        // 丟到隊列里
        events.offer(event);
        // 喚醒poller里的selector,及時將該socket注冊到selector中
        if (wakeupCounter.incrementAndGet() == 0) {
            selector.wakeup();
        }
     }
    

    到這里,acceptor線程的邏輯就結束了,一個異步放隊列,完美收工。接下來,就是poller線程的工作了。

    poller線程,要負責將該socket注冊到selector里面去,然后還要負責該socket的io讀寫事件處理。

  • poller線程邏輯

        public class Poller implements Runnable {
    
            private Selector selector;
            private final SynchronizedQueue<PollerEvent> events =
                    new SynchronizedQueue<>();
    

    可以看到,poller內部維護了一個selector,和一個隊列,隊列里也說了,主要是要新注冊到selector的新socket。

    既然丟到隊列了,那我們看看什么時候去隊列取的呢?

            @Override
            public void run() {
                // Loop until destroy() is called
                while (true) {
                    boolean hasEvents = false;
                    // 檢查events
                    hasEvents = events();
               }
           }
    

    這里我們跟一下events()。

    public boolean events() {
        boolean result = false;
    
        PollerEvent pe = null;
        for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
            result = true;
            pe.run();
            ...
        }
    
        return result;
    }
    

    這里的

    pe = events.poll()
    

    就是去隊列拉取事件,拉取到了之后,就會賦值給pe,然后下面就調用了pe.run方法。

    pe的類型是PollerEvent,我們看看其run方法會干啥?

            @Override
            public void run() {
                if (interestOps == OP_REGISTER) {
                    try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper());
                    } catch (Exception x) {
                        log.error(sm.getString("endpoint.nio.registerFail"), x);
                    }
                }
            }
    

    這個方法難理解嗎,看着有點嚇人,其實就是把這個新的連接,向selector注冊,感興趣的io事件為OP_READ。后續呢,這個連接的io讀寫,就全由本poller的selector包了。

tomcat如何處理客戶端讀事件

我們說了,poller是個線程,在其runnable實現里,除了要處理上面的新連接注冊到selector這個事,還要負責io讀寫,這部分邏輯就是在:

        Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper socketWrapper = sk.attachment();
            processKey(sk, socketWrapper);
        }

最后一行的processKey,會調用如下邏輯,將工作甩鍋給http-nio-8080-exec-2@5076這類打雜的線程。

public boolean processSocket(SocketWrapperBase<S> socketWrapper,SocketEvent event, boolean dispatch) {
		Executor executor = getExecutor();
		executor.execute(sc);
        return true;
}

給個圖的話,大概就是如下的紅線流程部分了:

小結

好了,到了課后思考時間了,我們也說了,最終會交給http-nio-8080-exec-2@5076這類線程所在的線程池,那假設這些線程全都在sleep,會發生什么呢?

下一篇,我們繼續。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM