quartz作爲成熟的任務調度系統對系統的異常及崩潰後處理機制有很好的設計,以保證整個調度過程是一個邏輯閉環,任何階段出現的問題都可以通過框架中的機制盡最大限度的彌補,並將系統的狀態引向正軌。
首先要明確的是:quartz如果在執行具體任務時,在任務執行過程中拋出異常,那麼不作任何處理,這是使用者程序本身的問題,不需要框架處理。
下面介紹quartz中的兩大類異常情況:
misfired 啞火(*注:筆者自己直譯)
fail-over 故障轉移
1.misfired 啞火
啞火顧名思義,就是quartz在應該觸發(fire)trigger的時候未能及時將其觸發,這將導致trigger的下次觸發時間落在當前時間之前,那麼按照正常的quartz調度流程,該trigger就再也沒有機會被調度了。由於一個調度器實例在每次調度的過程中都會有一定的睡眠時間,存在在一段時間內所有調度器實例都在睡眠,而無人觸發調度的潛在可能。於是調度器需要每隔一段時間(15s~60s)查看一次各trigger的nextfiretime,檢查出否有tirgger的下一次觸發落在了當前時間之前足夠長的時間,在這裏系統設定了一個60s的域(misfireThreshold),當一個trigger的下一次觸發時間早於當前時間60s之外時,調度器判定該觸發器misfired,在發現有觸發器啞火之後啓動相應的流程回覆trigger至正常狀態。上述這些過程是在調度器初始化時與主調度線程類quartzSchedulerThread同時開啓的一個線程類MisfireHandler中進行的。
下面是quartz檢測misfired的邏輯:
下面是quartz對misfire處理的關鍵代碼:
對trigger啞火處理的最關鍵一點在於針對不同策略對trigger的nextfiretime進行設定,這一過程對於不同的trigger類型有不同的策略供選擇。
下面是各種不同triigger對應的不同misfire策略(摘自網絡):
CronTrigger
withMisfireHandlingInstructionDoNothing
——不觸發立即執行
——等待下次Cron觸發頻率到達時刻開始按照Cron頻率依次執行
withMisfireHandlingInstructionIgnoreMisfires
——以錯過的第一個頻率時間立刻開始執行
——重做錯過的所有頻率週期後
——當下一次觸發頻率發生時間大於當前時間後,再按照正常的Cron頻率依次執行
withMisfireHandlingInstructionFireAndProceed(默認)
——以當前時間爲觸發頻率立刻觸發一次執行
——然後按照Cron頻率依次執行
SimpleTrigger
withMisfireHandlingInstructionFireNow
——以當前時間爲觸發頻率立即觸發執行
——執行至FinalTIme的剩餘週期次數
——以調度或恢復調度的時刻爲基準的週期頻率,FinalTime根據剩餘次數和當前時間計算得到
——調整後的FinalTime會略大於根據starttime計算的到的FinalTime值
withMisfireHandlingInstructionIgnoreMisfires
——以錯過的第一個頻率時間立刻開始執行
——重做錯過的所有頻率週期
——當下一次觸發頻率發生時間大於當前時間以後,按照Interval的依次執行剩下的頻率
——共執行RepeatCount+1次
withMisfireHandlingInstructionNextWithExistingCount
——不觸發立即執行
——等待下次觸發頻率週期時刻,執行至FinalTime的剩餘週期次數
——以startTime爲基準計算週期頻率,並得到FinalTime
——即使中間出現pause,resume以後保持FinalTime時間不變
withMisfireHandlingInstructionNowWithExistingCount(默認)
——以當前時間爲觸發頻率立即觸發執行
——執行至FinalTIme的剩餘週期次數
——以調度或恢復調度的時刻爲基準的週期頻率,FinalTime根據剩餘次數和當前時間計算得到
——調整後的FinalTime會略大於根據starttime計算的到的FinalTime值
withMisfireHandlingInstructionNextWithRemainingCount
——不觸發立即執行
——等待下次觸發頻率週期時刻,執行至FinalTime的剩餘週期次數
——以startTime爲基準計算週期頻率,並得到FinalTime
——即使中間出現pause,resume以後保持FinalTime時間不變
withMisfireHandlingInstructionNowWithRemainingCount
——以當前時間爲觸發頻率立即觸發執行
——執行至FinalTIme的剩餘週期次數
——以調度或恢復調度的時刻爲基準的週期頻率,FinalTime根據剩餘次數和當前時間計算得到
——調整後的FinalTime會略大於根據starttime計算的到的FinalTime值
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
——此指令導致trigger忘記原始設置的starttime和repeat-count
——觸發器的repeat-count將被設置爲剩餘的次數
——這樣會導致後面無法獲得原始設定的starttime和repeat-count值
2.fail-over 故障轉移
quartz考慮的另一個問題是運行時的系統崩潰,在集羣中如果有一個節點突然崩潰,那麼它所執行的任務會被首先發現其崩潰的節點接手,重新執行,換句話說就是把故障節點的工作轉移到其他節點上,簡稱故障轉移。recovery機制工作在集羣環境中,執行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就會很快被觸發並執行。
下面的代碼是ClusterManager線程類的run方法,可以看到,該線程類不斷地在調用manage方法,該方法中包含了check in與recover的邏輯。
manage方法主要調用doCheckIn方法,該方法中承載的check in與recover的詳細邏輯:
在代碼中對第一次check in的操作比較令人困惑,不管是不是第一次check in似乎都需要調用clusterCheckIn方法,而該方法內部又調用了findFailedInstances方法,見代碼:
而findFailedInstances方法中有一個處理無主trigger的邏輯,無主trigger是說在QRTZ2_FIRED_TRIGGERS表中如果一條記錄的調度器id在QRTZ2_SCHEDULER_STATE表中找不到相應的記錄,那麼這條trigger觸發就成爲一個無主的記錄。這種記錄只能查詢到其調用者id,而無法知道QRTZ2_SCHEDULER_STATE表中可以查到的其他字段,需要系統做特殊處理(TODO:如何處理)。
上述的情況需要在五新的QRTZ2_SCHEDULER_STATE記錄插入時進行,所以在doCheckin中的安排僅僅是爲了讓調度器在自身數據要加入進QRTZ2_SCHEDULER_STATE表之前檢查一遍是否有這種無主數據,並做處理。
回到doCheckin方法中,得到需要recover的調度器實例列表,啓動recover流程。clusterRecover的代碼如下:
可以看到,最終一個需要recover的節點,其未執行完整的任務最終會被其他節點已新建臨時trigger的形式重新執行。這一系列的機制正是保證quartz穩定性的可靠保證。