1. 程式人生 > >一個線上小問題解決過程 —— java執行緒池使用注意點

一個線上小問題解決過程 —— java執行緒池使用注意點

表象和場景

最近小夥伴在凌晨0點40分左右會收到某臺伺服器報警,cpu使用率過高(達到95%),但是不到10分鐘使用率降到45%,在之後5分鐘內降到10%以內,服務恢復。

背景:公司是用微服務架構,某個產品大約有30多個微服務,前端用阿里雲的SLB,架了nginx叢集;

為了節省機器成本,某些機器上部署了多個服務,單個服務也部署在多個機器上,互為備份

通過consul來做服務註冊與發現

分析

某個機器出現報警cpu過高,過幾分鐘就不會出現了,有時效性

排查思路:

思路一:根據監控內容檢視;(包括這個機器的效能資料,應用服務日誌等)

思路二:蹲點出現立馬去伺服器看現場資訊;

思路三:時間節點比較特殊,按照經驗,可能會出現的地方思考:半夜出問題週期這麼奇怪,很有可能定時任務類沒有處理好導致;

根據分析思路找問題

思路一:

根據監控檢視機器效能資料,發現這個機器的B服務cpu使用率達到了80%,基本確認是該服務問題;

檢視該服務日誌,沒有任何錯誤資訊,看到介面呼叫情況中有異樣,在介面監控中看到某個對外的介面被呼叫次數暴增,是平時的100倍+,而且都是A服務呼叫的,都集中在報警的這段時間內。確認是A服務發起的呼叫引起的問題;

檢視A服務呼叫的地方,去排查業務,確認最終問題。

思路二:

蹲點出現立馬去看現場資訊:檢視cpu使用情況,檢視哪個程序使用率高,檢視執行緒,數量和dump執行緒內容,具體乾的事情,找到結果是B服務的某個對外介面執行執行緒多,檢視日誌或根據業務排查場景,得到是A服務呼叫引起的問題。

思路三:

通過定時任務出發,根據任務排程中心檢視配置的定時任務時間,在這個週期之內的任務有哪些,針對具體的任務業務場景去分析,結合日誌 和 程式碼 很快也可以找到問題了。

具體問題說明

   


A服務的部分程式碼說明:
分頁從資料來源中獲取List<BillPlanPO>,呼叫如下process方法


    @Async
    public void process(List<BillPlanPO> billPlanPOs) {
        if (CollectionUtils.isEmpty(billPlanPOs)) {
            return;
        }
        billPlanPOs.forEach(item -> {
            calLateFee(item);
        });
    }





    calLateFee 方法內部呼叫B服務的介面。

1.分頁從資料來源獲取資料速度很快

2.通過非同步呼叫process方法(非同步呼叫執行緒池沒有自己配置,使用springboot框架自帶的)

3.每一個billPlanPO去呼叫B服務的介面;而B服務處理一個的速度較慢。

結論:導致B服務瞬間壓力很大,因為A服務獲取List<BillPlanPO>速度很快,又是非同步執行緒呼叫。

找到問題,解決過程就簡單多了。

簡單介紹下springboot中非同步執行緒池

預設配置:

corePoolSize:表示執行緒池核心執行緒,正常情況下開啟的執行緒數量。

queueCapacity:當核心執行緒都在跑任務,還有多餘的任務會存到此處。

maxPoolSize:如果queueCapacity存滿了,還有任務就會啟動更多的執行緒,直到執行緒數達到maxPoolSize。如果還有任務,則根據拒絕策略進行處理。

針對如上問題,添加了下面程式碼:


import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 *  配置執行緒池
 */
@EnableAsync
@Configuration
public class ThreadPoolsConfig {

    /**
     * 執行緒相關引數
     */
    @Value("${pay.threadNamePrefix}")
    private String threadNamePrefix;    // 配置執行緒池中的執行緒名稱字首

    @Value("${pay.corePoolSize}")
    private Integer corePoolSize;       // 配置執行緒池中的核心執行緒數

    @Value("${pay.maxPoolSize}")
    private Integer maxPoolSize;        // 配置最大執行緒數

    @Value("${pay.queueCapacity}")
    private Integer queueCapacity;      // 配置佇列大小

    /**
     * 執行緒池配置
     */
    @Bean
    public AsyncTaskExecutor paymentTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
      /**
       * 當任務數量超過MaxPoolSize和QueueCapacity時使用的策略,該策略是呼叫任務的執行緒執行
       */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

}

另外幾個細節注意點:

a.corePoolSize數量配置

 /**
     * IO密集型任務  = 一般為2*CPU核心數(常出現於執行緒中:資料庫資料互動、檔案上傳下載、網路資料傳輸等等)
     * CPU密集型任務 = 一般為CPU核心數+1(常出現於執行緒中:複雜演算法)
     * 混合型任務  = 視機器配置和複雜度自測而定
     */

private static int corePoolSize = Runtime.getRuntime().availableProcessors();

b.當佇列滿了之後的處理,setRejectedExecutionHandler,根據場景看需要怎麼樣的拒絕策略

ThreadPoolExecutor自身提供了4種策略
1.CallerRunsPolicy :這個策略重試添加當前的任務,他會自動重複呼叫 execute() 方法,直到成功。
2.AbortPolicy :對拒絕任務拋棄處理,並且丟擲異常。
3.DiscardPolicy :對拒絕任務直接無聲拋棄,沒有異常資訊。
4.DiscardOldestPolicy :對拒絕任務不拋棄,而是拋棄佇列裡面等待最久的一個執行緒,然後把拒絕任務加到佇列。

扯遠了,打住,根據調整,測試環境走一波。

驗證與觀察

通過各種工具壓一波,問題已解決,根據資源情況配置合適的執行緒池引數。

釋出後,線上驗證通過,晚上可以安靜的睡覺了~

其他建議

大部分線上問題,都是一個很小的點引起的,排查是一個反向推導的過程,這個過程往往比理解原因或者解決問題複雜。

問題排查是複雜的,不可控的,所以不要把排查和解決混在一起,儘量先解決、再排查。解決的方式基本上都是那麼幾板斧:重啟、回滾、擴容、降級、遷移等。

系統要儘可能的對外暴露內部狀態和干預手段,比如說少打了一句日誌,沒把變數輸出出來,那麼出現問題的時候很不但要用某些複雜的工具去查詢變數,而且很有可能要多繞一個大圈。

系統是不穩定的,所以對於高可用架構設計來說,隔離是必須的,不管是何種依賴方式,都需要考慮“實在不行了”的情況。

問題的原因、傳播路徑和現象不是一一對應的。同一個問題,這次的表現是多打了一行WARN日誌,下次可能就是一次系統雪崩。墨菲定律,如果有可能出問題,那一定會出問題。

提前準備

a.監控

b.日誌(分維度過濾和查詢),集中檢索

c.儲存現場

d.報警機制,(機器與服務,業務層資料)

這個4點可以單獨寫很多內容,這裡只是備註下,出了問題才會想到這些內容何其重要,所以平時要更加積累去處理。

排查問題不是一個人的事情,最好拉個消防群,把各個崗位的人員一起處理(開發,運維,DBA,系統,硬體等),大家工作的細分也是有道理的,專業的人幹專業的事情。問題一定要先解決再排查,問題一定要先解決再排查,問題一定要先解決再排查~