關于任務定時調度


Tags:定時作業(yè)調度 分布式定時任務調度 Quartz TBSchedule Elastic-job


基于給定時間點,給定時間間隔或者給定執(zhí)行次數自動執(zhí)行任務。

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是非常實用的類,做一些常規(guī)的簡單任務,如在線程池中用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) {
            //執(zhí)行線程池大小
            ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

            long initialDelay1 = 1;
            long period1 = 1;
            // 從現在開始1秒鐘之后,每隔1秒鐘執(zhí)行一次job1
            service.scheduleAtFixedRate(
                    new ScheduledExecutorTest("job1"), initialDelay1,
                    period1, TimeUnit.SECONDS);

            long initialDelay2 = 1;
            long delay2 = 1;
            // 從現在開始2秒鐘之后,每隔2秒鐘執(zhí)行一次job2
            service.scheduleWithFixedDelay(
                    new ScheduledExecutorTest("job2"), initialDelay2,
                    delay2, TimeUnit.SECONDS);
        }
    }

特點:in JDK 多線程 線程池

Unix Crontab

相比較Timer這種固定間隔調度,crontab的可以使用cron表達式表達更復雜調度策略:

每1分鐘執(zhí)行一次myCommand
* * * * * myCommand

實例2:每小時的第3和第15分鐘執(zhí)行
3,15 * * * * myCommand

實例3:在上午8點到11點的第3和第15分鐘執(zhí)行
3,15 8-11 * * * myCommand

實例4:每隔兩天的上午8點到11點的第3和第15分鐘執(zhí)行
3,15 8-11 */2  *  * myCommand

crontab往往和腳本搭配完成更復雜的任務,意味著當需要和主系統(tǒng)進行復雜交互時多有不便。

特點: Linux內置 Crontab表達式

Quartz##

Quartz是個開源JAVA庫,可以簡單看做以上三種的結合的擴展。

Quartz組件圖

Scheduler:調度容器
Job:Job接口類
JobDetail :Job的描述類,job執(zhí)行時的依據此對象的信息反射實例化出Job的具體執(zhí)行對象。
Trigger:存放Job執(zhí)行的時間策略
JobStore: 存儲作業(yè)和調度期間的狀態(tài)
Calendar:指定排除的時間點(如排除法定節(jié)假日)

Quartz線程圖

Quartz的主要線程有兩類,負責調度的線程和負責Misfire(指錯過了執(zhí)行時間的作業(yè))的線程,其中負責調度的線程RegularSchedulerThread是基于線程池的,而Misfire只有一個線程。 兩類線程都會訪問抽象為JobStore的層來獲取作業(yè)策略或寫入調度狀態(tài)。
JobStore也分持久化(JobStoreSupport)和非持久化(RAMJobStore)兩種,使用場景大大不同,后面有敘述。

Quartz的觸發(fā)

注意上圖左邊部分是調度器的守護線程QuartzScheduleThread的主要流程,也就是:QuartzScheduleThread會在RegularThread池有空閑時(否則block),從JobStore中取出N個(將來30秒內要觸發(fā)的)Trigger,并交給RegularThread線程池來運行job。

Quartz的功能非常豐富,結構也比上述的復雜的多,本文只是簡要介紹抽象層的概念,詳解請參考更多資料。
對于單機調度Quartz基本能完全滿足我們的需求,但多個機器怎么辦呢?

Quartz集群##

為了分擔單點壓力,往往需要多個節(jié)點運行定時任務,他們之間有協作又不能沖突。

Quartz集群部署圖

Quartz用了一個比較取巧的方式支持集群定時調度。

Quartz使用持久化的線程模型

首先,JobStore要選用數據庫持久化存儲:JDBCJobStore,且自己管理事務:JobStoreTX。

依附于本身的trigger存取策略,Quartz利用數據庫行級鎖來實現多節(jié)點的通訊(間接通訊)。

0.調度器線程run()

1.獲取待觸發(fā)trigger

    1.1數據庫LOCKS表TRIGGER_ACCESS行加鎖

    1.2讀取JobDetail信息

    1.3讀取trigger表中觸發(fā)器信息并標記為"已獲取"

    1.4commit事務,釋放鎖

2.觸發(fā)trigger

    2.1數據庫LOCKS表STATE_ACCESS行加鎖

    2.2確認trigger的狀態(tài)

    2.3讀取trigger的JobDetail信息

    2.4讀取trigger的Calendar信息

    2.3更新trigger信息

    2.3commit事務,釋放鎖

3實例化并執(zhí)行Job

    3.1從線程池獲取線程執(zhí)行JobRunShell的run方法
    

讀取之前獲取鎖,寫入之后釋放鎖,這是Quartz集群解決集群同步的核心思想。

Quartz集群是用工具拼湊起來的一個方案,巧妙的運用了數據庫鎖解決同步問題,這在一些場景中是非常work的,但問題也依舊明顯:
解決了節(jié)點同步問題,但沒有解決分布式問題。

官方也做出說明,集群特性對于高cpu使用率的任務效果很好,但是對于大量的短任務,各個節(jié)點都會搶占數據庫鎖,這樣就出現大量的線程等待資源.這種情況隨著節(jié)點的增加會越來越嚴重.

