Quartz調(diào)度系統(tǒng)入門和調(diào)度高可用實現(xiàn)方案

** 版本:2.2.1 **

Hello world:

  • 調(diào)度器:
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
 scheduler.start();
  • 任務(wù)詳情:任務(wù)體實現(xiàn)Job接口
JobDetail job = JobBuilder.newJob(MakeHtml.class)
                .withIdentity("job1", "group1")
                .build();
  • 觸發(fā)器:
Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
                .build();
  • 執(zhí)行調(diào)度:
scheduler.scheduleJob(job, trigger);
  • 數(shù)據(jù)傳輸、參數(shù)傳輸:
//傳入
job.getJobDataMap().put("FAVORITE_COLOR", "red");
//在執(zhí)行線程獲得
jobExecutionContext.getJobDetail().getJobDataMap();
//在該Map中嵌套傳輸Map可實現(xiàn)對象引用的傳輸,即實現(xiàn)實時對象參數(shù)傳輸,需要保證線程安全。
  • 多任務(wù)對象管理:
    Scheduler對象中保存了JobKey、TriggerKey等和對應(yīng)的Job、Trigger的映射關(guān)系,可以通過該Map進行同對象復(fù)用和檢索。

配置:classpath下的quartz.properties

觸發(fā)器類型:ScheduleBuilder接口

  1. CalendarIntervalScheduleBuilder:
  • 通過指定對應(yīng)日期的定時執(zhí)行觸發(fā)器
  1. CronScheduleBuilder:
  • cron表達式實現(xiàn)的定時執(zhí)行
  1. DailyTimeIntervalScheduleBuilder:
  • 根據(jù)時間定時執(zhí)行
  1. SimpleScheduleBuilder
  • 簡單循環(huán)執(zhí)行,設(shè)定執(zhí)行次數(shù),開始結(jié)束時間等

注解:

  • @PersistJobDataAfterExecution:執(zhí)行完成把狀態(tài)持久化保存
  • 目前發(fā)現(xiàn)沒這個注解,JobDataMap中的數(shù)據(jù)依然還是在數(shù)據(jù)庫中保存著,不明所以,可能這個注解的作用是在每次執(zhí)行調(diào)度刷新一次數(shù)據(jù)保持?jǐn)?shù)據(jù)庫中的數(shù)據(jù)是最新的值吧。。。
  • @DisallowConcurrentExecution:Job對象多實例禁止并發(fā)執(zhí)行
  • 就是當(dāng)這個調(diào)度作業(yè)還沒執(zhí)行完成的時候,下一次的調(diào)度又到了,如果注解了表示不會再申請一個線程讓兩個Job并發(fā)執(zhí)行,需要等上一次作業(yè)執(zhí)行完成才串行的執(zhí)行。

任務(wù)本身發(fā)生異常

  1. 再次嘗試執(zhí)行:
JobExecutionException e2 =
                new JobExecutionException(e);
            // this job will refire immediately
            e2.refireImmediately();
  1. 不再執(zhí)行,所有該job的調(diào)度全部停止:
JobExecutionException e2 =
                new JobExecutionException(e);
            // Quartz will automatically unschedule
            // all triggers associated with this job
            // so that it does not run again
            e2.setUnscheduleAllTriggers(true);

監(jiān)聽器

  • 沒難度,就是在各種事件或者生命周期過程中進行回調(diào)。
  • scheduler.getListenerManager()添加各式各樣的監(jiān)聽器,主要有三種:JobListener、TriggerListener、SchedulerListener
  • 支持通過JobKey和TriggerKey定向?qū)δ硞€Job或Trigger進行專屬監(jiān)聽。
  • JobListener
  • getName:獲取監(jiān)聽器名字
  • jobToBeExecuted:job執(zhí)行前
  • jobExecutionVetoed:job執(zhí)行被觸發(fā)器拒絕
  • jobWasExecuted:job執(zhí)行完
  • TriggerListener
  • getName:獲取監(jiān)聽器名字
  • triggerFired:觸發(fā)器觸發(fā)
  • vetoJobExecution:觸發(fā)器執(zhí)行拒絕Job,返回true就是拒絕
  • triggerMisfired:觸發(fā)器發(fā)現(xiàn)MisFire
  • triggerComplete:觸發(fā)器觸發(fā)完成
  • SchedulerListener
  • jobScheduled:job調(diào)度完
  • jobUnscheduled:作業(yè)沒被調(diào)度
  • triggerFinalized:有調(diào)度器被完全停止調(diào)度時
  • triggerPaused:單個觸發(fā)器暫停
  • triggersPaused:全部觸發(fā)器暫停
  • triggerResumed:單個觸發(fā)器喚醒
  • triggersResumed:全部觸發(fā)器喚醒
  • jobAdded:job添加
  • jobDeleted:job刪除
  • jobPaused:單個job暫停
  • jobsPaused:全部job暫停
  • jobResumed:單個job喚醒
  • jobsResumed:全部job喚醒
  • schedulerError:調(diào)度出錯
  • schedulerInStandbyMode:調(diào)度器正處于standby模式
  • schedulerStarted:調(diào)度器啟動完成
  • schedulerStarting:調(diào)度器正在啟動
  • schedulerShutdown:調(diào)度器關(guān)閉完成
  • schedulerShuttingdown:調(diào)度器正在關(guān)閉
  • schedulingDataCleared:調(diào)度器數(shù)據(jù)清除完成

