xxl-job 基於Quartz 的分散式任務排程平臺
介紹
XXL-JOB是一個輕量級分散式任務排程框架,其核心設計目標是開發迅速、學習簡單、輕量級、易擴充套件。現已開放原始碼並接入多家公司線上產品線,開箱即用。
專案git地址 | 專案首頁
為了方便使用改造,自己在github上fork了一個專案進行完善(地址),新增功能(發簡訊,展示更多欄位、解決部分BUG,新增cron表示式直譯器)
搭建專案、瞭解架構可以在專案官網學習。本文只做加強補充,個人內容通過標註的形式展現。
總體設計
1 原始碼目錄介紹
- /doc :文件資料 - /db :“排程資料庫”建表指令碼 - /xxl-job-admin :排程中心,專案原始碼 - /xxl-job-core :公共Jar依賴 - /xxl-job-executor-samples :執行器,Sample示例專案(大家可以在該專案上進行開發,也可以將現有專案改造生成執行器專案)
2 “排程資料庫”配置
XXL-JOB排程模組基於Quartz叢集實現,其“排程資料庫”是在Quartz的11張叢集mysql表基礎上擴充套件而成。
XXL-JOB首先定製了Quartz原生表結構字首(XXL_JOB_QRTZ_)。
然後,在此基礎上新增了幾張張擴充套件表,如下:
- XXL_JOB_QRTZ_TRIGGER_GROUP:執行器資訊表,維護任務執行器資訊;
- XXL_JOB_QRTZ_TRIGGER_REGISTRY:執行器登錄檔,維護線上的執行器和排程中心機器地址資訊;
- XXL_JOB_QRTZ_TRIGGER_INFO:排程擴充套件資訊表: 用於儲存XXL-JOB排程任務的擴充套件資訊,如任務分組、任務名、機器地址、執行器、執行入參和報警郵件等等;
- XXL_JOB_QRTZ_TRIGGER_LOG:排程日誌表: 用於儲存XXL-JOB任務排程的歷史資訊,如排程結果、執行結果、排程入參、排程機器和執行器等等;
- XXL_JOB_QRTZ_TRIGGER_LOGGLUE:任務GLUE日誌:用於儲存GLUE更新歷史,用於支援GLUE的版本回溯功能;
因此,XXL-JOB排程資料庫共計用於16張資料庫表。
主要基於
Quartz
的資料庫表,如果需要支援其他資料庫,可以到Quartz官網 下載,裡面的文件部分有支援多種型別資料庫(mysql、sqlserver、H2、sybase…)的初始化sql。
3 架構設計
3.1 設計思想
將排程行為抽象形成“排程中心”公共平臺,而平臺自身並不承擔業務邏輯,“排程中心”負責發起排程請求。
將任務抽象成分散的JobHandler,交由“執行器”統一管理,“執行器”負責接收排程請求並執行對應的JobHandler中業務邏輯。
因此,“排程”和“任務”兩部分可以相互解耦,提高系統整體穩定性和擴充套件性;
3.2 系統組成
- 排程模組(排程中心):
負責管理排程資訊,按照排程配置發出排程請求,自身不承擔業務程式碼。排程系統與任務解耦,提高了系統可用性和穩定性,同時排程系統性能不再受限於任務模組;
支援視覺化、簡單且動態的管理排程資訊,包括任務新建,更新,刪除,GLUE開發和任務報警等,所有上述操作都會實時生效,同時支援監控排程結果以及執行日誌,支援執行器Failover。 - 執行模組(執行器):
負責接收排程請求並執行任務邏輯。任務模組專注於任務的執行等操作,開發和維護更加簡單和高效;
接收“排程中心”的執行請求、終止請求和日誌請求等。
3.3 架構圖
4 排程模組剖析
4.1 quartz的不足
Quartz作為開源作業排程中的佼佼者,是作業排程的首選。但是叢集環境中Quartz採用API的方式對任務進行管理,從而可以避免上述問題,但是同樣存在以下問題:
- 問題一:呼叫API的的方式操作任務,不人性化;
- 問題二:需要持久化業務QuartzJobBean到底層資料表中,系統侵入性相當嚴重。
- 問題三:排程邏輯和QuartzJobBean耦合在同一個專案中,這將導致一個問題,在排程任務數量逐漸增多,同時排程任務邏輯逐漸加重的情況加,此時排程系統的效能將大大受限於業務;
- 問題四:quartz底層以“搶佔式”獲取DB鎖並由搶佔成功節點負責執行任務,會導致節點負載懸殊非常大;而XXL-JOB通過執行器實現“協同分配式”執行任務,充分發揮叢集優勢,負載各節點均衡。
XXL-JOB彌補了quartz的上述不足之處。
4.2 RemoteHttpJobBean
常規Quartz的開發,任務邏輯一般維護在QuartzJobBean中,耦合很嚴重。XXL-JOB中“排程模組”和“任務模組”完全解耦,排程模組中的所有排程任務使用同一個QuartzJobBean,即RemoteHttpJobBean。不同的排程任務將各自引數維護在各自擴充套件表資料中,當觸發RemoteHttpJobBean執行時,將會解析不同的任務引數發起遠端呼叫,呼叫各自的遠端執行器服務。
這種呼叫模型類似RPC呼叫,RemoteHttpJobBean提供呼叫代理的功能,而執行器提供遠端服務的功能。
排程器和執行器基於http通訊,通過動態代理的方式實現遠端呼叫。
具體程式碼可見NetComClientProxy
,通過重寫FactoryBean<Object>
的getObject()
發起遠端呼叫,使用時類似如下程式碼:
ExecutorBiz executorBiz = (ExecutorBiz) new NetComClientProxy(ExecutorBiz.class, "127.0.0.1:9999", null).getObject();
executorBiz.run(triggerParam);
4.3 排程中心HA(叢集)
基於Quartz的叢集方案,資料庫選用Mysql;叢集分散式併發環境中使用QUARTZ定時任務排程,會在各個節點會上報任務,存到資料庫中,執行時會從資料庫中取出觸發器來執行,如果觸發器的名稱和執行時間相同,則只有一個節點去執行此任務。
# for cluster
org.quartz.jobStore.tablePrefix = XXL_JOB_QRTZ_
org.quartz.scheduler.instanceId: AUTO
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 1000
4.4 排程執行緒池
排程採用執行緒池方式實現,避免單執行緒因阻塞而引起任務排程延遲。
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 15
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
XXL-JOB系統中業務邏輯在遠端執行器執行,全非同步化設計,排程中心每次觸發排程時僅傳送一次排程請求,執行器會將請求存入執行佇列並且立即響應排程中心,非同步執行;相比直接在quartz的QuartzJobBean中執行業務邏輯,極大的降低了排程執行緒佔用時間;
XXL-JOB排程中心中每個JOB邏輯非常 “輕”,單個JOB一次執行平均耗時基本在 “10ms” 之內(基本為一次請求的網路開銷);因此,可以保證使用有限的執行緒支撐大量的JOB併發執行;
理論支撐任務量公式如下:
理論支撐任務量 = 執行緒數配置 / 平均排程頻率(每秒) * 平均觸發耗時(單位s)
理論上採用推薦機器配置 “4核4G記憶體” + “配置1s執行1次密集任務” + “排程中心與執行器ping延遲10ms(0.01s)” 的情況下,
- 單執行緒支撐任務量 :1 / 1 * 0.01 = 100個任務
- 15個執行緒支撐任務量:15 / 1 * 0.01 = 1500個任務
實際場景中,由於排程中心與執行器ping延遲不同、DB讀寫耗時不同、任務排程密集程度不同,會導致任務量上限會上下波動。
如若需要支撐更多的任務量,可以通過 “調大排程執行緒數” 、”降低排程中心與執行器ping延遲” 和 “提升機器配置” 幾種方式實現。
4.5 @DisallowConcurrentExecution
XXL-JOB排程模組的“排程中心”預設不使用該註解,即預設開啟並行機制,因為RemoteHttpJobBean為公共QuartzJobBean,這樣在多執行緒排程的情況下,排程模組被阻塞的機率很低,大大提高了排程系統的承載量。
XXL-JOB的每個排程任務雖然在排程模組是並行排程執行的,但是任務排程傳遞到任務模組的“執行器”確實序列執行的,同時支援任務終止。
4.6 misfire
錯過了觸發時間,處理規則。
可能原因:服務重啟;排程執行緒被QuartzJobBean阻塞,執行緒被耗盡;某個任務啟用了@DisallowConcurrentExecution,上次排程持續阻塞,下次排程被錯過;
quartz.properties中關於misfire的閥值配置如下,單位毫秒:
org.quartz.jobStore.misfireThreshold: 60000
Misfire規則:
withMisfireHandlingInstructionDoNothing:不觸發立即執行,等待下次排程;
withMisfireHandlingInstructionIgnoreMisfires:以錯過的第一個頻率時間立刻開始執行;
withMisfireHandlingInstructionFireAndProceed:以當前時間為觸發頻率立刻觸發一次執行;
XXL-JOB預設misfire規則為:withMisfireHandlingInstructionDoNothing
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getJobCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
4.7 日誌回撥服務
排程模組的“排程中心”作為Web服務部署時,一方面承擔排程中心功能,另一方面也為執行器提供API服務。
排程中心提供的”日誌回撥服務API服務”程式碼位置如下:
xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback
“執行器”在接收到任務執行請求後,執行任務,在執行結束之後會將執行結果回撥通知“排程中心”:
亦是通過
4.2
所述的方式進行通訊
4.8 任務HA(Failover)
執行器如若叢集部署,排程中心將會感知到線上的所有執行器,如“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”。
當任務”路由策略”選擇”故障轉移(FAILOVER)”時,當排程中心每次發起排程請求時,會按照順序對執行器發出心跳檢測請求,第一個檢測為存活狀態的執行器將會被選定併發送排程請求。
排程成功後,可在日誌監控介面檢視“排程備註”,如下;
“排程備註”可以看出本地排程執行軌跡,執行器的”註冊方式”、”地址列表”和任務的”路由策略”。”故障轉移(FAILOVER)”路由策略下,排程中心首先對第一個地址進行心跳檢測,心跳失敗因此自動跳過,第二個依然心跳檢測失敗……
直至心跳檢測第三個地址“127.0.0.1:9999”成功,選定為“目標執行器”;然後對“目標執行器”傳送排程請求,排程流程結束,等待執行器回撥執行結果。
4.9 排程日誌
排程中心每次進行任務排程,都會記錄一條任務日誌,任務日誌主要包括以下三部分內容:
- 任務資訊:包括“執行器地址”、“JobHandler”和“執行引數”等屬性,點選任務ID按鈕可檢視,根據這些引數,可以精確的定位任務執行的具體機器和任務程式碼;
- 排程資訊:包括“排程時間”、“排程結果”和“排程日誌”等,根據這些引數,可以瞭解“排程中心”發起排程請求時具體情況。
- 執行資訊:包括“執行時間”、“執行結果”和“執行日誌”等,根據這些引數,可以瞭解在“執行器”端任務執行的具體情況;
排程日誌,針對單次排程,屬性說明如下:
- 執行器地址:任務執行的機器地址;
- JobHandler:Bean模式表示任務執行的JobHandler名稱;
- 任務引數:任務執行的入參;
- 排程時間:排程中心,發起排程的時間;
- 排程結果:排程中心,發起排程的結果,SUCCESS或FAIL;
- 排程備註:排程中心,發起排程的備註資訊,如地址心跳檢測日誌等;
- 執行時間:執行器,任務執行結束後回撥的時間;
- 執行結果:執行器,任務執行的結果,SUCCESS或FAIL;
- 執行備註:執行器,任務執行的備註資訊,如異常日誌等;
- 執行日誌:任務執行過程中,業務程式碼中列印的完整執行日誌,見“4.7 檢視執行日誌”;
4.10 任務依賴
原理:XXL-JOB中每個任務都對應有一個任務ID,同時,每個任務支援設定屬性“子任務ID”,因此,通過“任務ID”可以匹配任務依賴關係。
當父任務執行結束並且執行成功時,將會根據“子任務ID”匹配子任務依賴,如果匹配到子任務,將會主動觸發一次子任務的執行。
在任務日誌介面,點選任務的“執行備註”的“檢視”按鈕,可以看到匹配子任務以及觸發子任務執行的日誌資訊,如無資訊則表示未觸發子任務執行,可參考下圖。
5 任務 “執行模式” 剖析
5.1 “Bean模式” 任務
開發步驟:可參考 “章節三” ;
原理:每個Bean模式任務都是一個Spring的Bean類例項,它被維護在“執行器”專案的Spring容器中。任務類需要加“@JobHandler(value=”名稱”)”註解,因為“執行器”會根據該註解識別Spring容器中的任務。任務類需要繼承統一介面“IJobHandler”,任務邏輯在execute方法中開發,因為“執行器”在接收到排程中心的排程請求時,將會呼叫“IJobHandler”的execute方法,執行任務邏輯。
5.2 “GLUE模式(Java)” 任務
開發步驟:可參考 “章節三” ;
原理:每個 “GLUE模式(Java)” 任務的程式碼,實際上是“一個繼承自“IJobHandler”的實現類的類程式碼”,“執行器”接收到“排程中心”的排程請求時,會通過Groovy類載入器載入此程式碼,例項化成Java物件,同時注入此程式碼中宣告的Spring服務(請確保Glue程式碼中的服務和類引用在“執行器”專案中存在),然後呼叫該物件的execute方法,執行任務邏輯。
5.3 GLUE模式(Shell) + GLUE模式(Python) + GLUE模式(NodeJS)
開發步驟:可參考 “章節三” ;
原理:指令碼任務的原始碼託管在排程中心,指令碼邏輯在執行器執行。當觸發指令碼任務時,執行器會載入指令碼原始碼在執行器機器上生成一份指令碼檔案,然後通過Java程式碼呼叫該指令碼;並且實時將指令碼輸出日誌寫到任務日誌檔案中,從而在排程中心可以實時監控指令碼執行情況;
目前支援的指令碼型別如下:
- shell指令碼:任務執行模式選擇為 "GLUE模式(Shell)"時支援 "shell" 指令碼任務;
- python指令碼:任務執行模式選擇為 "GLUE模式(Python)"時支援 "python" 指令碼任務;
- nodejs指令碼:務執行模式選擇為 "GLUE模式(NodeJS)"時支援 "nodejs" 指令碼任務;
5.4 執行器
執行器實際上是一個內嵌的Jetty伺服器,預設埠9999(配置項:xxl.job.executor.port)。
在專案啟動時,執行器會通過“@JobHandler”識別Spring容器中“Bean模式任務”,以註解的value屬性為key管理起來。
“執行器”接收到“排程中心”的排程請求時,如果任務型別為“Bean模式”,將會匹配Spring容器中的“Bean模式任務”,然後呼叫其execute方法,執行任務邏輯。如果任務型別為“GLUE模式”,將會載入GLue程式碼,例項化Java物件,注入依賴的Spring服務(注意:Glue程式碼中注入的Spring服務,必須存在與該“執行器”專案的Spring容器中),然後呼叫execute方法,執行任務邏輯。
5.5 任務日誌
XXL-JOB會為每次排程請求生成一個單獨的日誌檔案,需要通過 “XxlJobLogger.log” 列印執行日誌,“排程中心”檢視執行日誌時將會載入對應的日誌檔案。
(歷史版本通過重寫LOG4J的Appender實現,存在依賴限制,該方式在新版本已經被拋棄)
日誌檔案存放的位置可在“執行器”配置檔案進行自定義,預設目錄格式為:/data/applogs/xxl-job/jobhandler/“格式化日期”/“資料庫排程日誌記錄的主鍵ID.log”。
在JobHandler中開啟子執行緒時,子執行緒將會將會把日誌列印在父執行緒即JobHandler的執行日誌中,方便日誌追蹤。
6 通訊模組剖析
6.1 一次完整的任務排程通訊流程
- 1、“排程中心”向“執行器”傳送http排程請求: “執行器”中接收請求的服務,實際上是一臺內嵌jetty伺服器,預設埠9999;
- 2、“執行器”執行任務邏輯;
- 3、“執行器”http回撥“排程中心”排程結果: “排程中心”中接收回調的服務,是針對執行器開放一套API服務;
6.2 通訊資料加密
排程中心向執行器傳送的排程請求時使用RequestModel和ResponseModel兩個物件封裝排程請求引數和響應資料, 在進行通訊之前底層會將上述兩個物件物件序列化,並進行資料協議以及時間戳檢驗,從而達到資料加密的功能;
7 任務註冊, 任務自動發現
自v1.5版本之後, 任務取消了”任務執行機器”屬性, 改為通過任務註冊和自動發現的方式, 動態獲取遠端執行器地址並執行。
AppName: 每個執行器機器叢集的唯一標示, 任務註冊以 "執行器" 為最小粒度進行註冊; 每個任務通過其繫結的執行器可感知對應的執行器機器列表;
登錄檔: 見"XXL_JOB_QRTZ_TRIGGER_REGISTRY"表, "執行器" 在進行任務註冊時將會週期性維護一條註冊記錄,即機器地址和AppName的繫結關係; "排程中心" 從而可以動態感知每個AppName線上的機器列表;
執行器註冊: 任務註冊Beat週期預設30s; 執行器以一倍Beat進行執行器註冊, 排程中心以一倍Beat進行動態任務發現; 註冊資訊的失效時間被三倍Beat;
執行器註冊摘除:執行器銷燬時,將會主動上報排程中心並摘除對應的執行器機器資訊,提高心跳註冊的實時性;
為保證系統”輕量級”並且降低學習部署成本,沒有采用Zookeeper作為註冊中心,採用DB方式進行任務註冊發現;
8 任務執行結果
自v1.6.2之後,任務執行結果通過 “IJobHandler” 的返回值 “ReturnT” 進行判斷;
當返回值符合 “ReturnT.code == ReturnT.SUCCESS_CODE” 時表示任務執行成功,否則表示任務執行失敗,而且可以通過 “ReturnT.msg” 回撥錯誤資訊給排程中心;
從而,在任務邏輯中可以方便的控制任務執行結果;
9 分片廣播 & 動態分片
執行器叢集部署時,任務路由策略選擇”分片廣播”情況下,一次任務排程將會廣播觸發對應叢集中所有執行器執行一次任務,同時傳遞分片引數;可根據分片引數開發分片任務;
“分片廣播” 以執行器為維度進行分片,支援動態擴容執行器叢集從而動態增加分片數量,協同進行業務處理;在進行大資料量業務操作時可顯著提升任務處理能力和速度。
“分片廣播” 和普通任務開發流程一致,不同之處在於可以可以獲取分片引數,獲取分片引數進行分片業務處理。
- Java語言任務獲取分片引數方式:BEAN、GLUE模式(Java)
// 可參考Sample示例執行器中的示例任務"ShardingJobHandler"瞭解試用
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
- 指令碼語言任務獲取分片引數方式:GLUE模式(Shell)、GLUE模式(Python)、GLUE模式(Nodejs)
// 指令碼任務入參固定為三個,依次為:任務傳參、分片序號、分片總數。以Shell模式任務為例,獲取分片引數程式碼如下
echo "分片序號 index = $2"
echo "分片總數 total = $3"
分片引數屬性說明:
index:當前分片序號(從0開始),執行器叢集列表中當前執行器的序號;
total:總分片數,執行器叢集的總機器數量;
該特性適用場景如:
- 1、分片任務場景:10個執行器的叢集來處理10w條資料,每臺機器只需要處理1w條資料,耗時降低10倍;
- 2、廣播任務場景:廣播執行器機器執行shell指令碼、廣播叢集節點進行快取更新等
10 訪問令牌(AccessToken)
為提升系統安全性,排程中心和執行器進行安全性校驗,雙方AccessToken匹配才允許通訊;
排程中心和執行器,可通過配置項 “xxl.job.accessToken” 進行AccessToken的設定。
排程中心和執行器,如果需要正常通訊,只有兩種設定;
- 設定一:排程中心和執行器,均不設定AccessToken;關閉安全性校驗;
- 設定二:排程中心和執行器,設定了相同的AccessToken;
11 排程中心API服務
排程中心提供了API服務,供執行器和業務方選擇使用,目前提供的API服務有:
1、任務結果回撥服務;
2、執行器註冊服務;
3、執行器註冊摘除服務;
4、觸發任務單次執行服務,支援任務根據業務事件觸發;
排程中心API服務位置:com.xxl.job.core.biz.AdminBiz.java
排程中心API服務請求參考程式碼:com.xxl.job.adminbiz.AdminBizTest.java
12 執行器API服務
執行器提供了API服務,供排程中心選擇使用,目前提供的API服務有:
1、心跳檢測
2、忙碌檢測
3、觸發任務執行
4、獲取Rolling Log
5、終止任務
執行器API服務位置:com.xxl.job.core.biz.ExecutorBiz
執行器API服務請求參考程式碼:com.xxl.executor.test.DemoJobHandlerTest
13 故障轉移 & 失敗重試
一次完整任務流程包括”排程(排程中心) + 執行(執行器)”兩個階段。
- “故障轉移”發生在排程階段,在執行器叢集部署時,如果某一臺執行器發生故障,該策略支援自動進行Failover切換到一臺正常的執行器機器並且完成排程請求流程。
- “失敗重試”發生在”排程 + 執行”兩個階段,如下:
- 排程中心排程失敗時,任務失敗處理策略選擇”失敗重試”,將會自動重試一次;
- 執行器執行失敗時,任務執行結果返回”失敗重試(IJobHandler.FAIL_RETRY)”回撥,將會自動重試一次;
14 執行器灰度上線
排程中心與業務解耦,只需部署一次後常年不需要維護。但是,執行器中託管執行著業務作業,作業上線和變更需要重啟執行器,尤其是Bean模式任務。
執行器重啟可能會中斷執行中的任務。但是,XXL-JOB得益於自建執行器與自建註冊中心,可以通過灰度上線的方式,避免因重啟導致的任務中斷的問題。
步驟如下:
- 1、執行器改為手動註冊,下線一半機器列表(A組),線上執行另一半機器列表(B組);
- 2、等待A組機器任務執行結束並編譯上線;執行器註冊地址替換為A組;
- 3、等待B組機器任務執行結束並編譯上線;執行器註冊地址替換為A組+B組;
操作結束;
15 任務執行結果說明
系統根據以下標準判斷任務執行結果,可參考之。
– | Bean/Glue(Java) | Glue(Shell) 等指令碼任務 |
---|---|---|
成功 | IJobHandler.SUCCESS | 0 |
失敗 | IJobHandler.FAIL | -1(其他) |
失敗重試 | IJobHandler.FAIL_RETRY | 101 |