有沒有解決分布式問題的方案?

TBSchedule##

TBSchedule部署圖

類比Quartz集群用數據庫做存儲,TBSchedule則使用更符合分布式場景的zookeeper來做任務狀態(tài)。

zookeeper有永久節(jié)點存儲作業(yè)的配置信息,使用臨時節(jié)點存儲調度時的狀態(tài),當其中一個調度端和zookeeper斷開鏈接時,回話消失臨時節(jié)點數據被抹除,所有在線調度端會感知到改變化并做出相應的動作。

來看幾個重要概念:

  • 任務項
    即分片。分布式機制是通過分片實現:
    如: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節(jié)點執(zhí)行,leader是最早上線的節(jié)點(編號最?。?/p>

  • 節(jié)點感知
    調度端會啟動一個刷新zookeeper的timer,如果有變動則回觸發(fā)leader的重新分配資源,
    如:
    新上線或下線了機器,會給各個調度端重新分配TaskItem。
    暫?;蛑匦聠幽硞€策略,調度端會停止之前的負責這個策略的線程組。

  • 觸發(fā)

TBScheduler依舊支持Crontab表達式,并進一步支持執(zhí)行的時間段(超過時間段則暫停),

但其內里實現有異于Quartz:

對于一個策略,在首次啟動時會計算出該策略的下次執(zhí)行開始時間和執(zhí)行結束,然后分別啟動一個負責啟動和暫停的Timer,Timer內的操作就是對調度器的暫停和恢復,以及下一批Timer的創(chuàng)建。

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中執(zhí)行,當execute時task為空時會再次selectTasks,
一次調度中,selectTasks可能會被調用多次,直到返回空,結束本次調度。

TBSchedule的出現最大的進步之處在于從關注作業(yè)到關注數據。在此概念上造就了高性能,也真正解決了集群分布式問題。

缺點:

  1. 對zookeeper的操作都是原生客戶端的直接操作,維護起來易出錯外,zookeeper的高可用也沒有良好支持。zookeeper掛掉要重啟所有調度端。
  2. 文檔缺失,四年內沒有任何更新(2016),缺少開源社區(qū)的維護。

Elastic-job##

Elatic-job部署圖

原理基本和TBSchedule一致。

一些重要概念:

  • leader選舉
    調度端機器上線后會檢查有沒有l(wèi)eader,如果沒有則提議自己做leader,兩個同時上線引發(fā)沖突是由zookeeper的內部解決的,總之它可以保證只有一個主。
    leader如果下線會觸發(fā)重新選舉,在選出下個leader前所有任務會被阻塞。

  • 分片
    leader選舉后,leader以『協調者』角色負責分片,同時依賴zookeeper的臨時節(jié)點和監(jiān)聽器的主動檢查和通知功能,對機器上、下線、任務配置更改、分片修改等事件做出響應。

任務的設計###

因為借助Quartz做實際調度工作,所以Elatic-job的任務都是Quartz的Job的實現,但做了更多的細分擴展:

  1. 簡單任務:
    AbstractSimpleElasticJob
    類似Quartz的Job,在Elastic-job的意義則多了高可用。

  2. 流式任務:
    AbstractDataFlowElasticJob
    類似TBSchedule的任務,又再次基礎細分重視順序的AbstractSequenceDataFlowElasticJob和重視性能的AbstractThroughputDataFlowElasticJob。

  3. 用戶擴展任務
    elatic-job是向著插件化看齊的,希望用戶以插件形式貢獻代碼,編寫更多有用的任務。

一些亮點###

  1. Sharding Offset
    框架提供了記錄當前處理位移的方式,這往往用于大批量的任務處理中機器掛掉,這時候別的機器接手了掛掉的機器的任務時,需要知道哪些任務處理過了哪些還沒處理。在TBSchedule中需要自己在自己的系統(tǒng)中做持久化標記,而在Elatic-job中則可以使用Sharding Offset,這為failover提供了便利。

  2. Misfire開關
    本次作業(yè)開啟后上次作業(yè)因為某種原因還沒有結束,框架把這次作業(yè)標記為Misfire,上次作業(yè)執(zhí)行完后會彌補標記了Misfire的作業(yè)。
    Quartz中原本也有Misfire,但在分布式環(huán)境中使用Misfire需要另外的支持,Elatic-job引入了它。

Elastic-job是2015年當當網發(fā)布的開源項目,它出現的意義是對TBSchedule在各方面的優(yōu)化,這體現在它借鑒了TBSchedule的流式任務概念,但基本的調度功能還是交給這方面的資深專家:Quartz,而對zookeeper的操作使用crutor封裝,以及文檔比較全面,這一點對于維護者來說是心頭好。

唯一的缺點是太新,缺少線上環(huán)境的考驗。但當當的開發(fā)者在推廣方面很給力,贊一個。

總結##

本文從淺至深的介紹了任務調度技術,但沒有使用說明和結構詳解,因為本文旨在對比的基礎上做原理介紹,可以在技術選型上給出參考。

參考資料##

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容