持久化

  • RAMJobStore:將工作中的作業(yè)Job和調(diào)度觸發(fā)器Trigger都存儲在內(nèi)存中,宕機都沒了。
  • JobStoreX:將Job和Trigger都存儲在數(shù)據(jù)庫中,實例重啟會自動掃描數(shù)據(jù)庫恢復(fù)數(shù)據(jù),可進行集群配置,詳細(xì)請看下面的集群配置。

Misfire處理規(guī)則

  • 指的是不小心沒調(diào)度時,對錯過的調(diào)度次數(shù)如何處理的規(guī)則策略選擇,情形如下:
  1. 比如調(diào)度器休眠了
  2. quartz集群全體宕機了再重啟之后掃描表中數(shù)據(jù)得知之前的調(diào)度錯失了就是這種情況

策略選擇(指的是CronTrigger,而SimpleTrigger有其對應(yīng)的策略,在這里不做探討):

  • withMisfireHandlingInstructionDoNothing
——不觸發(fā)立即執(zhí)行
——等待下次Cron觸發(fā)頻率到達時刻開始按照Cron頻率依次執(zhí)行
  • 這是網(wǎng)上的說法,表達是正確的,我經(jīng)過代碼測試的結(jié)果是,調(diào)度刻度依然不變,就是很干脆地把錯過的那些調(diào)度直接不管了
  • 調(diào)度刻度:指的是作業(yè)放入調(diào)度器之后通過cron表達式計算出的之后的每個需要調(diào)度的時間點組成的一段點線段,就如刻度尺上面的刻度,到達刻度點時就觸發(fā)調(diào)度。
  • withMisfireHandlingInstructionIgnoreMisfires
——以錯過的第一個頻率時間立刻開始執(zhí)行
——重做錯過的所有頻率周期后
——當(dāng)下一次觸發(fā)頻率發(fā)生時間大于當(dāng)前時間后,再按照正常的Cron頻率依次執(zhí)行
  • 這是網(wǎng)上說法,意思很明了,就是指錯過的那些調(diào)度都會全部重新執(zhí)行一遍,但是需要注意的是,如果錯過的調(diào)度數(shù)量很多,這一大堆的調(diào)度也是在發(fā)現(xiàn)misfire的之后的短時間內(nèi)一次性全部完成的,然后接著按照調(diào)度刻度進行執(zhí)行。這時候在調(diào)度任務(wù)內(nèi)部獲取的兩個時間:fireTime和scheduleFireTime,fireTime指的是misfire發(fā)現(xiàn)之后重新調(diào)度的實際時間,scheduleFireTime指的是調(diào)度刻度上的基準(zhǔn)時間,比如我有個本來應(yīng)該在12:12:12執(zhí)行的作業(yè),但是發(fā)生misfire或者failover了,重啟之后根據(jù)策略把錯過的任務(wù)重新執(zhí)行,這時候這個任務(wù)的實際調(diào)度時間可能為12:20:20,所有你如果有些任務(wù)的執(zhí)行是需要依賴于標(biāo)準(zhǔn)的調(diào)度時間的(比如每隔一小時dump數(shù)據(jù)庫的數(shù)據(jù)一次,應(yīng)該獲取的時間戳是scheduleFireTime而不是fireTime),這點要注意。
  • withMisfireHandlingInstructionFireAndProceed
