xxl-job原始碼閱讀二(服務端)

yejg1212發表於2021-05-24

1、原始碼入口

xxl-job-admin是一個簡單的springboot工程,簡單翻看原始碼,可以很快發現XxlJobAdminConfig入口。

@Override
public void afterPropertiesSet() throws Exception {
    adminConfig = this;

    xxlJobScheduler = new XxlJobScheduler();
    xxlJobScheduler.init();
}

我們就可以順著這個XxlJobScheduler,分析下這個xxl-job-admin做了些什麼。

2、初始化七大步

在XxlJobScheduler.init() 方法中,主要做了如下七件事情:

public void init() throws Exception {
    // init i18n
    initI18n();

    // admin trigger pool start
    JobTriggerPoolHelper.toStart();

    // admin registry monitor run
    JobRegistryHelper.getInstance().start();

    // admin fail-monitor run
    JobFailMonitorHelper.getInstance().start();

    // admin lose-monitor run ( depend on JobTriggerPoolHelper )
    JobCompleteHelper.getInstance().start();

    // admin log report start
    JobLogReportHelper.getInstance().start();

    // start-schedule  ( depend on JobTriggerPoolHelper )
    JobScheduleHelper.getInstance().start();

    logger.info(">>>>>>>>> init xxl-job admin success.");
}

按照程式碼的順序,逐一看看這七步到底做了些什麼。這裡的7個方法分別對應下面的2.1 ~ 2.7小節

2.1、國際化

真正實現國際化的,可不是這個initI18n(),這個方法只是設定了幾個title。真正實現國際化的實現是:

  1. com.xxl.job.admin.core.util.I18nUtil 載入 國際化資原始檔

    這塊實現充分利用spring的 EncodedResource和PropertiesLoaderUtils.loadProperties,嗯,利用好現有的輪子!

  2. CookieInterceptor 把I18nUtil物件返回到ftl頁面上去

  3. 在templates/common/common.macro.ftl中定義巨集

    <#global I18n = I18nUtil.getMultString()?eval />
    var I18n = ${I18nUtil.getMultString()};
    
  4. 在ftl頁面中使用,比如

<h1>${I18n.job_dashboard_name}</h1>

2.2、觸發器

JobTriggerPoolHelper.toStart();裡面啟動了2個執行緒池,一快一慢。

預設情況下,會使用fastTriggerPool。如果1分鐘視窗期內任務耗時達500ms超過10次,則該視窗期內判定為慢任務,慢任務自動降級進入”Slow”執行緒池,避免耗盡排程執行緒,提高系統穩定性;

這個JobTriggerPoolHelper裡面有個很重要的方法,就是addTrigger

public void addTrigger(final int jobId,
                           final TriggerTypeEnum triggerType,
                           final int failRetryCount,
                           final String executorShardingParam,
                           final String executorParam,
                           final String addressList) {

        // choose thread pool
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {      // job-timeout 10 times in 1 min
            triggerPool_ = slowTriggerPool;
        }

        // trigger
        triggerPool_.execute(new Runnable() {
            @Override
            public void run() {

                long start = System.currentTimeMillis();

                try {
                    // do trigger
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                } finally {

                    // check timeout-count-map
                    long minTim_now = System.currentTimeMillis()/60000;
                    if (minTim != minTim_now) {
                        minTim = minTim_now;
                        jobTimeoutCountMap.clear();
                    }

                    // incr timeout-count-map
                    long cost = System.currentTimeMillis()-start;
                    if (cost > 500) {       // ob-timeout threshold 500ms
                        AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                        if (timeoutCount != null) {
                            timeoutCount.incrementAndGet();
                        }
                    }

                }

            }
        });
    }

具體到真正觸發執行的地方,就得看看XxlJobTrigger.trigger了。

XxlJobTrigger.trigger中,先查表看有哪些執行器(其實就是業務伺服器,可以用來跑job的),如果配置的路由策略是分片,則組裝好分片引數。

引數組裝好之後,呼叫processTrigger方法,然後調到runExecutor去執行。

public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
        ReturnT<String> runResult = null;
        try {
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
            runResult = executorBiz.run(triggerParam);
        } catch (Exception e) {
            logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
            runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
        }

        StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
        runResultSB.append("<br>address:").append(address);
        runResultSB.append("<br>code:").append(runResult.getCode());
        runResultSB.append("<br>msg:").append(runResult.getMsg());

        runResult.setMsg(runResultSB.toString());
        return runResult;
}

這裡首先利用執行器的地址,構造一個ExecutorBizClient物件,然後呼叫run方法。

public class ExecutorBizClient implements ExecutorBiz {
    
    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }
}

前面在閱讀xxl-job-core的程式碼的時候,就看到過,客戶端基於netty建立了一個EmbedServer ,預設監聽9999 埠,接收job-admin端發過來的任務處理命令。

這裡的呼叫,就是調到客戶端的執行,即觸發客戶端執行任務!

觸發之後,客戶端會立即有返回觸發是否成功。真正任務執行是否成功,則是非同步返回給admin端的。(可以回頭看下 TriggerCallbackThread )

(排程觸發 和 job執行是兩碼事,所以xxl_job_log表裡面有trigger_code 和 handle_code,分別來存著兩個動作的結果)

