自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

動態(tài)線程池在轉(zhuǎn)轉(zhuǎn)平臺的實踐

開發(fā) 架構(gòu)
動態(tài)線程池自在轉(zhuǎn)轉(zhuǎn)平臺應(yīng)用以來,我們通過日常監(jiān)控及時發(fā)現(xiàn)潛在問題,通過自動容災(zāi)應(yīng)對突發(fā)流量,通過壓測調(diào)優(yōu)提升線程池性能,為轉(zhuǎn)轉(zhuǎn)平臺服務(wù)在多年的618、雙十一活動中保駕護(hù)航,未出現(xiàn)一次因線程池導(dǎo)致的線上事故。

作為一名互聯(lián)網(wǎng)程序員,經(jīng)常需要面對高并發(fā)的場景,為了更好地提高系統(tǒng)的吞吐量和響應(yīng)速度,我們通常采用并發(fā)編程。而線程池技術(shù)也是Java并發(fā)編程中的一個重要組成部分。本文將分享我的Java線程池使用經(jīng)歷,以及Java線程池在轉(zhuǎn)轉(zhuǎn)平臺的實踐。

一.初識線程池

線程池是一種常見的多線程并發(fā)編程技術(shù),它將多個線程組織在一起,以便能夠更有效地管理和控制它們的執(zhí)行。線程池中的每個線程都可以被重復(fù)利用,避免了頻繁地創(chuàng)建和銷毀線程所帶來的開銷,同時還可以限制系統(tǒng)中的線程數(shù)量,從而避免了資源的浪費和競爭。2019年剛參加工作時,我第一次使用線程池是在處理用戶請求,該請求需要聚合多個服務(wù)的數(shù)據(jù),然后返回給用戶。調(diào)用的服務(wù)均比較耗時,如果串行的去調(diào)用那么系統(tǒng)的響應(yīng)時間就會非常長。所以,我決定使用多線程來并行執(zhí)行這個聚合操作,因此也引入了線程池。在Java中線程池是通過java.util.concurrent包提供的ThreadPoolExecutor類來實現(xiàn)的。通過創(chuàng)建ThreadPoolExecutor對象并設(shè)置其參數(shù),線程池運行大致分為4個階段大致如下圖:

圖片

對于剛接觸Java線程池的同學(xué),遇到的第一個問題就是如何合理地設(shè)置線程池參數(shù),以最大限度地發(fā)揮線程池的性能,避免線程池滿載或資源浪費的問題。通過互聯(lián)網(wǎng)我們能收集到各類設(shè)置線程池參數(shù)的建議:

  1. corePoolSize:線程池的核心線程數(shù)應(yīng)該根據(jù)應(yīng)用程序的負(fù)載和硬件資源進(jìn)行調(diào)整。一般來說,它應(yīng)該設(shè)置為處理當(dāng)前負(fù)載的最大線程數(shù)。如果線程數(shù)太少,可能會導(dǎo)致請求排隊,降低響應(yīng)速度;如果線程數(shù)太多,可能會消耗過多的系統(tǒng)資源。
  2. maximumPoolSize:最大線程數(shù)應(yīng)該設(shè)置為系統(tǒng)能夠支持的最大線程數(shù),通常不宜過大。這可以避免系統(tǒng)因線程數(shù)過多而導(dǎo)致的性能下降和資源浪費。
  3. keepAliveTime:該參數(shù)設(shè)置空閑線程的最長存活時間。如果線程池中的線程超過了corePoolSize,且處于空閑狀態(tài)的時間超過了keepAliveTime,這些線程將被終止。這個時間需要根據(jù)應(yīng)用程序的負(fù)載和硬件資源進(jìn)行調(diào)整。如果keepAliveTime設(shè)置太短,可能會導(dǎo)致線程頻繁創(chuàng)建和銷毀,影響性能;如果設(shè)置太長,可能會消耗過多的系統(tǒng)資源。
  4. workQueue:工作隊列用于存儲等待執(zhí)行的任務(wù)。應(yīng)該根據(jù)應(yīng)用程序的負(fù)載和硬件資源選擇適當(dāng)?shù)年犃蓄愋?,比如ArrayBlockingQueue或LinkedBlockingQueue。如果隊列長度太小,可能會導(dǎo)致請求排隊,降低響應(yīng)速度;如果隊列長度太大,可能會消耗過多的系統(tǒng)資源。
  5. rejectedExecutionHandler:拒絕策略用于處理當(dāng)工作隊列已滿,無法接受新任務(wù)時的情況??梢赃x擇一些預(yù)定義的策略,比如AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy或DiscardPolicy。需要根據(jù)實際情況選擇最合適的拒絕策略,以避免任務(wù)丟失或長時間阻塞。

