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。真正實現國際化的實現是:
-
com.xxl.job.admin.core.util.I18nUtil 載入 國際化資原始檔
這塊實現充分利用spring的 EncodedResource和PropertiesLoaderUtils.loadProperties,嗯,利用好現有的輪子!
-
CookieInterceptor 把I18nUtil物件返回到ftl頁面上去
-
在templates/common/common.macro.ftl中定義巨集
<#global I18n = I18nUtil.getMultString()?eval /> var I18n = ${I18nUtil.getMultString()};
-
在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()。
- 開啟一個執行緒池 registryOrRemoveThreadPool ,用來註冊或者刪除。物件對外提供registry和registryRemove方法
- 開啟一個執行緒registryMonitorThread,每sleep 30秒(心跳時間) 移除失活業務伺服器記錄, 讀取存活的xxl_job_registry資訊,更新到 xxl_job_group 裡面去 。 這個執行緒被設定為守護執行緒,通過改變變數標記toStop退出執行
- registry和registryRemove方法被進一步封裝到 AdminBizImpl 中,供外部使用
2.4、失敗處理器
任務排程執行的時候,會寫xxl_job_log 記錄。如果排程執行失敗,需要重試或者發郵件通知。程式碼執行邏輯則是:
-
admin服務端啟動一個 monitorThread執行緒,每隔10秒,掃描失敗的記錄
-
通過sql的cas的形式,逐條鎖定失敗的日誌記錄來處理
UPDATE xxl_job_log SET `alarm_status` = #{newAlarmStatus} WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus}
-
如果配置的重試次數大於0,則先重試
JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), /*重試次數 -1 */ log.getExecutorShardingParam(), log.getExecutorParam(), null);
-
如果配置了email,則發郵件告警
-
處理完之後,再通過sql的case更新 alarm_status
2.5、 job完成後續處理
如果job正常執行,那admin端只需要正常等著,接收client端的執行結果報告就行了。但是如果執行了好久沒返回呢?所以在JobCompleteHelper裡面做了這麼幾件事:
-
構造一個 callbackThreadPool 執行緒池,主要用來更新 xxl_job_log 記錄 的執行結果
-
構造一個 monitorThread 執行緒,處理執行超時的。
找到【排程記錄停留在 "執行中" 狀態超過10min,且對應執行器心跳註冊失敗不線上,則將本地排程主動標記失敗;】
-
更新job_log狀態結果
2.6、定期清理日誌
JobLogReportHelper裡面做的事情主要就3點:
- 啟動logrThread守護執行緒,定時掃描xxl_job_log 表
- 統計執行成功失敗的資料,這個其實也是admin裡面 執行報表資料的來源
- 根據配置,清理久遠的xxl_job_log歷史日誌
2.7、 排程
排程中心的排程邏輯,最源頭在這裡--- JobScheduleHelper。這個類裡主要做了如下幾件事情:
-
啟動scheduleThread
-
通過執行sql,獲取資料庫鎖 (分散式鎖),通過這種方式避免多個admin端重複排程
select * from xxl_job_lock where lock_name = 'schedule_lock' for update
-
根據執行緒算力,計算出preReadCount,從xxl_job_info 表中找出 未來5秒內要執行的job,暫存 scheduleList
- 超時未排程(超過排程時間5秒)的任務,本次忽略,基於當前時間計算下次執行時間。
- 超過排程時間但未超時(超過5秒之內)的任務,立即放入執行執行緒池觸發一次,再修改執行時間,接著判斷下次執行時間若在5秒之內,加入timewheel的map後再次修改下次執行時間。
- 排程時間在未來5秒之內的(預讀5s),基於timewheel時間輪(map<秒數,list<任務實體>>),根據5秒內即將執行的任務的執行時間的秒數,將其放到timeheel對應秒數的list中,修改下次執行時間。
-
啟動ringThread ,處理timeRing裡面的job
至此,除開某些細節,程式碼層面上基本差不多了。
3、總結
看完客戶端+服務端的程式碼,現在來回顧小節一下。
3.1、 一次完整的任務排程通訊流程
- “排程中心”向“執行器”傳送http排程請求: “執行器”中接收請求的服務,實際上是一臺內嵌Server,預設埠9999;
- “執行器”執行任務邏輯;
- “執行器”http回撥“排程中心”排程結果: “排程中心”中接收回撥的服務,是針對執行器開放一套API服務;
3.2、 整體架構圖
3.3、 xxl-job的優點
優點真的很明顯:簡單、輕量級、易擴充套件。
框架確實非常清晰、簡單。程式碼寫的也很有啟發性。比如:
- 充分利用Spring現有的工具類,不重複造輪子
- 使用ThreadPoolExecutor的有參構造方法去建立執行緒池(阿里巴巴的開發手冊裡面貌似特意提到這一點)
- 儘量定義列舉型別,比如ExecutorRouteStrategyEnum(這裡又將列舉和路由策略結合在一起,算是策略模式的一個變種吧)
- 遵循迪米特法則。比如說:XxlJobAdminConfig中,所有dao都通過這裡注入、對外提供。不零散分佈在各個類中
- .....
3.3、 xxl-job的建議
目前xxl-job只分了2個子模組:admin和core,admin依賴core
個人覺得再多分出一個模組,結構會更好一些【admin,core,common】,依賴關係改成這樣子: