RM新时代网站-首页

                0
                • 聊天消息
                • 系統消息
                • 評論與回復
                登錄后你可以
                • 下載海量資料
                • 學(xué)習在線(xiàn)課程
                • 觀(guān)看技術(shù)視頻
                • 寫(xiě)文章/發(fā)帖/加入社區
                會(huì )員中心
                創(chuàng )作中心

                完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

                3天內不再提示

                動(dòng)態(tài)線(xiàn)程池思想學(xué)習及實(shí)踐

                京東云 ? 來(lái)源:jf_75140285 ? 作者:jf_75140285 ? 2024-06-13 15:43 ? 次閱讀

                相關(guān)文檔

                美團線(xiàn)程池實(shí)踐:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 線(xiàn)程池思想解析:https://www.javadoop.com/post/java-thread-pool?

                引言

                在后臺項目開(kāi)發(fā)過(guò)程中,我們常常借助線(xiàn)程池來(lái)實(shí)現多線(xiàn)程任務(wù),以此提升系統的吞吐率和響應性;而線(xiàn)程池的參數配置卻是一個(gè)難以合理評估的值,雖然業(yè)界也針對cpu密集型,IO密集型等場(chǎng)景給出了一些參數配置的經(jīng)驗與方案,但是實(shí)際業(yè)務(wù)場(chǎng)景中通常會(huì )因為流量的隨機性,業(yè)務(wù)的更迭性等情況出現預計和實(shí)際運行情況偏差較大的情況;而不合理的線(xiàn)程池參數,可能導致服務(wù)器負載升高,服務(wù)不可用,內存溢出等嚴重問(wèn)題;一旦遇到參數不合理的問(wèn)題,還需要重新上線(xiàn)修改,并且存在反復修改的情況,而這期間花費的時(shí)間可能帶來(lái)更大的風(fēng)險,甚至導致嚴重業(yè)務(wù)事故;那么有沒(méi)有一種方式能有效感知上述問(wèn)題并及時(shí)避免以上問(wèn)題呢?或許動(dòng)態(tài)線(xiàn)程池可以。

                什么是動(dòng)態(tài)線(xiàn)程池

                簡(jiǎn)單來(lái)說(shuō),動(dòng)態(tài)線(xiàn)程池就是能在不重新部署應用的情況下動(dòng)態(tài)實(shí)時(shí)變更其核心參數,并且能對其核心參數及運行狀態(tài)進(jìn)行監控及告警;以便開(kāi)發(fā)人員可以及時(shí)感知到實(shí)際業(yè)務(wù)中因為各種隨機情況導致線(xiàn)程池異常的場(chǎng)景,并依據動(dòng)態(tài)變更能力快速調整并驗證參數的合理性。

                為什么需要動(dòng)態(tài)線(xiàn)程池,存在什么痛點(diǎn)

                線(xiàn)程池在給我們業(yè)務(wù)帶來(lái)性能和吞吐提升的同時(shí),也存在諸多風(fēng)險和問(wèn)題,其中主要原因就在于我們難以設置出合理的線(xiàn)程池參數,一方面線(xiàn)程池的運行機制不是很好理解,配置合理強依賴(lài)開(kāi)發(fā)人員的個(gè)人經(jīng)驗和知識;另一方面,線(xiàn)程池執行的情況和任務(wù)類(lèi)型相關(guān)性較大,同時(shí)實(shí)際場(chǎng)景中流量的隨機性,業(yè)務(wù)的更迭性也導致業(yè)界難以有一套成熟或開(kāi)箱即用的經(jīng)驗策略來(lái)幫助開(kāi)發(fā)人員參考。而線(xiàn)程池參數難以合理設置的特性又不得不讓我們關(guān)注以下三個(gè)痛點(diǎn)問(wèn)題:

                1.運行情況難感知:在業(yè)務(wù)使用線(xiàn)程池的過(guò)程中,線(xiàn)程池的運行情況對于開(kāi)發(fā)人員來(lái)說(shuō)很難感知,我們難以知道每個(gè)線(xiàn)程池創(chuàng )建了多少個(gè)線(xiàn)程,是否有隊列積壓,線(xiàn)程池運行狀態(tài)怎么樣,線(xiàn)程池是否已經(jīng)耗盡... 直到出現線(xiàn)上問(wèn)題或收到客訴才后知后覺(jué);(我們能否對系統中用到的線(xiàn)程池進(jìn)行一個(gè)整體的把控,在線(xiàn)程池任務(wù)積壓,任務(wù)拒絕等問(wèn)題發(fā)生時(shí),甚至問(wèn)題發(fā)生前進(jìn)行及時(shí)感知,讓開(kāi)發(fā)人員能未雨綢繆,盡早發(fā)現和解決問(wèn)題呢?-線(xiàn)程池監控,異常告警)

                流量突增導致預估和實(shí)際情況偏差較大,同時(shí)由于未能及時(shí)感知并解決積壓情況,最終引發(fā)客訴 case1:廣告主大批量刪除物料后異步清理附屬表出現任務(wù)積壓 問(wèn)題描述:廣告主批量刪除計劃物料后,對應物料附屬表數據未及時(shí)刪除,導致廣告主關(guān)鍵詞等物料數上限得不到釋放而影響創(chuàng )建新物料,引發(fā)線(xiàn)上客訴。 問(wèn)題原因:廣告主刪除計劃物料后,系統會(huì )同步刪除計劃物料主表信息,然后通過(guò)線(xiàn)程池的方式異步刪除計劃物料附屬表數據。臨近大促廣告主物料增刪頻率及單次批量操作的物料數量都有明顯增加,由于核心線(xiàn)程設置較小同時(shí)隊列設置過(guò)長(cháng),導致計劃主表同步刪除后異步刪除附屬表的任務(wù)出現隊列積壓,對應的關(guān)鍵詞等物料數上限得不到釋放而影響新物料創(chuàng )建,引發(fā)線(xiàn)上客訴。

                ?

                2.線(xiàn)程拒絕難定位:當拒絕發(fā)生后,即使我們迅速感知到了線(xiàn)程池運行異常,也經(jīng)常會(huì )因為拒絕持續時(shí)間較短而拿不到問(wèn)題發(fā)生時(shí)的線(xiàn)程堆棧,因此通常難以快速定位甚至無(wú)法定位到是哪里的原因導致的拒絕,比如是流量的突增將線(xiàn)程池打滿(mǎn),還是某個(gè)業(yè)務(wù)邏輯耗時(shí)較長(cháng)將線(xiàn)程池中的線(xiàn)程拖??;(我們有沒(méi)有一種方式能在線(xiàn)程池拒絕后去更容易的定位到問(wèn)題呢?-自動(dòng)觸發(fā)線(xiàn)程池堆棧打印,分析工具)

                case2: 線(xiàn)程池拒絕具有隨機性,當拒絕時(shí)長(cháng)較短時(shí),難以定位問(wèn)題原因 問(wèn)題描述:某業(yè)務(wù)接口內部計算邏輯較多,且存在多處外部接口調用邏輯,上線(xiàn)后不定時(shí)出現線(xiàn)程池拒絕異常,由于持續時(shí)間不長(cháng),問(wèn)題發(fā)生后無(wú)法通過(guò)jstack去獲取問(wèn)題發(fā)生時(shí)現場(chǎng)的線(xiàn)程堆棧, 很難定位是什么原因導致了線(xiàn)程池拒絕;由于沒(méi)有較好的排查手段,只能通過(guò)逐步摟日志的方式排查,而排查過(guò)程又可能因為日志較多或者日志不全出現問(wèn)題定位時(shí)間長(cháng)或者是根本無(wú)法定位的情況。 問(wèn)題原因:某外部某接口不穩定,在性能較差且流量較大時(shí)就容易把調用線(xiàn)程池打滿(mǎn),導致可用率下降

                ?

                3.參數問(wèn)題難以快速調整:在定位到某個(gè)線(xiàn)程池參數設置不合理的情況后,我們需要根據情況隨即進(jìn)行調整,但是"修改->打包->審批->發(fā)布"的時(shí)間很可能會(huì )擴大問(wèn)題的影響甚至是事故嚴重程度;同時(shí)因為線(xiàn)程池參數難以合理設置的原因,可能導致我們要重復進(jìn)行上述"修改->打包->審批->發(fā)布"的流程...(有沒(méi)有一種方法能快速修改并驗證參數設置的合理性呢?-參數動(dòng)態(tài)調整)

                線(xiàn)程池參數設置不合理,難以快速調整參數,業(yè)務(wù)風(fēng)險上升 case3:應用JSF接口修改為異步調用后出現可用率下降 問(wèn)題描述:將應用中部分JSF接口切換為異步模式后,對應可用率有明顯下降 問(wèn)題原因:在修改為異步模式的JSF接口中,部分業(yè)務(wù)在拿到future對象后使用ThenApply做了一些耗時(shí)的操作,另外還有一部分在ThenApply里面又調用了另外一個(gè)異步方法;而thenApply的執行會(huì )使用jsf的callBack線(xiàn)程池,由于線(xiàn)程池線(xiàn)程配置較小,并且部分回調方法耗時(shí)較長(cháng),導致callBack線(xiàn)程池被打滿(mǎn),子任務(wù)請求線(xiàn)程時(shí)進(jìn)入阻塞隊列排隊,出現接口超時(shí)可用率下降。

                業(yè)界動(dòng)態(tài)線(xiàn)程池動(dòng)態(tài)線(xiàn)程池調研

                當前業(yè)界已存在部分動(dòng)態(tài)線(xiàn)程池組件,其主體功能及大體思想類(lèi)似,但存在以下幾個(gè)問(wèn)題

                1.與外部中間件耦合較多,難以二次開(kāi)發(fā)加以使用;

                2.使用靈活性受限,難以根據業(yè)務(wù)自身特點(diǎn)進(jìn)行定制化(自動(dòng)觸發(fā)線(xiàn)程池堆棧打印,一鍵清空隊列,callback線(xiàn)程池等)

                綜合考慮上述問(wèn)題,決定結合公司中間件及自身業(yè)務(wù)特點(diǎn)實(shí)現一套集線(xiàn)程池監控,異常告警,線(xiàn)程棧自動(dòng)獲取,動(dòng)態(tài)刷新為一體的動(dòng)態(tài)線(xiàn)程池組件。

                如何實(shí)現動(dòng)態(tài)線(xiàn)程池

                整體方案

                wKgaomZqouqAcvrSAAEC2xF7olE477.png

                線(xiàn)程池監控及告警

                要實(shí)現線(xiàn)程池監控及告警,我們需要關(guān)注以下幾個(gè)要點(diǎn)

                1.如何獲取到待監控的線(xiàn)程池信息

                在實(shí)際業(yè)務(wù)中我們通常想要知道應用中有哪些線(xiàn)程池,每個(gè)線(xiàn)程池各個(gè)參數在每個(gè)時(shí)刻的運行情況是怎么樣的;對于第一種場(chǎng)景,我們可以構建一個(gè)線(xiàn)程池管理器,用于管理應用中使用到的業(yè)務(wù)線(xiàn)程池,為此我們可以在應用初始化時(shí)將這些線(xiàn)程池按名稱(chēng)和實(shí)際對象注冊到管理器;后續使用時(shí)就可以根據名稱(chēng)從管理中心拉取到對應線(xiàn)程池;

                public class ThreadPoolManager {
                    // 線(xiàn)程池管理器
                    private static final ConcurrentHashMap REGISTER_MAP_BY_NAME = new ConcurrentHashMap();
                    private static final ConcurrentHashMap REGISTER_MAP_BY_EXECUTOR = new ConcurrentHashMap();
                
                    // 注冊線(xiàn)程池 
                    public static void registerExecutor(String threadPoolName, Executor executor) {
                        REGISTER_MAP_BY_NAME.putIfAbsent(threadPoolName, executor);
                        REGISTER_MAP_BY_EXECUTOR.putIfAbsent(executor, threadPoolName);
                    }
                
                    // 根據名稱(chēng)獲取線(xiàn)程池
                    public static Executor getExecutorByName(String threadPoolName) {
                        return REGISTER_MAP_BY_NAME.get(threadPoolName);
                    }
                
                    // 根據線(xiàn)程池獲取名稱(chēng)
                    public static String getNameByExecutor(Executor executor) {
                        return REGISTER_MAP_BY_EXECUTOR.get(executor);
                    }
                    
                    // 獲取所有線(xiàn)程池名稱(chēng)
                    public static Set getAllExecutorNames() {
                        return REGISTER_MAP_BY_NAME.keySet();
                    }
                }

                對于第二種場(chǎng)景,線(xiàn)程池的核心實(shí)現類(lèi)ThreadPoolExecutor提供了多個(gè)參數查詢(xún)方法,我們可以借助這些方法查詢(xún)某一時(shí)刻該線(xiàn)程池的運行快照

                getCorePoolSize() // 核心線(xiàn)程數
                getMaximumPoolSize() // 最大線(xiàn)程數
                getQueue() // 阻塞隊列,獲取隊列大小,容量等
                getActiveCount() // 活躍線(xiàn)程數
                getTaskCount() // 歷史已完成和正在執行的任務(wù)數量
                getCompletedTaskCount() // 已完成任務(wù)數

                2.如何將監控的信息保存和展示出來(lái)

                監管了應用中的業(yè)務(wù)線(xiàn)程池,也能獲取到某一時(shí)刻各線(xiàn)程池的運行情況快照,但要實(shí)現線(xiàn)程池數據監控還需要我們在每個(gè)時(shí)刻去采集線(xiàn)程池運行信息,并將其保存下來(lái),同時(shí)還需要將這些數據用一個(gè)可視化頁(yè)面展示出來(lái)供我們觀(guān)察才行,否則我們只知道某一時(shí)刻的線(xiàn)程池情況也意義不大。為此,我們需要考慮上面看到的過(guò)程,例如使用Micrometer采集性能數據,使用Prometheus時(shí)序數據庫存儲指標數據,使用Grafana展示數據;而現在,我們只需要根據pfinder的埋點(diǎn)要求將對應要監控的線(xiàn)程池指標配置到上報邏輯即可,剩下的數據分時(shí)采集,數據存儲,數據展示可以完全交給pfinder來(lái)完成。

                // 已經(jīng)設置埋點(diǎn)的線(xiàn)程池
                public static ConcurrentHashSet monitorThreadPool = new ConcurrentHashSet();
                
                // 監控埋點(diǎn)注冊
                public static void monitorRegister() {
                    log.info("===> monitor register start...");
                    // 1.獲取所有線(xiàn)程池
                    Set allExecutorNames = ThreadPoolManager.getAllExecutorNames();
                    // 2.遍歷線(xiàn)程池,注冊埋點(diǎn)
                    allExecutorNames.forEach(executorName-> {
                        if (!monitorThreadPool.contains(executorName)) {
                            monitorThreadPool.add(executorName);
                            Executor executor = ThreadPoolManager.getExecutorByName(executorName);
                            collect(executor, executorName);
                        }
                    });
                    log.info("===> monitor register end...");
                }
                
                // pfinder指標埋點(diǎn)
                public static void collect(Executor executorService, String threadPoolName) {
                    ThreadPoolExecutor executor = (ThreadPoolExecutor)executorService;
                    String prefix = "thread.pool."+threadPoolName;
                    gauge1 = PfinderContext.getMetricRegistry().gauges(prefix)
                            .gauge(() -> executor.isShutdown() ? 0 : executor.getCorePoolSize())
                            .tags(MetricTag.of("type_dimension", "core_size")).build();
                    gauge2 = PfinderContext.getMetricRegistry().gauges(prefix)
                            .gauge(() -> executor.isShutdown() ? 0 : executor.getMaximumPoolSize())
                            .tags(MetricTag.of("type_dimension", "max_size"))
                            .build();
                    gauge4 = PfinderContext.getMetricRegistry().gauges(prefix)
                            .gauge(() -> executor.isShutdown() ? 0 : executor.getQueue().size())
                            .tags(MetricTag.of("type_dimension", "queue_size"))
                            .build();
                }

                3.如何監聽(tīng)到異常并告警

                線(xiàn)程池運行過(guò)程中,我們可能更多關(guān)注線(xiàn)程池拒絕前感知線(xiàn)程池隊列是否有積壓,線(xiàn)程數是否已達設置核心或最大線(xiàn)程數高點(diǎn)以及線(xiàn)程池拒絕異常;由于使用pfinder作為線(xiàn)程池監控組件,其中線(xiàn)程池隊列是否有積壓,線(xiàn)程數是否已達設置核心,最大線(xiàn)程數高點(diǎn)等異常監聽(tīng)及告警可以直接依賴(lài)pfinder的告警配置來(lái)實(shí)現; 例如下圖中配置隊列積壓超過(guò)閾值時(shí)的報警

                wKgZomZqouuAIox-AAIKR_J_yos770.png

                而線(xiàn)程池拒絕異常,我們可以在線(xiàn)程池初始化時(shí)包裝線(xiàn)程池的拒絕策略,在執行實(shí)際拒絕策略前拋出告警;

                @Slf4j
                public class RejectInvocationHandler implements InvocationHandler {
                
                    private final Object target;
                
                    @Value("${jtool.pool.reject.alarm.key}")
                    private String key;
                
                    public RejectInvocationHandler(Object target) {
                        this.target = target;
                    }
                
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        ExecutorService executor = (ExecutorService)args[1];
                        if (Strings.equals(method.getName(), "rejectedExecution")) {
                            try {
                                rejectBefore(executor);
                            }
                            catch (Exception exp) {
                                log.error("==> Exception while do rejectBefore for pool [{}]", executor, exp);
                            }
                        }
                        return method.invoke(target, args);
                    }
                
                    private void rejectBefore(ExecutorService executor) {
                        // 觸發(fā)報警  
                        rejectAlarm(executor); 
                    }
                
                    /**
                     * 拒絕報警
                     */
                    private void rejectAlarm(ExecutorService executor) {
                        String alarmKey = Objects.nonNull(key) ? key : ThreadPoolConst.UMP_ALARM_KEY;
                        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)executor;
                        String threadPoolName = ThreadPoolManager.getNameByExecutor(threadPoolExecutor);
                        String errorMsg = String.format("===> 線(xiàn)程池拒絕報警 key: [%s], cur executor: [%s], core size: [%s], max size: [%s], queue size: [%s], curQueue size: [%s]",
                                alarmKey, threadPoolName, threadPoolExecutor.getCorePoolSize(), threadPoolExecutor.getMaximumPoolSize(), threadPoolExecutor.getQueue().size()+threadPoolExecutor.getQueue().remainingCapacity(), threadPoolExecutor.getQueue().size());
                        log.error(errorMsg);
                        Profiler.businessAlarm(alarmKey, errorMsg);
                    }
                
                }
                
                

                自動(dòng)觸發(fā)線(xiàn)程堆棧打印

                感知到拒絕線(xiàn)程池拒絕異常后,我們需要及時(shí)去定位線(xiàn)程池拒絕原因,但現在我們可能只知道是哪個(gè)線(xiàn)程池發(fā)生了線(xiàn)程池拒絕異常,卻難以知道是什么原因導致的,我們常常希望在線(xiàn)程池拒絕時(shí)能拿到應用的線(xiàn)程堆棧信息,并依據其分析拒絕原因;但是線(xiàn)程拒絕常常發(fā)生速度很快,我們很難捕捉到拒絕時(shí)刻的全局線(xiàn)程堆??煺?;為此,我們考慮在線(xiàn)程池拒絕發(fā)生時(shí)自動(dòng)觸發(fā)線(xiàn)程池堆棧打印到日志;

                public class RejectInvocationHandler implements InvocationHandler {
                ...
                private void rejectBefore(ExecutorService executor) {
                        // 打印線(xiàn)程堆棧到日志的間隔條件
                        if (CommonProperty.canPrintStackTrace()) { 
                            // 觸發(fā)報警 
                            rejectAlarm(executor); 
                            // 觸發(fā)線(xiàn)程堆棧打印 
                            printThreadStack(executor); 
                        }
                    }
                ...
                }
                
                ...
                /**
                 * 打印線(xiàn)程堆棧信息
                 */
                public static void printThreadStack(Executor executor) {
                    if (!CommonProperty.logPrintFlag) {
                        log.info("===> 線(xiàn)程池堆棧打印關(guān)閉:[{}]", CommonProperty.logPrintFlag);
                        return;
                    }
                    logger.info("n=================>>> 線(xiàn)程池拒絕堆棧打印start,觸發(fā)提交拒接處理的線(xiàn)程池:【{}】", executor);
                    Map allStackTraces = Thread.getAllStackTraces();
                    log.info("===> allStackTraces size :[{}]", allStackTraces.size());
                    StringBuilder stringBuilder = new StringBuilder();
                    allStackTraces.entrySet().stream()
                            .sorted(Comparator.comparing(entry -> entry.getKey().getName()))
                            .forEach(threadEntry -> {
                                Thread thread = threadEntry.getKey();
                                stringBuilder.append(Strings.format("n線(xiàn)程:[{}] 時(shí)間: [{}]n", thread.getName(), new Date()));
                                stringBuilder.append(Strings.format("tjava.lang.Thread.State: {}n", thread.getState()));
                                StackTraceElement[] stack = threadEntry.getValue();
                                for (StackTraceElement stackTraceElement : stack) {
                                    stringBuilder.append("tt").append(stackTraceElement.toString()).append("n");
                                }
                                stringBuilder.append("n");
                                logger.info(stringBuilder.toString());
                                stringBuilder.delete(0, stringBuilder.length());
                            });
                    logger.info("==============>>> end");
                }
                ...

                打印后的信息如下所示

                wKgaomZqouyAA5FUAAvh9Tc5rWE238.png

                拿到問(wèn)題發(fā)生時(shí)的堆棧信息后,我們就可以根據拒絕線(xiàn)程的名稱(chēng)去分析拒絕原因了,看看是否有什么原因導致線(xiàn)程被卡??;為了更方便的分析,可以根據簡(jiǎn)單的根據日志規則去分析拒絕線(xiàn)程池問(wèn)題發(fā)生時(shí)各線(xiàn)程池的運行狀態(tài)是什么,大部分都集中到了哪個(gè)方法的哪個(gè)位置

                public class ThreadLogAnalyzer {
                    public static void main(String[] args) {
                        String logFilePath = "/Users/huyongjia1/Desktop/huyongjia/demo/jsfdemo/MyJtool/src/main/resources/reject.monitor 21.log";
                        String threadPoolNameLike = "simpleTestExecutor";
                        int threadCount = 0;
                        HashMap statusMap = new HashMap();
                        HashMap methodMap = new HashMap();
                
                        try (BufferedReader br = new BufferedReader(new FileReader(logFilePath))) {
                            String line;
                            while ((line = br.readLine()) != null) {
                                if (line.contains("=================>>> 線(xiàn)程池拒絕堆棧打印start,觸發(fā)提交拒接處理的線(xiàn)程池")) {
                                    System.out.println("開(kāi)始讀整個(gè)線(xiàn)程");
                                }
                                if (line.contains(threadPoolNameLike)) {
                                    threadCount++;
                                    String curStatus = br.readLine();
                                    if (curStatus.contains("java.lang.Thread.State")) {
                                        statusMap.put(curStatus, (statusMap.getOrDefault(curStatus, 0) + 1));
                                        String methodTrace = br.readLine();
                                        methodMap.put(methodTrace, (methodMap.getOrDefault(methodTrace, 0) + 1));
                                    }
                                }
                                if (line.contains("==============>>> end")) {
                                    System.out.println("結束讀整個(gè)線(xiàn)程");
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                
                        System.out.println(Strings.format("===> 當前線(xiàn)程名[{}]共計:{}", threadPoolNameLike, threadCount));
                        System.out.println("n===> 狀態(tài)分析結果:");
                        for (Map.Entry statusEntry : statusMap.entrySet()) {
                            System.out.println(Strings.format("t {} {}", statusEntry.getKey(), statusEntry.getValue()));
                        }
                        System.out.println("n===> 方法分析結果:");
                        ArrayList> methodEntryList = Lists.newArrayList(methodMap.entrySet());
                        methodEntryList.sort(new Comparator>() {
                            @Override
                            public int compare(Map.Entry o1, Map.Entry o2) {
                                return o2.getValue() - o1.getValue();
                            }
                        });
                        for (Map.Entry methodEntry : methodEntryList) {
                            System.out.println(Strings.format("{} {} {}%", methodEntry.getKey(), methodEntry.getValue(), (methodEntry.getValue() / (double)threadCount) * 100));
                        }
                    }
                }

                例如當前收到simpleTestExecutor線(xiàn)程池拒絕告警,利用堆棧信息分析如下,可以看到該線(xiàn)程池共4個(gè)線(xiàn)程,其中3個(gè)的運行狀態(tài)為T(mén)IMED_WAITING,并且都停在了sleep邏輯處

                wKgZomZqou2AS8GAAACdZ5EU3-o479.png

                線(xiàn)程池參數動(dòng)態(tài)刷新

                要實(shí)現線(xiàn)程池參數動(dòng)態(tài)刷新,我們需要關(guān)注以下幾個(gè)要點(diǎn):

                1.哪些參數需要變更

                在使用線(xiàn)程池時(shí),我們通常需要配置多個(gè)參數,但是實(shí)際上我們只需要靈活配置好corePoolSize(核心線(xiàn)程數),maximumPoolSize(最大線(xiàn)程數),workQueue(隊列長(cháng)度)這三個(gè)核心參數就可以應對大部分場(chǎng)景了;

                2.運行中的線(xiàn)程池如何變更參數

                從前面我們可以知道線(xiàn)程池的核心實(shí)現類(lèi)ThreadPoolExecutor提供了改變corePoolSize,maximumPoolSize的兩個(gè)快捷方法:

                1. setCorePoolSize(int corePoolSize)

                2. setMaximumPoolSize(int maximumPoolSize)

                我們只需要通過(guò)rpc或者http的方式將想要變更的參數傳遞到應用再利用上述方法設置進(jìn)去即可;而隊列長(cháng)度的變更卻相對麻煩點(diǎn),因為我們常使用的阻塞隊列LinkedBlockingQueue將隊列大小設置為成了一個(gè)final類(lèi)型的變量,我們無(wú)法快捷變更,那該怎么辦呢,其中一個(gè)思想就是自定義一個(gè)LinkedBlockQueue,修改capacity為非final類(lèi)型,同時(shí)考慮并發(fā)問(wèn)題對其中涉及到的方法進(jìn)行修改;(可參考RabbitMq中的VariableLinkedBlockingQueue)

                3.應用集群場(chǎng)景下如何實(shí)現一鍵參數變更

                實(shí)際情況下,我們的應用是已集群的方式部署的,這時(shí)我們可以借助ducc全局配置工具將要變更的參數傳遞到集群的各個(gè)機器,各機器根據再根據參數中的線(xiàn)程池名稱(chēng)去線(xiàn)程池管理中心拿到對應的線(xiàn)程進(jìn)行參數變更即可;

                /**
                 * ducc控制線(xiàn)程池刷新方法, 需要動(dòng)態(tài)刷新的線(xiàn)程池信息列表,舉例如下:
                 * value:
                 * [
                 *   {
                 *     "threadPoolName": "my_pool",
                 *     "corePoolSize": "10",
                 *     "maximumPoolSize": "20",
                 *     "queueCapacity": "100"
                 *   }
                 * ]
                 */
                @LafValue("jtool.pool.refresh")
                public void refresh(@JsonConverter List threadPoolProperties) {
                    String jsonString = JSON.toJSONString(threadPoolProperties);
                    log.info("===> refresh thread pool properties [{}]", jsonString);
                    threadPoolProperties = JSONObject.parseArray(jsonString, ThreadPoolProperties.class);
                    refresh(threadPoolProperties);
                }
                
                public static boolean refresh(List threadPoolProperties) {
                    if (Objects.isNull(threadPoolProperties)) {
                        log.warn("refresh param is empty!");
                        return false;
                    }
                    log.info("Executor refresh param: [{}]", threadPoolProperties);
                    // 1.根據參數獲取對應的線(xiàn)程池
                    threadPoolProperties.forEach(threadPoolProperty -> {
                        String threadPoolName = threadPoolProperty.getThreadPoolName();
                        Executor executor = ThreadPoolManager.getExecutorByName(threadPoolName);
                        if (Objects.isNull(executor)) {
                            log.warn("Register not find this executor: {}", threadPoolName);
                            return;
                        }
                        // 2. 線(xiàn)程池刷新
                        refreshExecutor(executor, threadPoolName, threadPoolProperty);
                        log.info("Refresh thread pool finish, threadPoolName: [{}]", threadPoolName);
                    });
                    return true;
                }
                
                

                實(shí)踐效果

                線(xiàn)程池監控

                達成目的:對應用中的線(xiàn)程池情況做整體把控,能方便獲取各線(xiàn)程池的運行情況

                接入pfinder監控后的效果如下

                1.pfinder監控:注冊的監控線(xiàn)程池會(huì )自動(dòng)上報線(xiàn)程池的活躍,核心,最大,隊列大小,隊列容量,任務(wù)數等指標;監控地址在pfinder當前服務(wù)的業(yè)務(wù)監控中,正確接入后可以看到已經(jīng)被監控的線(xiàn)程池列表,監控埋點(diǎn)名稱(chēng): thread.pool.{線(xiàn)程池名稱(chēng)}

                wKgaomZqou6AeNRJAAT5FmKylQY652.png

                如果要看某一個(gè)線(xiàn)程池的指標數據,可以單獨進(jìn)入某個(gè)線(xiàn)程池的監控,其中通過(guò)數據選擇要看的指標(type_dimension)

                wKgZomZqovCAEYAbAAeSjvXYDz4006.png

                可在展示維度中選擇要具體展示的維度,比如分組,實(shí)例等

                wKgaomZqovKAMhcXAApQLGi3FfA613.png

                1.當前監控數據為分鐘級維度,即按分鐘粒度展示線(xiàn)程池參數的瞬時(shí)值,如果需要更精細化的數據,可以選擇pfinder的秒級監控

                2.支持監控指標:

                監控指標 指標值
                線(xiàn)程池核心參數 core_size
                線(xiàn)程池最大線(xiàn)程參數 max_size
                當前活躍線(xiàn)程數 active_size
                阻塞隊列容量(設置的大小) queue_capacity
                阻塞隊列當前大小(是否有排隊) queue_size
                線(xiàn)程池完成任務(wù)數 completed_task_count

                此外,可以通過(guò)JSF接口查看當前時(shí)刻應用中的線(xiàn)程信息快照,當前時(shí)刻被監控的線(xiàn)程池有哪些:

                感知異常告警

                達成目的:及時(shí)感知線(xiàn)程池異常情況,避免問(wèn)題放大

                以隊列積壓和線(xiàn)程池拒絕告警為例

                隊列積壓告警(郵件):

                wKgZomZqovOAKiccAAMy15Y4ca4686.png

                線(xiàn)程池拒絕告警(咚咚):

                wKgZomZqovWAf6jCAADUUPwpGwo953.png

                自動(dòng)觸發(fā)線(xiàn)程堆棧打印

                達成目的:自動(dòng)記錄問(wèn)題發(fā)生時(shí)的線(xiàn)程堆棧,為線(xiàn)程池拒絕異常排查提供思路,并加快問(wèn)題定位

                實(shí)踐案例1:

                大促期間對核心接口的壓測途中,突然收到偶發(fā)機器的JSF線(xiàn)程池拒絕告警

                wKgaomZqoveAFR3nAAEGQ4KgnGc923.png

                實(shí)際分析發(fā)現拒絕時(shí)間較短,整體持續時(shí)間不到1分鐘

                下圖因為線(xiàn)程不夠瞬間打上去的線(xiàn)程數持續時(shí)間

                wKgZomZqoviAc74sAAGUCrgaoS0376.png

                由此來(lái)看我們難以通過(guò)人工觸發(fā)jstack的方式在短時(shí)間內獲取到問(wèn)題發(fā)生時(shí)的線(xiàn)程堆棧,也因此無(wú)法定位到具體拒絕原因;因此我們嘗試借助線(xiàn)程池拒絕時(shí)自動(dòng)打印的線(xiàn)程堆棧分析,自動(dòng)打印機制會(huì )在線(xiàn)程池發(fā)生拒絕策略的同時(shí)將全局線(xiàn)程堆棧打印到機器對應的日志目錄

                wKgaomZqovmAFxKiAAKBmiUKKd0144.png

                下圖為問(wèn)題發(fā)生時(shí)自動(dòng)打印的堆棧日志

                wKgZomZqovuAWGWHAAlN9YYdUqc010.png

                wKgaomZqov2ATMMGAAJDuG9RHHA005.png

                wKgZomZqov6AEGzeAAJF192q8GU676.png

                wKgaomZqov-AGT11AAI7PviUKz4277.png

                wKgZomZqowCAUyYxAAI7qn8Og5w187.png

                借助分析工具發(fā)現512個(gè)線(xiàn)程,511個(gè)都卡在了同一位置

                wKgaomZqowGAILp4AADWUwmE3nI724.png

                從堆棧和分析結果可以比較容易的定位到時(shí)哪里出現了問(wèn)題,最終發(fā)現我們在每次記錄關(guān)鍵日志時(shí)通過(guò)InetAddress.getLocalHost()方法獲取了本機ip,由于獲取的方法在特定情況下可能出現加鎖的情況,所以可能會(huì )先間歇性的線(xiàn)程阻塞;(網(wǎng)上相似案例:https://qa.1r1g.com/sf/ask/4489560281/)

                實(shí)踐案例2:

                業(yè)務(wù)中某線(xiàn)程池偶爾會(huì )出現線(xiàn)程池拒絕異常,同樣時(shí)間僅持續秒級,報警信息如下

                wKgZomZqowKABtiOAADK7iqa0WE318.png

                通過(guò)自動(dòng)觸發(fā)的線(xiàn)程堆棧進(jìn)行分析,發(fā)現該線(xiàn)程池中大量線(xiàn)程在拒絕時(shí)積壓在某接口的jsf調用等待上

                wKgaomZqowSAOUs_AA7AMIq8cms868.png

                由此再結合方法監控及日志可以比較容易定位到該時(shí)刻接口性能波動(dòng)導致

                參數動(dòng)態(tài)刷新

                達成目的:迅速修改線(xiàn)程池參數,降低問(wèn)題風(fēng)險

                如果需要對某線(xiàn)程池的參數做變更,只需將修改后的參數設置到ducc并重新發(fā)布即可

                wKgZomZqowaAOeHJAAU6io0jfpg913.png

                實(shí)踐案例:

                業(yè)務(wù)中部分場(chǎng)景從同步JSF調用改為異步后,可用率出現下降,通過(guò)分析發(fā)現是JSF的JSF-CLI-CB線(xiàn)程池設置較小,出現等待超時(shí)導致;借助動(dòng)態(tài)線(xiàn)程池的動(dòng)態(tài)配置能力修改對應ducc發(fā)布后問(wèn)題得到改善

                調整前后對比

                wKgaomZqoweAU-6OAAO8MIi7JnE957.png

                審核編輯 黃宇

                聲明:本文內容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權轉載。文章觀(guān)點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習之用,如有內容侵權或者其他違規問(wèn)題,請聯(lián)系本站處理。 舉報投訴
                • 監控
                  +關(guān)注

                  關(guān)注

                  6

                  文章

                  2023

                  瀏覽量

                  54677
                • 線(xiàn)程池
                  +關(guān)注

                  關(guān)注

                  0

                  文章

                  54

                  瀏覽量

                  6778
                收藏 人收藏

                  評論

                  相關(guān)推薦

                  想學(xué)習prote

                  想學(xué)習prote
                  發(fā)表于 09-06 20:25

                  想學(xué)習 multisim 11

                  想學(xué)習 multisim 11
                  發(fā)表于 01-03 17:51

                  想學(xué)學(xué)習汽車(chē)電氣

                  各位大俠,想學(xué)習汽車(chē)電氣,應該從哪些方面開(kāi)始學(xué)習啊。
                  發(fā)表于 08-31 08:37

                  編程魔法師大思想視頻宣傳片

                  本帖最后由 yyy71cj 于 2017-4-25 12:30 編輯 編程魔法師大思想視頻,是對《單片機編程魔法師之高級裸編程思想》一書(shū)的實(shí)踐性提升。視頻通過(guò)分解項目、提取對象、到實(shí)現對象
                  發(fā)表于 04-23 21:46

                  想學(xué)labview異步多線(xiàn)程,大型項目規范,各類(lèi)通訊的找我

                  想學(xué)labview異步多線(xiàn)程,大型項目規范,各類(lèi)通訊的找我,qq***
                  發(fā)表于 02-27 21:19

                  【每日一練】第十六節:內存的使用

                  本視頻為【每日一練】的第16節學(xué)習視頻,注:剛開(kāi)始學(xué)習的童鞋請從第一節視頻開(kāi)始打卡哦(本節視頻在下面打卡即可)學(xué)習任務(wù):1、刪除內存時(shí),會(huì )首先喚醒等待在該內存
                  發(fā)表于 09-08 09:33

                  初學(xué)RT-thread線(xiàn)程動(dòng)態(tài)創(chuàng )建

                  RT-thread初學(xué)線(xiàn)程動(dòng)態(tài)創(chuàng )建線(xiàn)程靜態(tài)創(chuàng )建線(xiàn)程鉤子函數定時(shí)器獲取系統時(shí)間動(dòng)態(tài)創(chuàng )建定時(shí)器靜態(tài)創(chuàng )建定時(shí)器信號量靜態(tài)創(chuàng )建與
                  發(fā)表于 02-24 07:32

                  線(xiàn)程是如何實(shí)現的

                  線(xiàn)程的概念是什么?線(xiàn)程是如何實(shí)現的?
                  發(fā)表于 02-28 06:20

                  線(xiàn)程創(chuàng )建的兩種方法

                  1. 使用內置模塊在使用多線(xiàn)程處理任務(wù)時(shí)也不是線(xiàn)程越多越好,由于在切換線(xiàn)程的時(shí)候,需要切換上下文環(huán)境,依然會(huì )造成cpu的大量開(kāi)銷(xiāo)。為解決這個(gè)問(wèn)題,線(xiàn)程
                  發(fā)表于 03-16 16:15

                  RT-Thread線(xiàn)程管理快速入門(mén)資料合集

                  1、建立RT-Thread 多任務(wù)(線(xiàn)程)的編程思想對于裸機編程,整個(gè)軟件系統只有一個(gè)線(xiàn)程(任務(wù))在執行,實(shí)現方式是通過(guò)一個(gè)大循環(huán)完成的。應用程序是一個(gè)無(wú)限循環(huán),循環(huán)中調用各個(gè)功能模塊的函數,完成
                  發(fā)表于 03-30 17:40

                  關(guān)于RT-Thread內存管理的內存簡(jiǎn)析

                  鏈表上,并增加內存可用內存塊的數目。在釋放過(guò)程中,會(huì )判斷該內存對象上是否有掛起線(xiàn)程,若有,則喚醒掛起線(xiàn)程鏈表上第一個(gè)線(xiàn)程。內存
                  發(fā)表于 04-06 17:02

                  利用多線(xiàn)程思想實(shí)現單片機系統的偽并行處理

                  介紹和分析了一種原用于計算機高級語(yǔ)言的編程思想---多線(xiàn)程編程,向單片機控制系統的移植。利用多線(xiàn)程的編程思想進(jìn)行單片機的復雜控制,可以應用到一些對系統控制有苛刻
                  發(fā)表于 08-07 09:09 ?40次下載

                  基于Nacos的簡(jiǎn)單動(dòng)態(tài)線(xiàn)程池實(shí)現

                  本文以Nacos作為服務(wù)配置中心,以修改線(xiàn)程池核心線(xiàn)程數、最大線(xiàn)程數為例,實(shí)現一個(gè)簡(jiǎn)單的動(dòng)態(tài)線(xiàn)程池。
                  發(fā)表于 01-06 14:14 ?664次閱讀

                  深度學(xué)習框架pytorch入門(mén)與實(shí)踐

                  的。PyTorch是一個(gè)開(kāi)源的深度學(xué)習框架,在深度學(xué)習領(lǐng)域得到了廣泛應用。本文將介紹PyTorch框架的基本知識、核心概念以及如何在實(shí)踐中使用PyTorch框架。 一、PyTorch框架概述 PyTorch是一個(gè)Facebook
                  的頭像 發(fā)表于 08-17 16:03 ?1287次閱讀

                  什么是動(dòng)態(tài)線(xiàn)程池?動(dòng)態(tài)線(xiàn)程池的簡(jiǎn)單實(shí)現思路

                  因此,動(dòng)態(tài)可監控線(xiàn)程池一種針對以上痛點(diǎn)開(kāi)發(fā)的線(xiàn)程池管理工具。主要可實(shí)現功能有:提供對 Spring 應用內線(xiàn)程池實(shí)例的全局管控、應用運行時(shí)動(dòng)態(tài)
                  的頭像 發(fā)表于 02-28 10:42 ?273次閱讀
                  RM新时代网站-首页

                                RM新时代官方 RM新时代手机版 RM新时代官网 RM新时代官网 RM新时代手机版下载

                                              RM新时代反波胆平台有限公司 RM新时代反波胆33能稳多久 RM新时代平台靠谱平台入口-百度知道 新时代手机平台官网 RM新时代有限公司 RM新时代|官方理财平台 RM新时代反波 rm新时代反波胆平台 RM新时代成立多久了 RM新时代首页