動態執行緒池(DynamicTp)之動態調整Tomcat、Jetty、Undertow執行緒池引數篇

yanhom1314發表於2022-03-10

大家好,這篇文章我們來介紹下動態執行緒池框架(DynamicTp)的adapter模組,上篇文章也大概介紹過了,該模組主要是用來適配一些第三方元件的執行緒池管理,讓第三方元件內建的執行緒池也能享受到動態引數調整,監控告警這些增強功能。


DynamicTp專案地址

目前500多star,感謝你的star,歡迎pr,業務之餘給開源貢獻一份力量

gitee地址https://gitee.com/yanhom/dynamic-tp

github地址https://github.com/lyh200/dynamic-tp


系列文章

美團動態執行緒池實踐思路,開源了

動態執行緒池框架(DynamicTp),監控及原始碼解析篇


adapter已接入元件

adapter模組目前已經接入了SpringBoot內建的三大WebServer(Tomcat、Jetty、Undertow)的執行緒池管理,實現層面也是和核心模組做了解耦,利用spring的事件機制進行通知監聽處理。

可以看出有兩個監聽器

  1. 當監聽到配置中心配置變更時,在更新我們專案內部執行緒池後會釋出一個RefreshEvent事件,DtpWebRefreshListener監聽到該事件後會去更新對應WebServer的執行緒池引數。

  2. 同樣監控告警也是如此,在DtpMonitor中執行監控任務時會發布CollectEvent事件,DtpWebCollectListener監聽到該事件後會去採集相應WebServer的執行緒池指標資料。

要想去管理第三方元件的執行緒池,首先肯定要對這些元件有一定的熟悉度,瞭解整個請求的一個處理過程,找到對應處理請求的執行緒池,這些執行緒池不一定是JUC包下的ThreadPoolExecutor類,也可能是元件自己實現的執行緒池,但是基本原理都差不多。

Tomcat、Jetty、Undertow這三個都是這樣,他們並沒有直接使用JUC提供的執行緒池實現,而是自己實現了一套,或者擴充套件了JUC的實現;翻原始碼找到相應的執行緒池後,然後看有沒有暴露public方法供我們呼叫獲取,如果沒有就需要考慮通過反射來拿了。


Tomcat內部執行緒池的實現

  • Tomcat內部執行緒池沒有直接使用JUC下的ThreadPoolExecutor,而是選擇繼承JUC下的Executor體系類,然後重寫execute()等方法,不同版本有差異。

1.繼承JUC原生ThreadPoolExecutor(9.0.50版本及以下),並覆寫了一些方法,主要execute()和afterExecute()

2.繼承JUC的AbstractExecutorService(9.0.51版本及以上),程式碼基本是拷貝JUC的ThreadPoolExecutor,也相應的微調了execute()方法

注意Tomcat實現的執行緒池類名稱也叫ThreadPoolExecutor,名字跟JUC下的是一樣的,Tomcat的ThreadPoolExecutor類execute()方法如下:

public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            if (super.getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue)super.getQueue();
                try {
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }

        }
    }