2.3、執行器維護

這裡的執行器,其實就是業務伺服器。很好理解,誰執行job,誰就是執行器。

接下來看看JobRegistryHelper.getInstance().start()。

  1. 開啟一個執行緒池 registryOrRemoveThreadPool ,用來註冊或者刪除。物件對外提供registry和registryRemove方法
  2. 開啟一個執行緒registryMonitorThread,每sleep 30秒(心跳時間) 移除失活業務伺服器記錄, 讀取存活的xxl_job_registry資訊,更新到 xxl_job_group 裡面去 。 這個執行緒被設定為守護執行緒,通過改變變數標記toStop退出執行
  3. registry和registryRemove方法被進一步封裝到 AdminBizImpl 中,供外部使用

2.4、失敗處理器

任務排程執行的時候,會寫xxl_job_log 記錄。如果排程執行失敗,需要重試或者發郵件通知。程式碼執行邏輯則是:

  1. admin服務端啟動一個 monitorThread執行緒,每隔10秒,掃描失敗的記錄

  2. 通過sql的cas的形式,逐條鎖定失敗的日誌記錄來處理

    UPDATE xxl_job_log
    SET
       `alarm_status` = #{newAlarmStatus}
    WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus}
    
  3. 如果配置的重試次數大於0,則先重試

    JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, 
     (log.getExecutorFailRetryCount()-1), /*重試次數 -1  */
     log.getExecutorShardingParam(), 
    log.getExecutorParam(), null);
    
  4. 如果配置了email,則發郵件告警

  5. 處理完之後,再通過sql的case更新 alarm_status

2.5、 job完成後續處理

如果job正常執行,那admin端只需要正常等著,接收client端的執行結果報告就行了。但是如果執行了好久沒返回呢?所以在JobCompleteHelper裡面做了這麼幾件事:

  1. 構造一個 callbackThreadPool 執行緒池,主要用來更新 xxl_job_log 記錄 的執行結果

  2. 構造一個 monitorThread 執行緒,處理執行超時的。

    找到【排程記錄停留在 "執行中" 狀態超過10min,且對應執行器心跳註冊失敗不線上,則將本地排程主動標記失敗;】

  3. 更新job_log狀態結果

2.6、定期清理日誌

JobLogReportHelper裡面做的事情主要就3點:

  1. 啟動logrThread守護執行緒,定時掃描xxl_job_log 表
  2. 統計執行成功失敗的資料,這個其實也是admin裡面 執行報表資料的來源
  3. 根據配置,清理久遠的xxl_job_log歷史日誌

2.7、 排程

排程中心的排程邏輯,最源頭在這裡--- JobScheduleHelper。這個類裡主要做了如下幾件事情:

  1. 啟動scheduleThread

  2. 通過執行sql,獲取資料庫鎖 (分散式鎖),通過這種方式避免多個admin端重複排程

    select * from xxl_job_lock where lock_name = 'schedule_lock' for update
    
  3. 根據執行緒算力,計算出preReadCount,從xxl_job_info 表中找出 未來5秒內要執行的job,暫存 scheduleList

    1. 超時未排程(超過排程時間5秒)的任務,本次忽略,基於當前時間計算下次執行時間。
    2. 超過排程時間但未超時(超過5秒之內)的任務,立即放入執行執行緒池觸發一次,再修改執行時間,接著判斷下次執行時間若在5秒之內,加入timewheel的map後再次修改下次執行時間。
    3. 排程時間在未來5秒之內的(預讀5s),基於timewheel時間輪(map<秒數,list<任務實體>>),根據5秒內即將執行的任務的執行時間的秒數,將其放到timeheel對應秒數的list中,修改下次執行時間。
  4. 啟動ringThread ,處理timeRing裡面的job

至此,除開某些細節,程式碼層面上基本差不多了。

3、總結

看完客戶端+服務端的程式碼,現在來回顧小節一下。

3.1、 一次完整的任務排程通訊流程

  1. “排程中心”向“執行器”傳送http排程請求: “執行器”中接收請求的服務,實際上是一臺內嵌Server,預設埠9999;
  2. “執行器”執行任務邏輯;
  3. “執行器”http回撥“排程中心”排程結果: “排程中心”中接收回撥的服務,是針對執行器開放一套API服務;

3.2、 整體架構圖

3.3、 xxl-job的優點

優點真的很明顯:簡單、輕量級、易擴充套件。

框架確實非常清晰、簡單。程式碼寫的也很有啟發性。比如:

  1. 充分利用Spring現有的工具類,不重複造輪子
  2. 使用ThreadPoolExecutor的有參構造方法去建立執行緒池(阿里巴巴的開發手冊裡面貌似特意提到這一點)
  3. 儘量定義列舉型別,比如ExecutorRouteStrategyEnum(這裡又將列舉和路由策略結合在一起,算是策略模式的一個變種吧)
  4. 遵循迪米特法則。比如說:XxlJobAdminConfig中,所有dao都通過這裡注入、對外提供。不零散分佈在各個類中
  5. .....

3.3、 xxl-job的建議

目前xxl-job只分了2個子模組:admin和core,admin依賴core

個人覺得再多分出一個模組,結構會更好一些【admin,core,common】,依賴關係改成這樣子:

相關文章