——以當(dāng)前時間為觸發(fā)頻率立刻觸發(fā)一次執(zhí)行
——然后按照Cron頻率依次執(zhí)行
  • 這是網(wǎng)上說法,說的是可能misfire錯過了一堆任務(wù),這里只在發(fā)現(xiàn)misfire的時候補償性地調(diào)度一次該任務(wù),接下來還是按照調(diào)度刻度執(zhí)行。
  • 特別注意!?。【W(wǎng)上有的說法是調(diào)度刻度會在這種策略下平移,如下:16:00要執(zhí)行的調(diào)度,結(jié)果misfire了,到16:15才恢復(fù),網(wǎng)上的說法是16:15會調(diào)度一次,然后刻度往后移,下一次調(diào)度會在17:15發(fā)生,但是!??!我的代碼測試結(jié)果卻是:16:15確實會調(diào)用一次,這是策略控制結(jié)果,下一次的調(diào)度時間是17:00,調(diào)度刻度并沒有變!?。∷哉f這個策略和第二個策略其實是類似的,只不過第二個策略是把錯過的全部調(diào)度一次,這個是只調(diào)度一次,而且是用恢復(fù)的這一瞬間作為scheduleFireTime。

SimpleTrigger有一堆的另外的MisFire機制,這里先不做討論,以后有機會再更新,如下:
withMisfireHandlingInstructionFireNow、withMisfireHandlingInstructionIgnoreMisfires、withMisfireHandlingInstructionNextWithExistingCountwithMisfireHandlingInstructionNowWithExistingCount、withMisfireHandlingInstructionNextWithRemainingCount、withMisfireHandlingInstructionNowWithRemainingCount、MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

執(zhí)行中Job可獲取的時間

這些時間是在某一次的調(diào)度作業(yè)的作業(yè)執(zhí)行過程中可以獲取到的時間戳。

  • PreviousFireTime:當(dāng)前調(diào)度的上一次調(diào)度的時間戳。
  • ScheduledFireTime:當(dāng)前調(diào)度的基準(zhǔn)調(diào)度刻度中的時間戳,就是任務(wù)一開始調(diào)度就算出來的未來一系列的調(diào)度刻度。
  • FireTime:當(dāng)前調(diào)度的實際調(diào)度時間戳,通常和ScheduledFireTime一致,但是如果發(fā)生misFire或者Fail-Over就可能和ScheduledFireTime不一致。
  • NextFireTime:下一次調(diào)度的基準(zhǔn)刻度時間。

集群

  • 集群通過故障切換和負(fù)載平衡的功能,能給調(diào)度器帶來高可用性和伸縮性。目前集群只能工作在JDBC-JobStore(JobStore TX或者JobStoreCMT)方式下,從本質(zhì)上來說,是使集群上的每一個節(jié)點通過共享同一個數(shù)據(jù)庫來工作的(Quartz通過啟動兩個維護線程來維護數(shù)據(jù)庫狀態(tài)實現(xiàn)集群管理,一個是檢測節(jié)點狀態(tài)線程,一個是恢復(fù)任務(wù)線程)。
  • 負(fù)載平衡是自動完成的,集群的每個節(jié)點會盡快觸發(fā)任務(wù)。當(dāng)一個觸發(fā)器的觸發(fā)時間到達時,第一個節(jié)點將會獲得任務(wù)(通過鎖定),成為執(zhí)行任務(wù)的節(jié)點。
  • 故障切換的發(fā)生是在當(dāng)一個節(jié)點正在執(zhí)行一個或者多個任務(wù)失敗的時候。當(dāng)一個節(jié)點失敗了,其他的節(jié)點會檢測到并且標(biāo) 識在失敗節(jié)點上正在進行的數(shù)據(jù)庫中的任務(wù)。任何被標(biāo)記為可恢復(fù)(任務(wù)詳細(xì)信息的"requests recovery"屬性)的任務(wù)都會被其他的節(jié)點重新執(zhí)行。沒有標(biāo)記可恢復(fù)的任務(wù)只會被釋放出來,將會在下次相關(guān)觸發(fā)器觸發(fā)時執(zhí)行。

