大家好,這篇文章我們來介紹下動態執行緒池框架(DynamicTp)的adapter模組,上篇文章也大概介紹過了,該模組主要是用來適配一些第三方元件的執行緒池管理,讓第三方元件內建的執行緒池也能享受到動態引數調整,監控告警這些增強功能。
DynamicTp專案地址
目前500多star,感謝你的star,歡迎pr,業務之餘給開源貢獻一份力量
gitee地址:https://gitee.com/yanhom/dynamic-tp
github地址:https://github.com/lyh200/dynamic-tp
系列文章
adapter已接入元件
adapter模組目前已經接入了SpringBoot內建的三大WebServer(Tomcat、Jetty、Undertow)的執行緒池管理,實現層面也是和核心模組做了解耦,利用spring的事件機制進行通知監聽處理。
可以看出有兩個監聽器
-
當監聽到配置中心配置變更時,在更新我們專案內部執行緒池後會釋出一個RefreshEvent事件,DtpWebRefreshListener監聽到該事件後會去更新對應WebServer的執行緒池引數。
-
同樣監控告警也是如此,在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