今天我們來聊一個比較實用的話題,動態可監控可觀測的執行緒池實踐。
這是個全新的開源專案,作者提供了一種非常好的思路解決了執行緒池的可觀測問題。
這個開源專案叫:DynamicTp
地址在文章末尾。
寫在前面
稍微有些Java程式設計經驗的小夥伴都知道,Java的精髓在juc包,這是大名鼎鼎的Doug Lea老爺子的傑作,評價一個程式設計師Java水平怎麼樣,一定程度上看他對juc包下的一些技術掌握的怎麼樣,這也是面試中的基本上必問的一些技術點之一。
juc包主要包括:
1.原子類(AtomicXXX)
2.鎖類(XXXLock)
3.執行緒同步類(AQS、CountDownLatch、CyclicBarrier、Semaphore、Exchanger)
4.任務執行器類(Executor體系類,包括今天的主角ThreadPoolExecutor)
5.併發集合類(ConcurrentXXX、CopyOnWriteXXX)相關集合類
6.阻塞佇列類(BlockingQueue繼承體系類)
7.Future相關類
8.其他一些輔助工具類
多執行緒程式設計場景下,這些類都是必備技能,會這些可以幫助我們寫出高質量、高效能、少bug的程式碼,同時這些也是Java中比較難啃的一些技術,需要持之以恆,學以致用,在使用中感受他們帶來的奧妙。
上邊簡單羅列了下juc包下功能分類,這篇文章我們主要來介紹動態可監控執行緒池的,所以具體內容也就不展開講了,以後有時間單獨來聊吧。看這篇文章前,希望讀者最好有一定的執行緒池ThreadPoolExecutor使用經驗,不然看起來會有點懵。
如果你對ThreadPoolExecutor不是很熟悉,推薦閱讀下面兩篇文章
javadoop: https://www.javadoop.com/post/java-thread-pool
美團技術部落格: https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
背景
使用ThreadPoolExecutor過程中你是否有以下痛點呢?
1.程式碼中建立了一個ThreadPoolExecutor,但是不知道那幾個核心引數設定多少比較合適
2.憑經驗設定引數值,上線後發現需要調整,改程式碼重啟服務,非常麻煩
3.執行緒池相對開發人員來說是個黑盒,執行情況不能感知到,直到出現問題
如果你有以上痛點,這篇文章要介紹的動態可監控執行緒池(DynamicTp)或許能幫助到你。
如果看過ThreadPoolExecutor的原始碼,大概可以知道其實它有提供一些set方法,可以在執行時動態去修改相應的值,這些方法有:
public void setCorePoolSize(int corePoolSize);
public void setMaximumPoolSize(int maximumPoolSize);
public void setKeepAliveTime(long time, TimeUnit unit);
public void setThreadFactory(ThreadFactory threadFactory);
public void setRejectedExecutionHandler(RejectedExecutionHandler handler);
現在大多數的網際網路專案其實都會微服務化部署,有一套自己的服務治理體系,微服務元件中的分散式配置中心扮演的就是動態修改配置,實時生效的角色。那麼我們是否可以結合配置中心來做執行時執行緒池引數的動態調整呢?答案是肯定的,而且配置中心相對都是高可用的,使用它也不用過於擔心配置推送出現問題這類事兒,而且也能減少研發動態執行緒池元件的難度和工作量。
綜上,我們總結出以下的背景
- 廣泛性:在Java開發中,想要提高系統效能,執行緒池已經是一個90%以上的人都會選擇使用的基礎工具
- 不確定性:專案中可能會建立很多執行緒池,既有IO密集型的,也有CPU密集型的,但執行緒池的引數並不好確定;需要有套機制在執行過程中動態去調整引數
- 無感知性,執行緒池執行過程中的各項指標一般感知不到;需要有套監控報警機制在事前、事中就能讓開發人員感知到執行緒池的執行狀況,及時處理
- 高可用性,配置變更需要及時推送到客戶端;需要有高可用的配置管理推送服務,配置中心是現在大多數網際網路系統都會使用的元件,與之結合可以大幅度減少開發量及接入難度
簡介
我們基於配置中心對執行緒池ThreadPoolExecutor做一些擴充套件,實現對執行中執行緒池引數的動態修改,實時生效;以及實時監控執行緒池的執行狀態,觸發設定的報警策略時報警,報警資訊會推送辦公平臺(釘釘、企微等)。報警維度包括(佇列容量、執行緒池活性、拒絕觸發等);同時也會定時採集執行緒池指標資料供監控平臺視覺化使用。使我們能時刻感知到執行緒池的負載,根據情況及時調整,避免出現問題影響線上業務。
| __ \ (_) |__ __|
| | | |_ _ _ __ __ _ _ __ ___ _ ___| |_ __
| | | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \
| |__| | |_| | | | | (_| | | | | | | | (__| | |_) |
|_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/
__/ | | |
|___/ |_|
:: Dynamic Thread Pool ::
特性
- 參考美團執行緒池實踐 ,對執行緒池引數動態化管理,增加監控、報警功能
- 基於Spring框架,現只支援SpringBoot專案使用,輕量級,引入starter即可食用
- 基於配置中心實現執行緒池引數動態調整,實時生效;整合主流配置中心,預設支援Nacos、Apollo,同時也提供SPI介面可自定義擴充套件實現
- 內建通知報警功能,提供多種報警維度(配置變更通知、活性報警、容量閾值報警、拒絕策略觸發報警),預設支援企業微信、釘釘報警,同時提供SPI介面可自定義擴充套件實現
- 內建執行緒池指標採集功能,支援通過MicroMeter、JsonLog日誌輸出、Endpoint三種方式,可通過SPI介面自定義擴充套件實現
架構設計
主要分四大模組
配置變更監聽模組:
1.監聽特定配置中心的指定配置檔案(預設實現Nacos、Apollo),可通過內部提供的SPI介面擴充套件其他實現
2.解析配置檔案內容,內建實現yml、properties配置檔案的解析,可通過內部提供的SPI介面擴充套件其他實現
3.通知執行緒池管理模組實現重新整理
執行緒池管理模組:
1.服務啟動時從配置中心拉取配置資訊,生成執行緒池例項註冊到內部執行緒池註冊中心中
2.監聽模組監聽到配置變更時,將變更資訊傳遞給管理模組,實現執行緒池引數的重新整理
3.程式碼中通過getExecutor()方法根據執行緒池名稱來獲取執行緒池物件例項
監控模組:
實現監控指標採集以及輸出,預設提供以下三種方式,也可通過內部提供的SPI介面擴充套件其他實現
1.預設實現Json log輸出到磁碟
2.MicroMeter採集,引入MicroMeter相關依賴
3.暴雷Endpoint端點,可通過http方式訪問
通知告警模組:
對接辦公平臺,實現通告告警功能,預設實現釘釘、企微,可通過內部提供的SPI介面擴充套件其他實現,通知告警型別如下
1.執行緒池引數變更通知
2.阻塞佇列容量達到設定閾值告警
3.執行緒池活性達到設定閾值告警
4.觸發拒絕策略告警
使用
maven依賴
<dependency> <groupId>io.github.lyh200</groupId> <artifactId>dynamic-tp-spring-cloud-starter</artifactId> <version>1.0.2-RELEASE</version> </dependency>
執行緒池配置
spring: dynamic: tp: enabled: true enabledBanner: true # 是否開啟banner列印,預設true enabledCollect: false # 是否開啟監控指標採集,預設false collectorType: logging # 監控資料採集器型別(JsonLog | MicroMeter),預設logging logPath: /home/logs # 監控日誌資料路徑,預設${user.home}/logs monitorInterval: 5 # 監控時間間隔(報警判斷、指標採集),預設5s nacos: # nacos配置,不配置有預設值(規則name-dev.yml這樣) dataId: dynamic-tp-demo-dev.yml group: DEFAULT_GROUP apollo: # apollo配置,不配置預設拿apollo配置第一個namespace namespace: dynamic-tp-demo-dev.yml configType: yml # 配置檔案型別 platforms: # 通知報警平臺配置 - platform: wechat urlKey: 3a7500-1287-4bd-a798-c5c3d8b69c # 替換 receivers: test1,test2 # 接受人企微名稱 - platform: ding urlKey: f80dad441fcd655438f4a08dcd6a # 替換 secret: SECb5441fa6f375d5b9d21 # 替換,非sign模式可以沒有此值 receivers: 15810119805 # 釘釘賬號手機號 executors: # 動態執行緒池配置 - threadPoolName: dynamic-tp-test-1 corePoolSize: 6 maximumPoolSize: 8 queueCapacity: 200 queueType: VariableLinkedBlockingQueue # 任務佇列,檢視原始碼QueueTypeEnum列舉類 rejectedHandlerType: CallerRunsPolicy # 拒絕策略,檢視RejectedTypeEnum列舉類 keepAliveTime: 50 allowCoreThreadTimeOut: false threadNamePrefix: test # 執行緒名字首 notifyItems: # 報警項,不配置自動會配置(變更通知、容量報警、活性報警、拒絕報警) - type: capacity # 報警項型別,檢視原始碼 NotifyTypeEnum列舉類 enabled: true threshold: 80 # 報警閾值 platforms: [ding,wechat] # 可選配置,不配置預設拿上層platforms配置的所以平臺 interval: 120 # 報警間隔(單位:s) - type: change enabled: true - type: liveness enabled: true threshold: 80 - type: reject enabled: true threshold: 1
程式碼方式生成,服務啟動會自動註冊
@Configuration public class DtpConfig { @Bean public DtpExecutor demo1Executor() { return DtpCreator.createDynamicFast("demo1-executor"); } @Bean public ThreadPoolExecutor demo2Executor() { return ThreadPoolBuilder.newBuilder() .threadPoolName("demo2-executor") .corePoolSize(8) .maximumPoolSize(16) .keepAliveTime(50) .allowCoreThreadTimeOut(true) .workQueue(QueueTypeEnum.SYNCHRONOUS_QUEUE.getName(), null, false) .rejectedExecutionHandler(RejectedTypeEnum.CALLER_RUNS_POLICY.getName()) .buildDynamic(); } }
程式碼呼叫,根據執行緒池名稱獲取
public static void main(String[] args) { DtpExecutor dtpExecutor = DtpRegistry.getExecutor("dynamic-tp-test-1"); dtpExecutor.execute(() -> System.out.println("test")); }
注意事項
- 配置檔案配置的引數會覆蓋通過程式碼生成方式配置的引數
- 阻塞佇列只有VariableLinkedBlockingQueue型別可以修改capacity,該型別功能和LinkedBlockingQueue相似,只是capacity不是final型別,可以修改,
VariableLinkedBlockingQueue參考RabbitMq的實現 啟動看到如下日誌輸出證明接入成功
| __ \ (_) |__ __| | | | |_ _ _ __ __ _ _ __ ___ _ ___| |_ __ | | | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \ | |__| | |_| | | | | (_| | | | | | | | (__| | |_) | |_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/ __/ | | | |___/ |_| :: Dynamic Thread Pool :: DynamicTp register, executor: DtpMainPropWrapper(dtpName=dynamic-tp-test-1, corePoolSize=6, maxPoolSize=8, keepAliveTime=50, queueType=VariableLinkedBlockingQueue, queueCapacity=200, rejectType=RejectedCountableCallerRunsPolicy, allowCoreThreadTimeOut=false)
配置變更會推送通知訊息,且會高亮變更的欄位
DynamicTp [dynamic-tp-test-1] refresh end, changed keys: [corePoolSize, queueCapacity], corePoolSize: [6 => 4], maxPoolSize: [8 => 8], queueType: [VariableLinkedBlockingQueue => VariableLinkedBlockingQueue], queueCapacity: [200 => 2000], keepAliveTime: [50s => 50s], rejectedType: [CallerRunsPolicy => CallerRunsPolicy], allowsCoreThreadTimeOut: [false => false]
通知報警
觸發報警閾值會推送相應報警訊息(活性、容量、拒絕),且會高亮顯示相應欄位
配置變更會推送通知訊息,且會高亮變更的欄位
監控日誌
通過collectType屬性配置監控指標採集型別,預設 logging
- MicroMeter:通過引入相關MicroMeter依賴採集到相應的平臺
(如Prometheus,InfluxDb...) Logging:定時採集指標資料以Json日誌格式輸出磁碟,地址${logPath}/dy
namictp/${appName}.monitor.log2022-01-11 00:25:20.599 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8} 2022-01-11 00:25:25.603 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8} 2022-01-11 00:25:30.609 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8} 2022-01-11 00:25:35.613 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8} 2022-01-11 00:25:40.616 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":0,"queueSize":0,"largestPoolSize":0,"poolSize":0,"rejectHandlerName":"RejectedCountableCallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":0,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":0,"dtpName":"remoting-call","maximumPoolSize":8}
暴露EndPoint端點(dynamic-tp),可以通過http方式請求
[ { "dtp_name": "remoting-call", "core_pool_size": 6, "maximum_pool_size": 12, "queue_type": "SynchronousQueue", "queue_capacity": 0, "queue_size": 0, "fair": false, "queue_remaining_capacity": 0, "active_count": 0, "task_count": 21760, "completed_task_count": 21760, "largest_pool_size": 12, "pool_size": 6, "wait_task_count": 0, "reject_count": 124662, "reject_handler_name": "CallerRunsPolicy" }, { "max_memory": "228 MB", "total_memory": "147 MB", "free_memory": "44.07 MB", "usable_memory": "125.07 MB" } ]
專案地址
gitee地址: https://gitee.com/yanhom/dynamic-tp
github地址:https://github.com/lyh200/dynamic-tp-spring-cloud-starter
聯絡作者
對專案有什麼想法或者建議,可以在上述地址中加到作者微信進行交流,或者建立issues,一起完善專案!
最後,支援的話還望大家去點個star哦。