實驗結(jié)論

  • 簡單來說就是多個quartz節(jié)點共同訪問同一個數(shù)據(jù)庫來保證各個節(jié)點的調(diào)度信息同步,后臺有守護線程實時同步節(jié)點內(nèi)存和數(shù)據(jù)庫中的信息同步

  • 一個節(jié)點宕機,mysql數(shù)據(jù)沒丟失,重啟后從mysql讀取恢復(fù)內(nèi)存信息還原宕機前狀態(tài)

  • 一個被調(diào)度的任務(wù)由哪個節(jié)點執(zhí)行調(diào)度?所有節(jié)點去搶mysql表中一個分布式鎖(悲觀),誰搶到了就誰執(zhí)行當(dāng)前任務(wù)

  • 宕機切換:我測試在一臺機器上啟動4個quartz實例模擬集群,其中把一個搶到鎖的實例kill掉,quartz會自動切換另一個實例繼續(xù)執(zhí)行剩下的調(diào)度

  • quartz能夠保證一個作業(yè)在cron表達式作用下的一次調(diào)度不重不漏,以及宕機調(diào)度任務(wù)重新分配,但是當(dāng)一個Job被quartz正確調(diào)度了,在Job內(nèi)部邏輯過程中出錯拋異常了、或者此時宕機了,那這個Job在quartz系統(tǒng)中其實是已執(zhí)行狀態(tài),因為的確正確調(diào)度了,只不過調(diào)度執(zhí)行的Job本身內(nèi)部出錯了,quartz對Job內(nèi)部異常也有相應(yīng)的方案,上面有說,但是在作業(yè)平臺調(diào)度系統(tǒng)設(shè)計過程中覺得quartz本身提供的job異常機制不夠可靠,對此進行了這方面的高可用拓展,詳細(xì)請看Jobs作業(yè)平臺的調(diào)度系統(tǒng)設(shè)計方案

  • 配置:

#集群名稱和id
org.quartz.scheduler.instanceName = MyClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
#線程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 25
org.quartz.threadPool.threadPriority = 5
#misfire檢測時間
org.quartz.jobStore.misfireThreshold = 60000
#jobStore配置和數(shù)據(jù)表前綴等基礎(chǔ)配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ
#集群模式和集群節(jié)點間活性檢測臨界時間
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
#database jdbs配置
org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@cluster:1521:dev
org.quartz.dataSource.myDS.user = quartz
org.quartz.dataSource.myDS.password = quartz
org.quartz.dataSource.myDS.maxConnections = 5
org.quartz.dataSource.myDS.validationQuery=select 0 from dual

Fail-Over容災(zāi)機制

  • 系統(tǒng)崩潰、某個節(jié)點宕機的情況下,其他節(jié)點自動主備替換的機制,整個機制了高可用Hadoop的主備切換思路也是類似的,主宕機就備掙鎖選舉。
  • Fail-Over機制工作在集群環(huán)境中,執(zhí)行recovery工作的線程類叫做ClusterManager,該線程類同樣是在調(diào)度器初始化時就開啟運行了。這個線程類在運行期間每15s進行一次check in操作,所謂check in,就是在數(shù)據(jù)庫的QRTZ2_SCHEDULER_STATE表中更新該調(diào)度器對應(yīng)的LAST_CHECKIN_TIME字段為當(dāng)前時間,并且查看其他調(diào)度器實例的該字段有沒有發(fā)生停止更新的情況,如果檢查到有調(diào)度器的check in time比當(dāng)前時間要早約15s(視具體的執(zhí)行預(yù)配置情況而定),那么就判定該調(diào)度實例需要recover,隨后會啟動該調(diào)度器的recovery機制,獲取目標(biāo)調(diào)度器實例正在觸發(fā)的trigger,并針對每一個trigger臨時添加一各對應(yīng)的僅執(zhí)行一次的simpletrigger。等到調(diào)度流程掃描trigger時,這些trigger會被觸發(fā),這樣就成功的把這些未完整執(zhí)行的調(diào)度以一種特殊trigger的形式納入了普通的調(diào)度流程中,只要調(diào)度流程在正常運行,這些被recover的trigger就會很快被觸發(fā)并執(zhí)行。
    就是這個機制,使用了SimpleTrigger導(dǎo)致了上面的fireTime和scheduleFireTime可能不同的情況。

負(fù)載均衡

  • Quartz集群自動支持節(jié)點間任務(wù)調(diào)度的負(fù)載均衡。
  • 由于自身實現(xiàn)調(diào)度集群分布式鎖、節(jié)點數(shù)據(jù)同步,因此部署好Quartz集群之后就自然而然實現(xiàn)了宕機自動節(jié)點切換,服務(wù)器壓力大直接橫向拓展也能迅速應(yīng)對短時間內(nèi)的業(yè)務(wù)爆發(fā)。

缺點

