Tags:定時作業調度
分布式定時任務調度
Quartz
TBSchedule
Elastic-job
基于給定時間點,給定時間間隔或者給定執行次數自動執行任務。
Timer
對于簡單的有固定間隔(period)的任務,使用JAVA內置的Timer即可解決問題。
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("do sth...");
}
}, 1000, 2000);
}
特點:in JDK
,簡潔
,單線程
對于簡單的定時任務,Timer是非常實用的類,做一些常規的簡單任務,如在線程池中用Timer掃描出空閑線程。
ScheduledExecutor
多線程的固定間隔簡單調度,JDK也提供了工具類
public static class ScheduledExecutorTest implements Runnable {
private String jobName = "";
public ScheduledExecutorTest(String jobName) {
super();
this.jobName = jobName;
}
@Override
public void run() {
System.out.println("execute " + jobName);
}
public static void main(String[] args) {
//執行線程池大小
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
long initialDelay1 = 1;
long period1 = 1;
// 從現在開始1秒鐘之后,每隔1秒鐘執行一次job1
service.scheduleAtFixedRate(
new ScheduledExecutorTest("job1"), initialDelay1,
period1, TimeUnit.SECONDS);
long initialDelay2 = 1;
long delay2 = 1;
// 從現在開始2秒鐘之后,每隔2秒鐘執行一次job2
service.scheduleWithFixedDelay(
new ScheduledExecutorTest("job2"), initialDelay2,
delay2, TimeUnit.SECONDS);
}
}
特點:in JDK
多線程
線程池
Unix Crontab
相比較Timer這種固定間隔調度,crontab的可以使用cron表達式表達更復雜調度策略:
每1分鐘執行一次myCommand
* * * * * myCommand
實例2:每小時的第3和第15分鐘執行
3,15 * * * * myCommand
實例3:在上午8點到11點的第3和第15分鐘執行
3,15 8-11 * * * myCommand
實例4:每隔兩天的上午8點到11點的第3和第15分鐘執行
3,15 8-11 */2 * * myCommand
crontab往往和腳本搭配完成更復雜的任務,意味著當需要和主系統進行復雜交互時多有不便。
特點: Linux內置
Crontab表達式
Quartz##
Quartz是個開源JAVA庫,可以簡單看做以上三種的結合的擴展。
Scheduler:調度容器
Job:Job接口類
JobDetail :Job的描述類,job執行時的依據此對象的信息反射實例化出Job的具體執行對象。
Trigger:存放Job執行的時間策略
JobStore: 存儲作業和調度期間的狀態
Calendar:指定排除的時間點(如排除法定節假日)
Quartz的主要線程有兩類,負責調度的線程和負責Misfire(指錯過了執行時間的作業)的線程,其中負責調度的線程RegularSchedulerThread是基于線程池的,而Misfire只有一個線程。 兩類線程都會訪問抽象為JobStore的層來獲取作業策略或寫入調度狀態。
JobStore也分持久化(JobStoreSupport)和非持久化(RAMJobStore)兩種,使用場景大大不同,后面有敘述。
注意上圖左邊部分是調度器的守護線程QuartzScheduleThread的主要流程,也就是:QuartzScheduleThread會在RegularThread池有空閑時(否則block),從JobStore中取出N個(將來30秒內要觸發的)Trigger,并交給RegularThread線程池來運行job。
Quartz的功能非常豐富,結構也比上述的復雜的多,本文只是簡要介紹抽象層的概念,詳解請參考更多資料。
對于單機調度Quartz基本能完全滿足我們的需求,但多個機器怎么辦呢?
Quartz集群##
為了分擔單點壓力,往往需要多個節點運行定時任務,他們之間有協作又不能沖突。
Quartz用了一個比較取巧的方式支持集群定時調度。
首先,JobStore要選用數據庫持久化存儲:JDBCJobStore,且自己管理事務:JobStoreTX。
依附于本身的trigger存取策略,Quartz利用數據庫行級鎖來實現多節點的通訊(間接通訊)。
0.調度器線程run()
1.獲取待觸發trigger
1.1數據庫LOCKS表TRIGGER_ACCESS行加鎖
1.2讀取JobDetail信息
1.3讀取trigger表中觸發器信息并標記為"已獲取"
1.4commit事務,釋放鎖
2.觸發trigger
2.1數據庫LOCKS表STATE_ACCESS行加鎖
2.2確認trigger的狀態
2.3讀取trigger的JobDetail信息
2.4讀取trigger的Calendar信息
2.3更新trigger信息
2.3commit事務,釋放鎖
3實例化并執行Job
3.1從線程池獲取線程執行JobRunShell的run方法
讀取之前獲取鎖,寫入之后釋放鎖,這是Quartz集群解決集群同步的核心思想。
Quartz集群是用工具拼湊起來的一個方案,巧妙的運用了數據庫鎖解決同步問題,這在一些場景中是非常work的,但問題也依舊明顯:
解決了節點同步問題,但沒有解決分布式問題。
官方也做出說明,集群特性對于高cpu使用率的任務效果很好,但是對于大量的短任務,各個節點都會搶占數據庫鎖,這樣就出現大量的線程等待資源.這種情況隨著節點的增加會越來越嚴重.
有沒有解決分布式問題的方案?
TBSchedule##
類比Quartz集群用數據庫做存儲,TBSchedule則使用更符合分布式場景的zookeeper來做任務狀態。
zookeeper有永久節點存儲作業的配置信息,使用臨時節點存儲調度時的狀態,當其中一個調度端和zookeeper斷開鏈接時,回話消失臨時節點數據被抹除,所有在線調度端會感知到改變化并做出相應的動作。
來看幾個重要概念:
- 任務項
即分片。分布式機制是通過分片實現:
如:TaskItem: 0,1,2,3
可以用數據的ID取模對應TaskItem,一個TaskItem就代表了一部分 數據。
如上線了機器[A,B,C], TBScher會做如下分配:
[A=1,0,B=2,C=3]
如上線了機器[A,B,C,D,E], TBScher會做如下分配:
[A=0,B=1,C=2,D=3],E空閑。
分片操作由是leader節點執行,leader是最早上線的節點(編號最?。?。
節點感知
調度端會啟動一個刷新zookeeper的timer,如果有變動則回觸發leader的重新分配資源,
如:
新上線或下線了機器,會給各個調度端重新分配TaskItem。
暫停或重新啟動某個策略,調度端會停止之前的負責這個策略的線程組。觸發
TBScheduler依舊支持Crontab表達式,并進一步支持執行的時間段(超過時間段則暫停),
但其內里實現有異于Quartz:
對于一個策略,在首次啟動時會計算出該策略的下次執行開始時間和執行結束,然后分別啟動一個負責啟動和暫停的Timer,Timer內的操作就是對調度器的暫停和恢復,以及下一批Timer的創建。
TBScher的流式Job###
相對于Quartz的job只有execute,Tbscher的Job主要多了selectTasks()方法。
/**
* 單個任務處理實現
*
* @author xuannan
*
*/
public class DemoTaskBean implements IScheduleTaskDealSingle<Long> {
public List<Long> selectTasks(String taskParameter,String ownSign, int taskItemNum,
List<TaskItemDefine> queryCondition, int fetchNum) throws Exception {
List<Long> result = new ArrayList<Long>();
String message = "獲取數據...[ownSign=" + ownSign + ",taskParameter=\"" + taskParameter +"\"]:";
return result;
}
public boolean execute(Long task, String ownSign) throws Exception {
Thread.sleep(50);
log.info("處理任務["+ownSign+"]:" + task);
return true;
}
}
selectTasks返回的結果會被帶入execute中執行,當execute時task為空時會再次selectTasks,
一次調度中,selectTasks可能會被調用多次,直到返回空,結束本次調度。
TBSchedule的出現最大的進步之處在于從關注作業到關注數據。在此概念上造就了高性能,也真正解決了集群分布式問題。
缺點:
- 對zookeeper的操作都是原生客戶端的直接操作,維護起來易出錯外,zookeeper的高可用也沒有良好支持。zookeeper掛掉要重啟所有調度端。
- 文檔缺失,四年內沒有任何更新(2016),缺少開源社區的維護。
Elastic-job##
原理基本和TBSchedule一致。
一些重要概念:
leader選舉
調度端機器上線后會檢查有沒有leader,如果沒有則提議自己做leader,兩個同時上線引發沖突是由zookeeper的內部解決的,總之它可以保證只有一個主。
leader如果下線會觸發重新選舉,在選出下個leader前所有任務會被阻塞。分片
leader選舉后,leader以『協調者』角色負責分片,同時依賴zookeeper的臨時節點和監聽器的主動檢查和通知功能,對機器上、下線、任務配置更改、分片修改等事件做出響應。
任務的設計###
因為借助Quartz做實際調度工作,所以Elatic-job的任務都是Quartz的Job的實現,但做了更多的細分擴展:
簡單任務:
AbstractSimpleElasticJob
類似Quartz的Job,在Elastic-job的意義則多了高可用。流式任務:
AbstractDataFlowElasticJob
類似TBSchedule的任務,又再次基礎細分重視順序的AbstractSequenceDataFlowElasticJob和重視性能的AbstractThroughputDataFlowElasticJob。用戶擴展任務
elatic-job是向著插件化看齊的,希望用戶以插件形式貢獻代碼,編寫更多有用的任務。
一些亮點###
Sharding Offset
框架提供了記錄當前處理位移的方式,這往往用于大批量的任務處理中機器掛掉,這時候別的機器接手了掛掉的機器的任務時,需要知道哪些任務處理過了哪些還沒處理。在TBSchedule中需要自己在自己的系統中做持久化標記,而在Elatic-job中則可以使用Sharding Offset,這為failover提供了便利。Misfire開關
本次作業開啟后上次作業因為某種原因還沒有結束,框架把這次作業標記為Misfire,上次作業執行完后會彌補標記了Misfire的作業。
Quartz中原本也有Misfire,但在分布式環境中使用Misfire需要另外的支持,Elatic-job引入了它。
Elastic-job是2015年當當網發布的開源項目,它出現的意義是對TBSchedule在各方面的優化,這體現在它借鑒了TBSchedule的流式任務概念,但基本的調度功能還是交給這方面的資深專家:Quartz,而對zookeeper的操作使用crutor封裝,以及文檔比較全面,這一點對于維護者來說是心頭好。
唯一的缺點是太新,缺少線上環境的考驗。但當當的開發者在推廣方面很給力,贊一個。
總結##
本文從淺至深的介紹了任務調度技術,但沒有使用說明和結構詳解,因為本文旨在對比的基礎上做原理介紹,可以在技術選型上給出參考。
參考資料##
https://www.ibm.com/developerworks/cn/java/j-lo-taskschedule/
http://tech.meituan.com/mt-crm-quartz.html
http://www.cnblogs.com/davidwang456/p/4205237.html
http://code.taobao.org/p/tbschedule/wiki/index/
https://github.com/dangdangdotcom/elastic-job