總之,合理地設(shè)置線程池的參數(shù)需要程序員對線程池運行原理有足夠的了解,并且有對應(yīng)用程序的負(fù)載調(diào)優(yōu)和硬件資源調(diào)優(yōu)的經(jīng)驗,顯然這是非常困難得。因此我最終選擇中庸的配置方法,根據(jù)IO密集型來設(shè)置線程數(shù)為CPUs*2,根據(jù)平均任務(wù)時長與QPS來預(yù)估隊列長度為1000,設(shè)置完畢上線且能夠正常運行,就這樣我與線程池的相遇如此簡單的結(jié)束了。

二.優(yōu)化與實踐

轉(zhuǎn)眼間來到2021年,隨著業(yè)務(wù)發(fā)展App使用的人數(shù)越來越多,對服務(wù)性能的要求也越來越高。因此我們在618前對服務(wù)進(jìn)行全鏈路壓測,在壓測中線程池出現(xiàn)以下問題:

  1. 線程池大小不足:線程池大小不足可能導(dǎo)致請求無法得到處理,進(jìn)而影響系統(tǒng)性能。
  2. 線程池大小過大:線程池大小過大可能會導(dǎo)致系統(tǒng)資源消耗過度,影響系統(tǒng)的穩(wěn)定性和性能。
  3. 隊列滿了:如果任務(wù)隊列滿了,新的請求將被拒絕,可能會導(dǎo)致請求失敗。
  4. 任務(wù)執(zhí)行時間過長:任務(wù)執(zhí)行時間過長,影響線程池中其他任務(wù)的執(zhí)行,進(jìn)而影響系統(tǒng)性能。
  5. 線程池互擾:服務(wù)中存在多個線程池,其中一個線程池占用資源過的,造成其它線程池性能下降。

對這些問題進(jìn)行復(fù)盤可以發(fā)現(xiàn)在實際應(yīng)用中,即使是微服務(wù)架構(gòu)的同一個模塊中由于業(yè)務(wù)的復(fù)雜性也需要引入多個線程池來進(jìn)行業(yè)務(wù)隔離,而不同的業(yè)務(wù)場景也需要對線程池參數(shù)進(jìn)行不同的設(shè)置。比如用戶請求場景需要更大的核心線程數(shù)來進(jìn)行快速響應(yīng),數(shù)據(jù)導(dǎo)出場景需要更大的隊列來緩解大量的導(dǎo)出任務(wù),突發(fā)流量場景需要更大的最大線程數(shù)和任務(wù)隊列等等。而為了找到合適各場景的參數(shù)值,我們需要重復(fù)進(jìn)行壓測、調(diào)整參數(shù)、上線的過程,消耗大量的人力物力。最終我們將遇到的問題歸納為兩方面:

  • 線程池參數(shù)調(diào)整依賴代碼上線,非常耗時
  • 線程池運行情況黑盒,無法準(zhǔn)確的進(jìn)行調(diào)優(yōu)

為解決這些問題我們設(shè)計并實現(xiàn)一套可動態(tài)調(diào)整可監(jiān)控的線程池,具體設(shè)計與實現(xiàn)如下。

2.1 整體架構(gòu)

動態(tài)線程池主要包含客戶端、監(jiān)控平臺、配置后臺三部分:

  1. 客戶端部分是線程池主體部分,動態(tài)線程池通過繼承ThreadPoolExecutor來實現(xiàn),保留了Java原生線程池所有的能力,并為業(yè)務(wù)服務(wù)提供線程池創(chuàng)建、注冊、預(yù)熱和參數(shù)更新的能力。
  2. 配置后臺主要負(fù)責(zé)管理線程池配置修改及配置下發(fā),可對線程池核心參數(shù)corePoolSize、maximumPoolSize、workQueueCapacity進(jìn)行動態(tài)修改,無需業(yè)務(wù)服務(wù)上線。為了能夠在線程池出現(xiàn)異常時自動切換備用參數(shù)方案,我們最終采用配置后臺為實現(xiàn)方案。如無此需求可使用Apollo,Nacos等配置中心實現(xiàn)成本更小。
  3. 監(jiān)控報警平臺主要負(fù)責(zé)線程池運行狀態(tài)的監(jiān)控,可對線程池的線程池活躍度,隊列飽和度,隊列阻塞耗時進(jìn)行監(jiān)控和報警。使得程序員能夠?qū)€程池的運行情況進(jìn)行直觀的觀察。