可以看出他是先呼叫父類的execute()方法,然後捕獲RejectedExecutionException異常,再去判斷如果任務佇列型別是TaskQueue,則嘗試將任務新增到任務佇列中,如果新增失敗,證明佇列已滿,然後再執行拒絕策略,此處submittedCount是一個原子變數,記錄提交到此執行緒池但未執行完成的任務數(主要在下面要提到的TaskQueue佇列的offer()方法用),為什麼要這樣設計呢?繼續往下看!

  • Tomcat定義了阻塞佇列TaskQueue繼承自LinkedBlockingQueue,該佇列主要重寫了offer()方法。
 @Override
    public boolean offer(Runnable o) {
        //we can't do any checks
        if (parent==null) return super.offer(o);
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

可以看到他在入隊之前做了幾個判斷,這裡的parent就是所屬的執行緒池物件

1.如果parent為null,直接呼叫父類offer方法入隊

2.如果當前執行緒數等於最大執行緒數,則直接呼叫父類offer()方法入隊

3.如果當前未執行的任務數量小於等於當前執行緒數,仔細思考下,是不是說明有空閒的執行緒呢,那麼直接呼叫父類offer()入隊後就馬上有執行緒去執行它

4.如果當前執行緒數小於最大執行緒數量,則直接返回false,然後回到JUC執行緒池的執行流程回想下,是不是就去新增新執行緒去執行任務了呢

5.其他情況都直接入隊

  • 因為Tomcat執行緒池主要是來做IO任務的,做這一切的目的主要也是為了以最小代價的改動更好的支援IO密集型的場景,JUC自帶的執行緒池主要是適合於CPU密集型的場景,可以回想一下JUC原生執行緒池ThreadPoolExecutor#execute()方法的執行流程

1.判斷如果當前執行緒數小於核心執行緒池,則新建一個執行緒來處理提交的任務

2.如果當前執行緒數大於核心執行緒數且佇列沒滿,則將任務放入任務佇列等待執行

3.如果當前當前執行緒池數大於核心執行緒池,小於最大執行緒數,且任務佇列已滿,則建立新的執行緒執行提交的任務

4.如果當前執行緒數等於最大執行緒數,且佇列已滿,則拒絕該任務

可以看出噹噹前執行緒數大於核心執行緒數時,JUC原生執行緒池首先是把任務放到佇列裡等待執行,而不是先建立執行緒執行。

如果Tomcat接收的請求數量大於核心執行緒數,請求就會被放到佇列中,等待核心執行緒處理,這樣會降低請求的總體處理速度,所以Tomcat並沒有使用JUC原生執行緒池,利用TaskQueue的offer()方法巧妙的修改了JUC執行緒池的執行流程,改寫後Tomcat執行緒池執行流程如下:

1.判斷如果當前執行緒數小於核心執行緒池,則新建一個執行緒來處理提交的任務

2.如果當前當前執行緒池數大於核心執行緒池,小於最大執行緒數,則建立新的執行緒執行提交的任務

3.如果當前執行緒數等於最大執行緒數,則將任務放入任務佇列等待執行

4.如果佇列已滿,則執行拒絕策略

  • Tomcat核心執行緒池有對應的獲取方法,獲取方式如下
    public Executor doGetTp(WebServer webServer) {
        TomcatWebServer tomcatWebServer = (TomcatWebServer) webServer;
        return tomcatWebServer.getTomcat().getConnector().getProtocolHandler().getExecutor();
    }
  • 想要動態調整Tomcat執行緒池的執行緒引數,可以在引入DynamicTp依賴後,在配置檔案中新增以下配置就行,引數名稱也是和SpringBoot提供的Properties配置類引數相同,配置檔案完整示例看專案readme介紹
spring:
  dynamic:
    tp:
      // 其他配置項
      tomcatTp:       # tomcat web server執行緒池配置
        minSpare: 100   # 核心執行緒數
        max: 400        # 最大執行緒數

Tomcat執行緒池就介紹到這裡吧,通過以上的一些介紹想必大家對Tomcat執行緒池執行任務的流程都很清楚了吧。


Jetty內部執行緒池的實現

  • Jetty內部執行緒池,定義了一個繼承自Executor的ThreadPool頂級介面,實現類有以下幾個

  • 內部主要使用QueuedThreadPool這個實現類,該執行緒池執行流程就不在詳細解讀了,感興趣的可以自己去看原始碼,核心思想都差不多,圍繞核心執行緒數、最大執行緒數、任務佇列三個引數入手,跟Tocmat比對著來看,其實也挺簡單的。
public void execute(Runnable job)
    {
        // Determine if we need to start a thread, use and idle thread or just queue this job
        int startThread;
        while (true)
        {
            // Get the atomic counts
            long counts = _counts.get();

            // Get the number of threads started (might not yet be running)
            int threads = AtomicBiInteger.getHi(counts);
            if (threads == Integer.MIN_VALUE)
                throw new RejectedExecutionException(job.toString());

            // Get the number of truly idle threads. This count is reduced by the
            // job queue size so that any threads that are idle but are about to take
            // a job from the queue are not counted.
            int idle = AtomicBiInteger.getLo(counts);

            // Start a thread if we have insufficient idle threads to meet demand
            // and we are not at max threads.
            startThread = (idle <= 0 && threads < _maxThreads) ? 1 : 0;

            // The job will be run by an idle thread when available
            if (!_counts.compareAndSet(counts, threads + startThread, idle + startThread - 1))
                continue;

            break;
        }

        if (!_jobs.offer(job))
        {
            // reverse our changes to _counts.
            if (addCounts(-startThread, 1 - startThread))
                LOG.warn("{} rejected {}", this, job);
            throw new RejectedExecutionException(job.toString());
        }

        if (LOG.isDebugEnabled())
            LOG.debug("queue {} startThread={}", job, startThread);

        // Start a thread if one was needed
        while (startThread-- > 0)
            startThread();
    }
  • Jetty執行緒池有提供public的獲取方法,獲取方式如下
    public Executor doGetTp(WebServer webServer) {
        JettyWebServer jettyWebServer = (JettyWebServer) webServer;
        return jettyWebServer.getServer().getThreadPool();
    }
  • 想要動態調整Jetty執行緒池的執行緒引數,可以在引入DynamicTp依賴後,在配置檔案中新增以下配置就行,引數名稱也是和SpringBoot提供的Properties配置類引數相同,配置檔案完整示例看專案readme介紹
spring:
  dynamic:
    tp:
      // 其他配置項
      jettyTp:       # jetty web server執行緒池配置
        min: 100     # 核心執行緒數
        max: 400     # 最大執行緒數

Undertow內部執行緒池的實現

  • Undertow因為其效能彪悍,輕量,現在用的還是挺多的,wildfly(前身Jboss)從8開始內部預設的WebServer用Undertow了,之前是Tomcat吧。瞭解Undertow的小夥伴應該知道,他底層是基於XNIO框架(3.X之前)來做的,這也是Jboss開發的一款基於java nio的優秀網路框架。但Undertow宣佈從3.0開始底層網路框架要切換成Netty了,官方給的原因是說起網路程式設計,Netty已經是事實上標準,用Netty的好處遠大於XNIO能提供的,所以讓我們期待3.0的釋出吧,只可惜三年前就宣佈了,至今也沒動靜,不知道是夭折了還是咋的,說實話,改動也挺大的,看啥時候釋出吧,以下的介紹是基於Undertow 2.x版本來的

  • Undertow內部是定義了一個叫TaskPool的執行緒池頂級介面,該介面有如圖所示的幾個實現。其實這幾個實現類都是採用組合的方式,內部都維護一個JUC的Executor體系類或者維護Jboss提供的EnhancedQueueExecutor類(也繼承JUC ExecutorService類),執行流程可以自己去分析

  • 具體的建立程式碼如下,根據外部是否傳入,如果有傳入則用外部傳入的類,如果沒有,根據引數設定內部建立一個,具體是用JUC的ThreadPoolExecutor還是Jboss的EnhancedQueueExecutor,根據配置引數選擇

  • Undertow執行緒池沒有提供public的獲取方法,所以通過反射來獲取,獲取方式如下
    public Executor doGetTp(WebServer webServer) {

        UndertowWebServer undertowWebServer = (UndertowWebServer) webServer;
        Field undertowField = ReflectionUtils.findField(UndertowWebServer.class, "undertow");
        if (Objects.isNull(undertowField)) {
            return null;
        }
        ReflectionUtils.makeAccessible(undertowField);
        Undertow undertow = (Undertow) ReflectionUtils.getField(undertowField, undertowWebServer);
        if (Objects.isNull(undertow)) {
            return null;
        }
        return undertow.getWorker();
    }
  • 想要動態調整Undertow執行緒池的執行緒引數,可以在引入DynamicTp依賴後,在配置檔案中新增以下配置就行,配置檔案完整示例看專案readme介紹
spring:
  dynamic:
    tp:
      // 其他配置項
      undertowTp:   # undertow web server執行緒池配置
        coreWorkerThreads: 100  # worker核心執行緒數
        maxWorkerThreads: 400   # worker最大執行緒數
        workerKeepAlive: 60     # 空閒執行緒超時時間

總結

以上介紹了Tomcat、Jetty、Undertow三大WebServer內建執行緒池的一些情況,重點介紹了Tomcat的,篇幅有限,其他兩個感興趣可以自己分析,原理都差不多。同時也介紹了基於DynamicTp怎麼動態調整執行緒池的引數,當我們做WebServer效能調優時,能動態調整引數真的是非常好用的。

再次歡迎大家使用DynamicTp框架,一起完善專案。

下篇文章打算分享一個DynamicTp使用過程中因為Tomcat版本不一致導致的監控執行緒halt住的奇葩問題,通過一個問題來掌握ScheduledExecutorService的原理,歡迎大家持續關注。


聯絡我

歡迎加我微信或者關注公眾號交流,一起變強!

公眾號:CodeFox

微信:yanhom1314

相關文章