一時技癢,擼了個動態執行緒池,原始碼放Github了
闡述背景
執行緒池在日常工作中用的還挺多,當需要非同步,批次處理一些任務的時候我們會定義一個執行緒池來處理。
在使用執行緒池的過程中有一些問題,下面簡單介紹下之前遇到的一些問題。
場景一:實現一些批次處理資料的功能,剛開始執行緒池的核心執行緒數設的比較小,然後想調整下,只能改完後重啟應用。
場景二:有一個任務處理的應用,會接收 MQ 的訊息進行任務的處理,執行緒池的佇列也允許快取一定數量的任務。當任務處理的很慢的時候,想看看到底有多少沒有處理完不是很方便。當時為了快速方便,就直接啟動了一個執行緒去迴圈列印執行緒池佇列的大小。
正好之前在我公眾號有轉發過美團的一篇執行緒池應用的文章(https://mp.weixin.qq.com/s/tIWAocevZThfbrfWoJGa9w),覺得他們的思路非常好,就是沒有開放原始碼,所以自己就抽時間在我的開源專案 Kitty 中增加了一個動態執行緒池的元件,支援了 Cat 監控,動態變更核心引數,任務堆積告警等。今天就給大家分享一下實現的方式。
專案原始碼地址:[1]
使用方式
新增依賴
依賴執行緒池的元件,目前 Kitty 未釋出,需要自己下載原始碼 install 本地或者私有倉庫。
<dependency>
<groupId>com.cxytiandi</groupId>
<artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
</dependency>
新增配置
然後在 Nacos 配置執行緒池的資訊,我的這個整合了 Nacos。推薦一個應用建立一個單獨的執行緒池配置檔案,比如我們這個叫 dataId 為 kitty-cloud-thread-pool.properties,group 為 BIZ_GROUP。
內容如下:
kitty.threadpools.nacosDataId=kitty-cloud-thread-pool.properties
kitty.threadpools.nacosGroup=BIZ_GROUP
kitty.threadpools.accessToken=ae6eb1e9e6964d686d2f2e8127d0ce5b31097ba23deee6e4f833bc0a77d5b71d
kitty.threadpools.secret=SEC6ec6e31d1aa1bdb2f7fd5eb5934504ce09b65f6bdc398d00ba73a9857372de00
kitty.threadpools.owner=尹吉歡
kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
kitty.threadpools.executors[0].corePoolSize=4
kitty.threadpools.executors[0].maximumPoolSize=4
kitty.threadpools.executors[0].queueCapacity=5
kitty.threadpools.executors[0].queueCapacityThreshold=5
kitty.threadpools.executors[1].threadPoolName=TestThreadPoolExecutor2
kitty.threadpools.executors[1].corePoolSize=2
kitty.threadpools.executors[1].maximumPoolSize=4
nacosDataId,nacosGroup
監聽配置修改的時候需要知道監聽哪個 DataId,值就是當前配置的 DataId。
accessToken,secret
釘釘機器人的驗證資訊,用於告警。
owner
這個應用的負責人,告警的訊息中會顯示。
threadPoolName
執行緒池的名稱,使用的時候需要關注。
剩下的配置就不一一介紹了,跟執行緒池內部的引數一致,還有一些可以檢視原始碼得知。
注入使用
@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
dynamicThreadPoolManager.getThreadPoolExecutor("TestThreadPoolExecutor").execute(() -> {
log.info("執行緒池的使用");
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "getArticle");
透過 DynamicThreadPoolManager 的 getThreadPoolExecutor 方法獲取執行緒池物件,然後傳入 Runnable,Callable 等。第二個引數是這個任務的名稱,之所以要擴充套件一個引數是因為如果任務沒有標識,那麼無法區分任務。
這個執行緒池元件預設整合了 Cat 打點,設定了名稱可以在 Cat 上檢視這個任務相關的監控資料。
擴充套件功能
任務執行情況監控
在 Cat 的 Transaction 報表中會以執行緒池的名稱為型別顯示。
詳情中會以任務的名稱顯示。
核心引數動態修改
核心引數目前只支援 corePoolSize,maximumPoolSize,queueCapacity(佇列型別為 LinkedBlockingDeque 才可以修改),rejectedExecutionType,keepAliveTime,unit 這些引數的修改。
一般 corePoolSize,maximumPoolSize,queueCapacity 是最常要動態改變的。
需要改動的話直接在 Nacos 中將對應的配置值修改即可,客戶端會監聽配置的修改,然後同步修改先執行緒池的引數。
佇列容量告警
queueCapacityThreshold 是佇列容量告警的閥值,如果佇列中的任務數量超過了 queueCapacityThreshold 就會告警。
拒絕次數告警
當佇列容量滿了後,新進來的任務會根據使用者設定的拒絕策略去選擇對應的處理方式。如果是採用 AbortPolicy 策略,也會進行告警。相當於消費者已經超負荷了。
執行緒池執行情況
底層對接了 Cat,所以將執行緒的執行資料上報給了 Cat。我們可以在 Cat 中檢視這些資訊。
如果你想在自己的平臺去展示,我這邊暴露了/actuator/thread-pool 端點,你可以自行拉取資料。
{
threadPools: [{
threadPoolName: "TestThreadPoolExecutor",
activeCount: 0,
keepAliveTime: 0,
largestPoolSize: 4,
fair: false,
queueCapacity: 5,
queueCapacityThreshold: 2,
rejectCount: 0,
waitTaskCount: 0,
taskCount: 5,
unit: "MILLISECONDS",
rejectedExecutionType: "AbortPolicy",
corePoolSize: 4,
queueType: "LinkedBlockingQueue",
completedTaskCount: 5,
maximumPoolSize: 4
}, {
threadPoolName: "TestThreadPoolExecutor2",
activeCount: 0,
keepAliveTime: 0,
largestPoolSize: 0,
fair: false,
queueCapacity: 2147483647,
queueCapacityThreshold: 2147483647,
rejectCount: 0,
waitTaskCount: 0,
taskCount: 0,
unit: "MILLISECONDS",
rejectedExecutionType: "AbortPolicy",
corePoolSize: 2,
queueType: "LinkedBlockingQueue",
completedTaskCount: 0,
maximumPoolSize: 4
}]
}
自定義拒絕策略
平時我們使用程式碼建立執行緒池可以自定義拒絕策略,在構造執行緒池物件的時候傳入即可。這裡由於建立執行緒池都被封裝好了,我們只能在 Nacos 配置拒絕策略的名稱來使用對應的策略。預設是可以配置 JDK 自帶的 CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy 這四種。
如果你想自定義的話也是支援的,定義方式跟以前一樣,如下:
@Slf4j
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.info("進來了。。。。。。。。。");
}
}
要讓這個策略生效的話使用的是 SPI 的方式,需要在 resources 下面建立一個 META-INF 的資料夾,然後建立一個 services 的資料夾,再建立一個 java.util.concurrent.RejectedExecutionHandler 的檔案,內容為你定義的類全路徑。
自定義告警方式
預設是內部整合了釘釘機器人的告警方式,如果你不想用也可以將其關閉。或者將告警資訊對接到你的監控平臺去。
如果沒有告警平臺也可以在專案中實現新的告警方式,比如簡訊等。
只需要實現 ThreadPoolAlarmNotify 這個類即可。
/**
* 自定義簡訊告警通知
*
* @作者 尹吉歡
* @個人微信 jihuan900
* @微信公眾號 猿天地
* @GitHub
* @作者介紹
* @時間 2020-05-27 22:26
*/
@Slf4j
@Component
public class ThreadPoolSmsAlarmNotify implements ThreadPoolAlarmNotify {
@Override
public void alarmNotify(AlarmMessage alarmMessage) {
log.info(alarmMessage.toString());
}
}
程式碼實現
具體的就不講的很細了,原始碼在/tree/master/kitty-dynamic-thread-pool[2],大家自己去看,並不複雜。
建立執行緒池
根據配置建立執行緒池,ThreadPoolExecutor 是自定義的,因為需要做 Cat 埋點。
/**
* 建立執行緒池
* @param threadPoolProperties
*/
private void createThreadPoolExecutor(DynamicThreadPoolProperties threadPoolProperties) {
threadPoolProperties.getExecutors().forEach(executor -> {
KittyThreadPoolExecutor threadPoolExecutor = new KittyThreadPoolExecutor(
executor.getCorePoolSize(),
executor.getMaximumPoolSize(),
executor.getKeepAliveTime(),
executor.getUnit(),
getBlockingQueue(executor.getQueueType(), executor.getQueueCapacity(), executor.isFair()),
new KittyThreadFactory(executor.getThreadPoolName()),
getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()), executor.getThreadPoolName());
threadPoolExecutorMap.put(executor.getThreadPoolName(), threadPoolExecutor);
});
}
重新整理執行緒池
首先需要監聽 Nacos 的修改。
/**
* 監聽配置修改,spring-cloud-alibaba 2.1.0版本不支援@NacosConfigListener的監聽
*/
public void initConfigUpdateListener(DynamicThreadPoolProperties dynamicThreadPoolProperties) {
ConfigService configService = nacosConfigProperties.configServiceInstance();
try {
configService.addListener(dynamicThreadPoolProperties.getNacosDataId(), dynamicThreadPoolProperties.getNacosGroup(), new AbstractListener() {
@Override
public void receiveConfigInfo(String configInfo) {
new Thread(() -> refreshThreadPoolExecutor()).start();
log.info("執行緒池配置有變化,重新整理完成");
}
});
} catch (NacosException e) {
log.error("Nacos配置監聽異常", e);
}
}
然後再重新整理執行緒池的引數資訊,由於監聽事件觸發的時候,這個時候配置其實還沒重新整理,所以我就等待了 1 秒鐘,讓配置完成重新整理然後直接從配置類取值。
雖然有點挫還是可以用,其實更好的方式是解析 receiveConfigInfo 那個 configInfo,configInfo 就是改變之後的整個配置內容。因為不太好解析成屬性檔案,就沒做,後面再改吧。
/**
* 重新整理執行緒池
*/
private void refreshThreadPoolExecutor() {
try {
// 等待配置重新整理完成
Thread.sleep(1000);
} catch (InterruptedException e) {
}
dynamicThreadPoolProperties.getExecutors().forEach(executor -> {
ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorMap.get(executor.getThreadPoolName());
threadPoolExecutor.setCorePoolSize(executor.getCorePoolSize());
threadPoolExecutor.setMaximumPoolSize(executor.getMaximumPoolSize());
threadPoolExecutor.setKeepAliveTime(executor.getKeepAliveTime(), executor.getUnit());
threadPoolExecutor.setRejectedExecutionHandler(getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()));
BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
if (queue instanceof ResizableCapacityLinkedBlockIngQueue) {
((ResizableCapacityLinkedBlockIngQueue<Runnable>) queue).setCapacity(executor.getQueueCapacity());
}
});
}
其他的重新整理都是執行緒池自帶的,需要注意的是執行緒池佇列大小的重新整理,目前只支援 LinkedBlockingQueue 佇列,由於 LinkedBlockingQueue 的大小是不允許修改的,所以按照美團那篇文章提供的思路,自定義了一個可以修改的佇列,其實就是把 LinkedBlockingQueue 的程式碼複製了一份,改一下就可以。
往 Cat 上報執行資訊
往 Cat 的 Heartbeat 報表上傳資料的程式碼如下,主要還是 Cat 本身提供了擴充套件的能力。只需要定時去呼叫下面的方式上報資料即可。
public void registerStatusExtension(ThreadPoolProperties prop, KittyThreadPoolExecutor executor) {
StatusExtensionRegister.getInstance().register(new StatusExtension() {
@Override
public String getId() {
return "thread.pool.info." + prop.getThreadPoolName();
}
@Override
public String getDescription() {
return "執行緒池監控";
}
@Override
public Map<String, String> getProperties() {
AtomicLong rejectCount = getRejectCount(prop.getThreadPoolName());
Map<String, String> pool = new HashMap<>();
pool.put("activeCount", String.valueOf(executor.getActiveCount()));
pool.put("completedTaskCount", String.valueOf(executor.getCompletedTaskCount()));
pool.put("largestPoolSize", String.valueOf(executor.getLargestPoolSize()));
pool.put("taskCount", String.valueOf(executor.getTaskCount()));
pool.put("rejectCount", String.valueOf(rejectCount == null ? 0 : rejectCount.get()));
pool.put("waitTaskCount", String.valueOf(executor.getQueue().size()));
return pool;
}
});
}
定義執行緒池端點
透過自定義端點來暴露執行緒池的配置和執行的情況,可以讓外部的監控系統拉取資料做對應的處理。
@Endpoint(id = "thread-pool")
public class ThreadPoolEndpoint {
@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
@Autowired
private DynamicThreadPoolProperties dynamicThreadPoolProperties;
@ReadOperation
public Map<String, Object> threadPools() {
Map<String, Object> data = new HashMap<>();
List<Map> threadPools = new ArrayList<>();
dynamicThreadPoolProperties.getExecutors().forEach(prop -> {
KittyThreadPoolExecutor executor = dynamicThreadPoolManager.getThreadPoolExecutor(prop.getThreadPoolName());
AtomicLong rejectCount = dynamicThreadPoolManager.getRejectCount(prop.getThreadPoolName());
Map<String, Object> pool = new HashMap<>();
Map config = JSONObject.parseObject(JSONObject.toJSONString(prop), Map.class);
pool.putAll(config);
pool.put("activeCount", executor.getActiveCount());
pool.put("completedTaskCount", executor.getCompletedTaskCount());
pool.put("largestPoolSize", executor.getLargestPoolSize());
pool.put("taskCount", executor.getTaskCount());
pool.put("rejectCount", rejectCount == null ? 0 : rejectCount.get());
pool.put("waitTaskCount", executor.getQueue().size());
threadPools.add(pool);
});
data.put("threadPools", threadPools);
return data;
}
}
Cat 監控執行緒池中執行緒的執行時間
本來是將監控放在 KittyThreadPoolExecutor 的 execute,submit 方法裡的。後面測試下來發現有問題,資料在 Cat 上確實有了,但是執行時間都是 1 毫秒,也就是沒生效。
不說想必大家也知道,因為執行緒是後面單獨去執行的,所以再新增任務的地方埋點沒任務意義。
後面還是想到了一個辦法來實現埋點的功能,就是利用執行緒池提供的 beforeExecute 和 afterExecute 兩個方法,線上程執行之前和執行之後都會觸發這兩個方法。
@Override
protected void beforeExecute(Thread t, Runnable r) {
String threadName = Thread.currentThread().getName();
Transaction transaction = Cat.newTransaction(threadPoolName, runnableNameMap.get(r.getClass().getSimpleName()));
transactionMap.put(threadName, transaction);
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
String threadName = Thread.currentThread().getName();
Transaction transaction = transactionMap.get(threadName);
transaction.setStatus(Message.SUCCESS);
if (t != null) {
Cat.logError(t);
transaction.setStatus(t);
}
transaction.complete();
transactionMap.remove(threadName);
}
後面的程式碼大家自己去看就行了,本文到這裡就結束了。如果感覺本文還不錯的記得轉發下哦!
多謝多謝。
最後感謝美團技術團隊的那篇文章,雖然沒有分享原始碼,但是思路什麼的和應用場景都講的很明白。
感興趣的 Star 下唄:[3]
關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號 猿天地 發起人。個人微信 jihuan900,歡迎勾搭。
參考資料
kitty:
[2]kitty-dynamic-thread-pool: /tree/master/kitty-dynamic-thread-pool
[3]kitty-cloud: -cloud
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31561268/viewspace-2699088/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 擼完這篇執行緒池,我快咳血了!執行緒
- 美團動態執行緒池實踐思路,開源了執行緒
- Netty原始碼解析一——執行緒池模型之執行緒池NioEventLoopGroupNetty原始碼執行緒模型OOP
- 詳解Java執行緒池的ctl(執行緒池控制狀態)【原始碼分析】Java執行緒原始碼
- java執行緒池原始碼一窺Java執行緒原始碼
- 執行緒池的建立和使用,執行緒池原始碼初探(篇一)執行緒原始碼
- 執行緒池原始碼探究執行緒原始碼
- 執行緒池原始碼分析執行緒原始碼
- 執行緒池之ScheduledThreadPoolExecutor執行緒池原始碼分析筆記執行緒thread原始碼筆記
- 執行緒池之ThreadPoolExecutor執行緒池原始碼分析筆記執行緒thread原始碼筆記
- JAVA執行緒池原理原始碼解析—為什麼啟動一個執行緒池,提交一個任務後,Main方法不會退出?Java執行緒原始碼AI
- 原始碼|從序列執行緒封閉到物件池、執行緒池原始碼執行緒物件
- 我用 Python 擼了一個 plist 圖集拆圖工具!附上github原始碼PythonGithub原始碼
- JDK執行緒池原始碼研究JDK執行緒原始碼
- java 執行緒池執行緒忙碌且阻塞佇列也滿了時給一個拒接的詳細報告Java執行緒佇列
- 執行緒池執行模型原始碼全解析執行緒模型原始碼
- 手動造一個執行緒池(Java)執行緒Java
- 動態執行緒池(DynamicTp)之動態調整Tomcat、Jetty、Undertow執行緒池引數篇執行緒TomcatJetty
- 美團動態執行緒池實踐思路開源專案(DynamicTp),執行緒池原始碼解析及通知告警篇執行緒原始碼
- Java執行緒池ThreadPoolExecutor原始碼解析Java執行緒thread原始碼
- Python執行緒池ThreadPoolExecutor原始碼分析Python執行緒thread原始碼
- Java執行緒池原始碼及原理Java執行緒原始碼
- Java原始碼解析 ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java原始碼解析 - ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- 執行緒(一)——執行緒,執行緒池,Task概念+程式碼實踐執行緒
- 從原始碼的角度解析執行緒池執行原理原始碼執行緒
- 這麼講執行緒池,徹底明白了!執行緒
- 深入Java原始碼理解執行緒池原理Java原始碼執行緒
- 死磕 java執行緒系列之自己動手寫一個執行緒池Java執行緒
- 深入淺出Java執行緒池:原始碼篇Java執行緒原始碼
- Java排程執行緒池ScheduledThreadPoolExecutor原始碼分析Java執行緒thread原始碼
- jdk1.8 執行緒池部分原始碼分析JDK執行緒原始碼
- 併發系列(一)——執行緒池原始碼(ThreadPoolExecutor類)簡析執行緒原始碼thread
- ThreadPollExcutor執行緒池的狀態thread執行緒
- Java執行緒池一:執行緒基礎Java執行緒
- 簡單擼了個 GitHub trending 爬取 APIGithubAPI
- 六、執行緒池(一)執行緒
- Swoole 啟動一個服務,開啟了哪些程式和執行緒?執行緒