圖片

2.2 動態(tài)參數(shù)實現(xiàn)

動態(tài)參數(shù)調(diào)整主要依賴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);

綜合考慮需求和風(fēng)險我們最終選擇使用set方法實現(xiàn)對corePoolSize,maximumPoolSize的動態(tài)調(diào)整,setCorePoolSize和setMaximumPoolSize方法能夠直接對當(dāng)前線程池進(jìn)行賦值,并且能夠自動調(diào)整線程數(shù)。若當(dāng)前值大于修改值,通過標(biāo)記中斷的方式回收多余線程。若當(dāng)前值小于修改值,setMaximumPoolSize值進(jìn)行賦值不操作線程,setCorePoolSize會取排隊的任務(wù)數(shù)和修改差值的最小值,來新增對應(yīng)數(shù)量的核心線程數(shù)。可以看出set方法能夠平穩(wěn)的進(jìn)行參數(shù)的修改。這樣解決了線程數(shù)的動態(tài)調(diào)整問題,但ThreadPoolExecutor不提供對工作隊列的動態(tài)調(diào)整。重新回顧訴求我們只是想要能夠調(diào)整工作隊列的大小而不是替換線程池的工作隊列,因此我們基于LinkedBlockingQueue實現(xiàn)長度可調(diào)的工作隊列。最終實現(xiàn)效果如下圖:

圖片

2.3 線程池監(jiān)控實現(xiàn)

同樣的線程池監(jiān)控也依賴于ThreadPoolExecutor提供的如下的get方法:

public int getActiveCount();
public BlockingQueue<Runnable> getQueue;
public int getCorePoolSize();
public int getMaximumPoolSize();
public long getTaskCount();

通過這些get方法可以實時的獲取到線程池的運行數(shù)據(jù),將這些數(shù)據(jù)上報監(jiān)控與報警平臺便可讓程序員實時查看具體數(shù)據(jù)。具體的實現(xiàn)方式可以分為兩種:

  • 通過重寫ThreadPoolExecutor中的beforeExecute(),afterExecute()方法,在任務(wù)執(zhí)行前后上報數(shù)據(jù),便可完成監(jiān)控。
  • 通過繼承ThreadPoolExecutor并重載對應(yīng)的方法增加監(jiān)控代碼,來進(jìn)行監(jiān)控數(shù)據(jù)數(shù)據(jù)上報。

對線程池的監(jiān)控主要是對工作線程和工作隊列進(jìn)行監(jiān)控,因此我們整理如下監(jiān)控指標(biāo):

指標(biāo)

方案

作用

線程池活躍度

activeCount /maximumPoolSize

用于描述線程池負(fù)載情況

隊列飽和度

queueSize / queueCapacity

用戶描述工作隊列負(fù)載情況

任務(wù)阻塞阻塞時間

executeStartTime-inQueueTime

用戶描述任務(wù)排隊情況

最終監(jiān)控報警效果:

圖片

3.總結(jié)

動態(tài)線程池自在轉(zhuǎn)轉(zhuǎn)平臺應(yīng)用以來,我們通過日常監(jiān)控及時發(fā)現(xiàn)潛在問題,通過自動容災(zāi)應(yīng)對突發(fā)流量,通過壓測調(diào)優(yōu)提升線程池性能,為轉(zhuǎn)轉(zhuǎn)平臺服務(wù)在多年的618、雙十一活動中保駕護(hù)航,未出現(xiàn)一次因線程池導(dǎo)致的線上事故。希望本文能夠幫助到遇到同樣問題的同學(xué)們。

關(guān)于作者

武翱,轉(zhuǎn)轉(zhuǎn)-平臺技術(shù)部-后端開發(fā)。

責(zé)任編輯:武曉燕 來源: 轉(zhuǎn)轉(zhuǎn)技術(shù)
點贊
收藏

51CTO技術(shù)棧公眾號