基於quartz的雲調度中心實現

1、背景

做爲業務開發人員,會常常須要寫一個定時任務。目前,寫定時任務應用最普遍最成熟的方案是OpenSymphony開源組織在任務調度領域的一個開源項目quartz,好比要寫一個定時數據同步任務,在完成quartz的相關配置後,只須要寫一個jobBean_A就能基於quartz完成調度;若是要再開發一個任務,那麼再寫一個jobBean_B。html

這樣作的方式有一個地方不是很好,quartz調度與job業務耦合在一塊兒,當任務數量愈來愈多,甚至達到成千上萬個,對於項目自己的維護也是一個挺大的挑戰。這時,會考慮將quartz任務項目拆分出來,但這樣每個任務工程都須要依賴quartz,不方便統一調度管理。爲了能統一管理調度任務,又能將調度和job業務分離,咱們提出雲調度方案git

2、架構設計

雲調度中心ferrari的設計目標:github

  • 調度中心自己不執行任何業務代碼,只負責管理各個任務。這樣的好處是,調度中心與業務job解耦,方便調度中心自身的升級維護;
  • job無需關心調度中心的內部邏輯,只需關注自身的job業務邏輯,讓寫一個job像寫一個web action同樣方便。

基於上述初衷,咱們提出的雲調度中心設計方案,如圖: ferrariweb

總共分爲三層:調度控制層,調度接入層,業務層。spring

  1. 調度控制層,基於quartz實現,主要用於管理任務調度信息,如調度job的類名、方法名、方法入參、job地址、job執行時間等;
  2. 調度接入層,主要用於接收調度控制中心的調度指令(如執行、終止任務命令),並根據接收到的任務信息(類名、方法名、入參)進行反射調用相應的任務類;
  3. 業務層,這裏主要用於實現job邏輯;

這樣,要開發一個任務,基本不用關心調度控制層和接入層的邏輯,只需在業務層實現任務邏輯便可。任務開發完後,在調度控制中心新增一個調度任務信息,即可接收調度中心的調度。數據庫

3、實現方案

雲調度中心的實現是基於quartz,因此對quartz必須有個清楚的理解。apache

Quartz任務調度的核心元素是scheduler(調度器),trigger(觸發器,用於定義調度時間規則) 和 job(任務),其中 trigger 和 job 是任務調度的元數據,scheduler 是實際執行調度的控制器。quartz內部的調度原理能夠查看後面列舉的參考文檔,這裏具體講講使用quartz的幾個注意點。 ####3.1 線程池配置 quartz.properties裏的線程池配置:架構

#Configure ThreadPool
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 15
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

Quartz 中自帶了一個線程池的實現SimpleThreadPool,並經過threadCount來設置最大併發數,通常設置在10~50比較合適。 ####3.2 misfire策略 misfire,即錯過的,指原本應該被執行但實際沒有被執行的任務調度,通常來講引發misfire的狀況有如下4種:併發

  • 調度控制中心由於某些緣由被重啓。在系統關閉到從新啓動之間的一段時間裏,可能有些任務會被 misfire;
  • Trigger 被暫停(suspend)的一段時間裏,有些任務可能會被 misfire;
  • 線程池中全部線程都被佔用,致使任務沒法被觸發執行,形成 misfire;
  • 有狀態任務在下次觸發時間到達時,上次執行尚未結束(無狀態任務沒有這種狀況);

quartz對misfire的產生有個時間條件,超過這個設定的時間則認爲是misfire,配置以下:app

#Misfire
org.quartz.jobStore.misfireThreshold: 120000 #120秒
org.quartz.jobStore.maxMisfiresToHandleAtATime: 1

爲了處理 misfired job,Quartz 中爲 trigger 定義了處理策略,主要有下面兩種:

  1. MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:針對 misfired job 立刻執行一次;
  2. MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次觸發;

ferrari使用的是第2種策略:

CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();

####3.3 調度集羣設置 Quartz 中的 trigger 和 job 須要存儲下來才能被使用,有兩種存儲方式:內存和數據庫。若是將調度信息存儲在內存,那麼只要quartz應用重啓,這些信息就會丟失,因此在生產環境中,通常都用數據庫存儲的方式,配置以下:

#Configure JobStore for RAM
#org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
#for cluster
org.quartz.jobStore.tablePrefix = XXX_
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX 
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 20000

jobStore設置爲jdbcjobstore.JobStoreTX即表明數據庫存儲方式,因爲quartz集羣是經過數據表來實現併發鎖控制,因此須要設置集羣的節點檢查輪訓時間clusterCheckinInterval,這裏設置爲20s。 ####3.4 調度器配置 spring對quartz進行了整合,這裏採用基於spring的quartz配置,以下:

<!-- 默認 lazy-init="false"spring-context-support version: 3.2.14.RELEASE quartz version:2.2.2-->
 <bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
     <property name="dataSource" ref="dataSource" />
     <!-- 設置自動啓動 -->
     <property name="autoStartup" value="true" />
     <!--必須的,QuartzScheduler 延時啓動,應用啓動完後 QuartzScheduler 再啓動 -->
     <property name="startupDelay" value="20" />
     <!--須要overwrite已經存在的job,若是須要動態的修改已經存在的job,就須要設置爲true,不然會以數據庫中已經存在的爲準-->
     <property name="overwriteExistingJobs" value="true" />
     <property name="applicationContextSchedulerContextKey"  value="applicationContextKey" /> 
     <property name="configLocation" value="classpath:quartz.properties"/>
 </bean>

####3.5 quartz任務開發 基於雲調度中心ferrari的架構,在調度控制中心層,咱們只要開發一個quartzJobBean。在這個jobBean中,基於ferrari協議將須要執行的類名、方法名、入參、任務機器地址等信息封裝成一個request,而後發送請求(ferrari用的是http請求)到任務目標機器。

在業務層,任務機器接收到調度控制中心的指令,解析出任務信息,便交給任務執行線程池,任務執行線程經過反射調用目標任務類進行執行。做爲業務層,無需基於quartz作任何開發,只需開發一個普通的類(稱爲任務類),而後將任務信息配置到調度控制中心,即可實現調度。

4、ferrari接入說明

####4.1 maven依賴

<dependency>
    <groupId>com.dianping</groupId>
    <artifactId>ferrari-core</artifactId>
    <version>1.2.4</version>
</dependency>

####4.2 web.xml配置servlet入口

<servlet>
    <servlet-name>FerrariServlet</servlet-name>
    <servlet-class>com.cip.ferrari.core.FerrariDirectServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>FerrariServlet</servlet-name>
    <url-pattern>/xxx/*</url-pattern>
</servlet-mapping>

####4.3 開始寫你的任務類及方法,類名、方法、入參在新增任務時配置 ferrari新增任務界面: addjob ####4.4 雲調度中心日誌接入 因爲job任務不在調度中心執行,而是有另外的job服務機器執行,因此要看業務日誌代碼,必須登陸對應的業務機器。若是job不少,又散落在各個機器,那麼要查看job運行日誌,效率就會比較低。爲了方便日誌查看,ferrari提供了日誌接入方案,在log4j.xml中增長一個append配置:

<appender name="FERRARI" class="com.cip.ferrari.core.log.FerrariFileAppender">
    <param name="filePath" value="/data/applogs/xxx/"/>
    <param name="append" value="true"/>
    <param name="encoding" value="UTF-8"/>
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%-d{yyyy-MM-dd HH:mm:ss} [%c]-[%t]-[%M]-[%L]-[%p] %m%n"/>
    </layout>
</appender>

其中,filePath 是日誌文件夾路徑。只要將日誌輸出在這個appender上,就能在調度控制中心遠程查看業務執行的日誌,如圖所示: ferrari-log

雲調度中心ferrari的實現源碼請移步: https://github.com/tkyuan/ferrari 記得給star^_^ ####參考文檔:

  1. http://www.cnblogs.com/davidwang456/p/4205237.html
  2. https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/
  3. http://www.quartz-scheduler.org/
相關文章
相關標籤/搜索