Quartz自身提供了對任務(wù)調(diào)度本身的不重不漏的高可用保證,但是一個任務(wù)確實被Quartz正確調(diào)度之后呢,Quartz系統(tǒng)中的記錄已經(jīng)標(biāo)記為這個任務(wù)執(zhí)行完成了,但是這個任務(wù)在執(zhí)行過程中出錯了,怎么辦?

  • Quartz自身提供了兩套策略:
  1. 任務(wù)失敗不再重試
  2. 任務(wù)失敗自行重試
  • 這兩套方案粒度太粗了,或者說任務(wù)自動重試再次失敗呢?還要接著重試嗎?總而言之就是在我做的統(tǒng)計作業(yè)平臺對調(diào)度要求不重不漏(其實允許重,但是不允許漏),并且要支持在重試次數(shù)上限下的失敗重試,并且需要對作業(yè)平臺中的作業(yè)調(diào)度失敗原因做出不同的錯誤處理策略。

我的方案:

  • 任務(wù)調(diào)度高可用 —— 依然依靠Quartz集群提供
  • 任務(wù)調(diào)度了,在任務(wù)執(zhí)行過程中出錯,這段處理邏輯中的出錯處理,我來控制,策略如下:
  1. 任務(wù)失敗自動重試,到達重試上線設(shè)置失敗
  2. 開機重啟掃描任務(wù)列表,把正在調(diào)度狀態(tài)的作業(yè)進行開機恢復(fù)
  3. 任務(wù)調(diào)度前后記錄start和end狀態(tài)位日志(Quartz監(jiān)聽器實現(xiàn)),后臺定時掃描start和end的對應(yīng)關(guān)系,對不對應(yīng)的任務(wù)進行恢復(fù),這步執(zhí)行概率極低,是在極端情況下的調(diào)度任務(wù)丟失采取的最后的保障措施
  • 下面詳細(xì)說明我的任務(wù)調(diào)度高可用方案實現(xiàn)過程

一個Job的整個執(zhí)行過程分解

  • (1). 記錄start日志
  • (2). 調(diào)度記錄表插入記錄之前的數(shù)據(jù)準(zhǔn)備
  • (3). 調(diào)度記錄表查詢本次調(diào)度記錄,狀態(tài)ready
  • (4). 準(zhǔn)備本次作業(yè)流調(diào)度執(zhí)行需要的相關(guān)數(shù)據(jù),并設(shè)置狀態(tài)位scheduling
  • (5). 調(diào)用作業(yè)流執(zhí)行的dubbo服務(wù),延時操作,等待調(diào)度結(jié)果
  • (6).
  • 成功:狀態(tài)位success
  • 失?。涸O(shè)置為retrying,等待守護線程掃描之后重試
  • 不支持的調(diào)度:直接設(shè)置fail,告警
  • (7). 記錄日志end

quartz系統(tǒng)在Jobs平臺下的高可用保證的改造方案

  1. 系統(tǒng)啟動:守護線程調(diào)度(jobs-schedule-daemon)、恢復(fù)線程調(diào)度(jobs-schedule-recovery)這兩個是系統(tǒng)分組中的調(diào)度
  • jobs-schedule-daemon負(fù)責(zé)三分鐘掃描一次調(diào)度記錄表中的retrying狀態(tài)的記錄,并將其添加新的調(diào)度來重新執(zhí)行,添加的調(diào)度在jobs-retry分組,該線程后臺死循環(huán)重復(fù)執(zhí)行
  • jobs-schedule-recovery是宕機恢復(fù)調(diào)度,在系統(tǒng)宕機重啟之后自動掃描調(diào)度記錄表中的ready和scheduling兩種狀態(tài)的記錄,將其重新執(zhí)行調(diào)度,分組為jobs-recovery,之后恢復(fù)線程結(jié)束
  1. 之后按照各個作業(yè)調(diào)度器cron表達式正確調(diào)度
  2. 作業(yè)各個狀態(tài)高可用保證:
  • 步驟(3):ready狀態(tài),宕機重啟會進行恢復(fù)
  • 步驟(4):scheduling狀態(tài),宕機重啟會進行恢復(fù)
  • 步驟(5):retrying狀態(tài),守護線程每隔3分鐘掃描一次進行重試調(diào)度
  • 步驟(6):success和fail狀態(tài),屬于最終狀態(tài),已完成
  • 以上的步驟基于調(diào)度記錄表對應(yīng)調(diào)度記錄存在的情況下保證高可用
  1. 步驟(1)和(2)是在沒有MySQL表記錄的情況下,要保證高可用計劃通過前后的start和end日志對稱對比來實現(xiàn)宕機作業(yè)恢復(fù),該步驟除了mysql連接以外都是內(nèi)存計算,宕機可能性極地,并且這幾個步驟沒有mysql表中對應(yīng)的一條記錄為依托,因此只能依靠任務(wù)調(diào)度前后的start和end狀態(tài)日志進行任務(wù)丟失恢復(fù)。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容