Quartz調度系統入門和調度高可用實現方案

** 版本:2.2.1 **

Hello world:

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

配置:classpath下的quartz.properties

觸發器類型:ScheduleBuilder接口

  1. CalendarIntervalScheduleBuilder:
  • 通過指定對應日期的定時執行觸發器
  1. CronScheduleBuilder:
  • cron表達式實現的定時執行
  1. DailyTimeIntervalScheduleBuilder:
  • 根據時間定時執行
  1. SimpleScheduleBuilder
  • 簡單循環執行,設定執行次數,開始結束時間等

注解:

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

任務本身發生異常

  1. 再次嘗試執行:
JobExecutionException e2 =
                new JobExecutionException(e);
            // this job will refire immediately
            e2.refireImmediately();
  1. 不再執行,所有該job的調度全部停止:
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);

監聽器

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

持久化

  • RAMJobStore:將工作中的作業Job和調度觸發器Trigger都存儲在內存中,宕機都沒了。
  • JobStoreX:將Job和Trigger都存儲在數據庫中,實例重啟會自動掃描數據庫恢復數據,可進行集群配置,詳細請看下面的集群配置。

Misfire處理規則

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

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

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

SimpleTrigger有一堆的另外的MisFire機制,這里先不做討論,以后有機會再更新,如下:
withMisfireHandlingInstructionFireNowwithMisfireHandlingInstructionIgnoreMisfireswithMisfireHandlingInstructionNextWithExistingCountwithMisfireHandlingInstructionNowWithExistingCountwithMisfireHandlingInstructionNextWithRemainingCountwithMisfireHandlingInstructionNowWithRemainingCountMISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

執行中Job可獲取的時間

這些時間是在某一次的調度作業的作業執行過程中可以獲取到的時間戳。

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

集群

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

實驗結論

  • 簡單來說就是多個quartz節點共同訪問同一個數據庫來保證各個節點的調度信息同步,后臺有守護線程實時同步節點內存和數據庫中的信息同步

  • 一個節點宕機,mysql數據沒丟失,重啟后從mysql讀取恢復內存信息還原宕機前狀態

  • 一個被調度的任務由哪個節點執行調度?所有節點去搶mysql表中一個分布式鎖(悲觀),誰搶到了就誰執行當前任務

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

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

  • 配置:

#集群名稱和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配置和數據表前綴等基礎配置
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
#集群模式和集群節點間活性檢測臨界時間
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容災機制

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

負載均衡

  • Quartz集群自動支持節點間任務調度的負載均衡。
  • 由于自身實現調度集群分布式鎖、節點數據同步,因此部署好Quartz集群之后就自然而然實現了宕機自動節點切換,服務器壓力大直接橫向拓展也能迅速應對短時間內的業務爆發。

缺點

Quartz自身提供了對任務調度本身的不重不漏的高可用保證,但是一個任務確實被Quartz正確調度之后呢,Quartz系統中的記錄已經標記為這個任務執行完成了,但是這個任務在執行過程中出錯了,怎么辦?

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

我的方案:

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

一個Job的整個執行過程分解

  • (1). 記錄start日志
  • (2). 調度記錄表插入記錄之前的數據準備
  • (3). 調度記錄表查詢本次調度記錄,狀態ready
  • (4). 準備本次作業流調度執行需要的相關數據,并設置狀態位scheduling
  • (5). 調用作業流執行的dubbo服務,延時操作,等待調度結果
  • (6).
  • 成功:狀態位success
  • 失敗:設置為retrying,等待守護線程掃描之后重試
  • 不支持的調度:直接設置fail,告警
  • (7). 記錄日志end

quartz系統在Jobs平臺下的高可用保證的改造方案

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

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容