Quartz叢集原理及配置應用

振宇要低調發表於2015-08-25

1、Quartz任務排程的基本實現原理

  Quartz是OpenSymphony開源組織在任務排程領域的一個開源專案,完全基於Java實現。作為一個優秀的開源排程框架,Quartz具有以下特點:

    (1)強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求;

    (2)靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式;

    (3)分散式和叢集能力,Terracotta收購後在原來功能基礎上作了進一步提升。本文將對該部分相加闡述。

1.1 Quartz 核心元素

  Quartz任務排程的核心元素為:Scheduler——任務排程器、Trigger——觸發器、Job——任務。其中trigger和job是任務排程的後設資料,scheduler是實際執行排程的控制器。

  Trigger是用於定義排程時間的元素,即按照什麼時間規則去執行任務。Quartz中主要提供了四種型別的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。這四種trigger可以滿足企業應用中的絕大部分需求。

  Job用於表示被排程的任務。主要有兩種型別的job:無狀態的(stateless)和有狀態的(stateful)。對於同一個trigger來說,有狀態的job不能被並行執行,只有上一次觸發的任務被執行完之後,才能觸發下一次執行。Job主要有兩種屬性:volatility和durability,其中volatility表示任務是否被持久化到資料庫儲存,而durability表示在沒有trigger關聯的時候任務是否被保留。兩者都是在值為true的時候任務被持久化或保留。一個job可以被多個trigger關聯,但是一個trigger只能關聯一個job。

  Scheduler由scheduler工廠建立:DirectSchedulerFactory或者StdSchedulerFactory。第二種工廠StdSchedulerFactory使用較多,因為DirectSchedulerFactory使用起來不夠方便,需要作許多詳細的手工編碼設定。Scheduler主要有三種:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。

  Quartz核心元素之間的關係如圖1.1所示:

圖1.1 核心元素關係圖

1.2 Quartz 執行緒檢視

  在Quartz中,有兩類執行緒,Scheduler排程執行緒和任務執行執行緒,其中任務執行執行緒通常使用一個執行緒池維護一組執行緒。

圖1.2 Quartz執行緒檢視

  Scheduler排程執行緒主要有兩個:執行常規排程的執行緒,和執行misfiredtrigger的執行緒。常規排程執行緒輪詢儲存的所有trigger,如果有需要觸發的trigger,即到達了下一次觸發的時間,則從任務執行執行緒池獲取一個空閒執行緒,執行與該trigger關聯的任務。Misfire執行緒是掃描所有的trigger,檢視是否有misfiredtrigger,如果有的話根據misfire的策略分別處理(fire now OR wait for the next fire)。

1.3 Quartz Job資料儲存

  Quartz中的trigger和job需要儲存下來才能被使用。Quartz中有兩種儲存方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是將trigger和job儲存在記憶體中,而JobStoreSupport是基於jdbc將trigger和job儲存到資料庫中。RAMJobStore的存取速度非常快,但是由於其在系統被停止後所有的資料都會丟失,所以在叢集應用中,必須使用JobStoreSupport。

2、Quartz叢集原理

2.1 Quartz 叢集架構

  一個Quartz叢集中的每個節點是一個獨立的Quartz應用,它又管理著其他的節點。這就意味著你必須對每個節點分別啟動或停止。Quartz叢集中,獨立的Quartz節點並不與另一其的節點或是管理節點通訊,而是通過相同的資料庫表來感知到另一Quartz應用的,如圖2.1所示。

圖2.1 Quartz叢集架構

2.2 Quartz叢集相關資料庫表

  因為Quartz叢集依賴於資料庫,所以必須首先建立Quartz資料庫表,Quartz釋出包中包括了所有被支援的資料庫平臺的SQL指令碼。這些SQL指令碼存放於<quartz_home>/docs/dbTables 目錄下。這裡採用的Quartz 1.8.4版本,總共12張表,不同版本,表個數可能不同。資料庫為mysql,用tables_mysql.sql建立資料庫表。全部表如圖2.2所示,對這些表的簡要介紹如圖2.3所示。

 

圖2.2 Quartz 1.8.4在mysql資料庫中生成的表

圖2.3 Quartz資料表簡介

2.2.1 排程器狀態表(QRTZ_SCHEDULER_STATE)

  說明:叢集中節點例項資訊,Quartz定時讀取該表的資訊判斷叢集中每個例項的當前狀態。

  instance_name配置檔案中org.quartz.scheduler.instanceId配置的名字,如果設定為AUTO,quartz會根據物理機名和當前時間產生一個名字。

  last_checkin_time上次檢入時間

  checkin_interval檢入間隔時間

2.2.2 觸發器與任務關聯表(qrtz_fired_triggers)

  儲存與已觸發的Trigger相關的狀態資訊,以及相聯Job的執行資訊。

2.2.3 觸發器資訊表(qrtz_triggers)

  trigger_nametrigger的名字,該名字使用者自己可以隨意定製,無強行要求

  trigger_grouptrigger所屬組的名字,該名字使用者自己隨意定製,無強行要求

  job_nameqrtz_job_details表job_name的外來鍵

  job_groupqrtz_job_details表job_group的外來鍵

  trigger_state當前trigger狀態設定為ACQUIRED,如果設為WAITING,則job不會觸發

  trigger_cron觸發器型別,使用cron表示式

2.2.4 任務詳細資訊表(qrtz_job_details)

  說明:儲存job詳細資訊,該表需要使用者根據實際情況初始化

  job_name叢集中job的名字,該名字使用者自己可以隨意定製,無強行要求。

  job_group叢集中job的所屬組的名字,該名字使用者自己隨意定製,無強行要求。

  job_class_name叢集中job實現類的完全包名,quartz就是根據這個路徑到classpath找到該job類的。

  is_durable是否持久化,把該屬性設定為1,quartz會把job持久化到資料庫中

  job_data一個blob欄位,存放持久化job物件。

2.2.5許可權資訊表(qrtz_locks)

  說明:tables_oracle.sql裡有相應的dml初始化,如圖2.4所示。

圖2.4 Quartz許可權資訊表中的初始化資訊

2.3 Quartz Scheduler在叢集中的啟動流程

  Quartz Scheduler自身是察覺不到被叢集的,只有配置給Scheduler的JDBC JobStore才知道。當Quartz Scheduler啟動時,它呼叫JobStore的schedulerStarted()方法,它告訴JobStore Scheduler已經啟動了。schedulerStarted() 方法是在JobStoreSupport類中實現的。JobStoreSupport類會根據quartz.properties檔案中的設定來確定Scheduler例項是否參與到叢集中。假如配置了叢集,一個新的ClusterManager類的例項就被建立、初始化並啟動。ClusterManager是在JobStoreSupport類中的一個內嵌類,繼承了java.lang.Thread,它會定期執行,並對Scheduler例項執行檢入的功能。Scheduler也要檢視是否有任何一個別的叢集節點失敗了。檢入操作執行週期在quartz.properties中配置。

2.4 偵測失敗的Scheduler節點

  當一個Scheduler例項執行檢入時,它會檢視是否有其他的Scheduler例項在到達他們所預期的時間還未檢入。這是通過檢查SCHEDULER_STATE表中Scheduler記錄在LAST_CHEDK_TIME列的值是否早於org.quartz.jobStore.clusterCheckinInterval來確定的。如果一個或多個節點到了預定時間還沒有檢入,那麼執行中的Scheduler就假定它(們) 失敗了。

2.5 從故障例項中恢復Job

  當一個Sheduler例項在執行某個Job時失敗了,有可能由另一正常工作的Scheduler例項接過這個Job重新執行。要實現這種行為,配置給JobDetail物件的Job可恢復屬性必須設定為true(job.setRequestsRecovery(true))。如果可恢復屬性被設定為false(預設為false),當某個Scheduler在執行該job失敗時,它將不會重新執行;而是由另一個Scheduler例項在下一次觸發時間觸發。Scheduler例項出現故障後多快能被偵測到取決於每個Scheduler的檢入間隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。

3、Quartz叢集例項(Quartz+Spring)

3.1 Spring不相容Quartz問題

  Spring從2.0.2開始便不再支援Quartz。具體表現在 Quartz+Spring 把 Quartz 的 Task 例項化進入資料庫時,會產生: Serializable 的錯誤:

<bean id="jobtask" class="org.springframework.scheduling.quartz. MethodInvokingJobDetailFactoryBean ">
  <property name="targetObject">
    <ref bean="quartzJob"/>
  </property>
  <property name="targetMethod">
    <value>execute</value>
  </property>
</bean>

  這個 MethodInvokingJobDetailFactoryBean 類中的 methodInvoking 方法,是不支援序列化的,因此在把 QUARTZ 的 TASK 序列化進入資料庫時就會拋錯。

  首先解決MethodInvokingJobDetailFactoryBean的問題,在不修改Spring原始碼的情況下,可以避免使用這個類,直接呼叫JobDetail。但是使用JobDetail實現,需要自己實現MothodInvoking的邏輯,可以使用JobDetail的jobClass和JobDataAsMap屬性來自定義一個Factory(Manager)來實現同樣的目的。例如,本示例中新建了一個MyDetailQuartzJobBean來實現這個功能。

3.2 MyDetailQuartzJobBean.java檔案

package org.lxh.mvc.jobbean;

import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class MyDetailQuartzJobBean extends QuartzJobBean {
    protected final Log logger = LogFactory.getLog(getClass());
    private String targetObject;
    private String targetMethod;
    private ApplicationContext ctx;
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
            logger.info("execute [" + targetObject + "] at once>>>>>>");
            Object otargetObject = ctx.getBean(targetObject);
            Method m = null;
            try {
                m = otargetObject.getClass().getMethod(targetMethod, new Class[] {});
                m.invoke(otargetObject, new Object[] {});
            } catch (SecurityException e) {
                logger.error(e);
            } catch (NoSuchMethodException e) {
                logger.error(e);
            }
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext){
        this.ctx=applicationContext;
    }

    public void setTargetObject(String targetObject) {
        this.targetObject = targetObject;
    }

    public void setTargetMethod(String targetMethod) {
        this.targetMethod = targetMethod;
    }

}

 

3.3真正的Job實現類

  在Test類中,只是簡單實現了列印系統當前時間的功能。

package org.lxh.mvc.job;
import java.io.Serializable;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Test implements Serializable{
    private Log logger = LogFactory.getLog(Test.class);
    private static final long serialVersionUID = -2073310586499744415L;  
    public void execute () {
        Date date=new Date();  
        System.out.println(date.toLocaleString());  
    }
    
}

3.4 配置quartz.xml檔案

<bean id="Test" class="org.lxh.mvc.job.Test" scope="prototype">
    </bean>

    <bean id="TestjobTask" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass">
            <value>org.lxh.mvc.jobbean.MyDetailQuartzJobBean</value>
        </property>
        <property name="jobDataAsMap">
            <map>
                <entry key="targetObject" value="Test" />
                <entry key="targetMethod" value="execute" />
             </map>
         </property> 
     </bean>
    
    <bean name="TestTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">  
        <property name="jobDetail" ref="TestjobTask" />
        <property name="cronExpression" value="0/1 * * * * ?" />
    </bean> 
    
<bean id="quartzScheduler"
    class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="configLocation" value="classpath:quartz.properties"/>
        <property name="triggers">
            <list>
                <ref bean="TestTrigger" />
            </list>
        </property>
        <property name="applicationContextSchedulerContextKey" value="applicationContext" />
    </bean>        

3.5 測試

  ServerA、ServerB的程式碼、配置完全一樣,先啟動ServerA,後啟動ServerB,當Server關斷之後,ServerB會監測到其關閉,並將ServerA上正在執行的Job接管,繼續執行。

4、Quartz叢集例項(單獨Quartz)

  儘管我們已經實現了Spring+Quartz的叢集配置,但是因為Spring與Quartz之間的相容問題還是不建議使用該方式。在本小節中,我們實現了單獨用Quartz配置的叢集,相對Spring+Quartz的方式來說,簡單、穩定。

4.1 工程結構

  我們採用單獨使用Quartz來實現其叢集功能,程式碼結構及所需的第三方jar包如圖3.1所示。其中,Mysql版本:5.1.52,Mysql驅動版本:mysql-connector-java-5.1.5-bin.jar(針對於5.1.52,建議採用該版本驅動,因為Quartz存在BUG使得其與某些Mysql驅動結合時不能正常執行)。

 

圖4.1 Quartz叢集工程結構及所需第三方jar包

  其中quartz.properties為Quartz配置檔案,放在src目錄下,若無該檔案,Quartz將自動載入jar包中的quartz.properties檔案;SimpleRecoveryJob.java、SimpleRecoveryStatefulJob.java為兩個Job;ClusterExample.java中編寫了排程資訊、觸發機制及相應的測試main函式。

4.2 配置檔案quartz.properties

  預設檔名稱quartz.properties,通過設定"org.quartz.jobStore.isClustered"屬性為"true"來啟用叢集特性。在叢集中的每一個例項都必須有一個唯一的"instance id" ("org.quartz.scheduler.instanceId" 屬性), 但是應該有相同的"scheduler instance name" ("org.quartz.scheduler.instanceName"),也就是說叢集中的每一個例項都必須使用相同的quartz.properties 配置檔案。除了以下幾種例外,配置檔案的內容其他都必須相同:

  a.執行緒池大小。

  b.不同的"org.quartz.scheduler.instanceId"屬性值(通過設定為"AUTO"即可)。

#==============================================================  
#Configure Main Scheduler Properties  
#==============================================================   
org.quartz.scheduler.instanceName = quartzScheduler
org.quartz.scheduler.instanceId = AUTO

#==============================================================  
#Configure JobStore  
#============================================================== 
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 10000  
org.quartz.jobStore.dataSource = myDS
 
#==============================================================  
#Configure DataSource  
#============================================================== 
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = 123456
org.quartz.dataSource.myDS.maxConnections = 30

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

4.3 ClusterExample.java檔案

package cluster;
import java.util.Date;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
public class ClusterExample {
   
    public void cleanUp(Scheduler inScheduler) throws Exception {
        System.out.println("***** Deleting existing jobs/triggers *****");
        // unschedule jobs
        String[] groups = inScheduler.getTriggerGroupNames();
        for (int i = 0; i < groups.length; i++) {
            String[] names = inScheduler.getTriggerNames(groups[i]);
            for (int j = 0; j < names.length; j++) {
                inScheduler.unscheduleJob(names[j], groups[i]);
            }
        }
        // delete jobs
        groups = inScheduler.getJobGroupNames();
        for (int i = 0; i < groups.length; i++) {
            String[] names = inScheduler.getJobNames(groups[i]);
            for (int j = 0; j < names.length; j++) {
                inScheduler.deleteJob(names[j], groups[i]);
            }
        }
    }
       
    public void run(boolean inClearJobs, boolean inScheduleJobs) 
        throws Exception {
        // First we must get a reference to a scheduler
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler sched = sf.getScheduler();
       
        if (inClearJobs) {
            cleanUp(sched);
        }
        System.out.println("------- Initialization Complete -----------");
        if (inScheduleJobs) {
            System.out.println("------- Scheduling Jobs ------------------");
            String schedId = sched.getSchedulerInstanceId();
            int count = 1;
            JobDetail job = new JobDetail("job_" + count, schedId, SimpleRecoveryJob.class);
            // ask scheduler to re-execute this job if it was in progress when
            // the scheduler went down...
            job.setRequestsRecovery(true);
            SimpleTrigger trigger = 
                new SimpleTrigger("triger_" + count, schedId, 200, 1000L);
            trigger.setStartTime(new Date(System.currentTimeMillis() + 1000L));
            System.out.println(job.getFullName() +
                    " will run at: " + trigger.getNextFireTime() +  
                    " and repeat: " + trigger.getRepeatCount() + 
                    " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
            sched.scheduleJob(job, trigger);
            count++;
            job = new JobDetail("job_" + count, schedId, 
                    SimpleRecoveryStatefulJob.class);
            // ask scheduler to re-execute this job if it was in progress when
            // the scheduler went down...
            job.setRequestsRecovery(false);
            trigger = new SimpleTrigger("trig_" + count, schedId, 100, 2000L);
            trigger.setStartTime(new Date(System.currentTimeMillis() + 2000L));
            System.out.println(job.getFullName() +
                    " will run at: " + trigger.getNextFireTime() +  
                    " and repeat: " + trigger.getRepeatCount() + 
                    " times, every " + trigger.getRepeatInterval() / 1000 + " seconds");
            sched.scheduleJob(job, trigger);
        }
        // jobs don't start firing until start() has been called...
        System.out.println("------- Starting Scheduler ---------------");
        sched.start();
        System.out.println("------- Started Scheduler ----------------");
        System.out.println("------- Waiting for one hour... ----------");
        try {
            Thread.sleep(3600L * 1000L);
        } catch (Exception e) {
        }
        System.out.println("------- Shutting Down --------------------");
        sched.shutdown();
        System.out.println("------- Shutdown Complete ----------------");
        }
   
    public static void main(String[] args) throws Exception {
        boolean clearJobs = true;
        boolean scheduleJobs = true;
        for (int i = 0; i < args.length; i++) {
            if (args[i].equalsIgnoreCase("clearJobs")) {
                clearJobs = true;                
            } else if (args[i].equalsIgnoreCase("dontScheduleJobs")) {
                scheduleJobs = false;
            }
        }
        ClusterExample example = new ClusterExample();
        example.run(clearJobs, scheduleJobs);
    }
}

4.4 SimpleRecoveryJob.java

package cluster;

import java.io.Serializable;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
//如果有想反覆執行的動作,作業,任務就把相關的程式碼寫在execute這個方法裡,前提:實現Job這個介面
//至於SimpleJob這個類什麼時候例項化,execute這個方法何時被呼叫,我們不用關注,交給Quartz
public class SimpleRecoveryJob implements Job, Serializable {
    private static Log _log = LogFactory.getLog(SimpleRecoveryJob.class);
    public SimpleRecoveryJob() {
    }
    public void execute(JobExecutionContext context)
        throws JobExecutionException {
     //這個作業只是簡單的列印出作業名字和此作業執行的時間
        String jobName = context.getJobDetail().getFullName();
        System.out.println("JOB 1111111111111111111 SimpleRecoveryJob says: " + jobName + " executing at " + new Date());
    }
}

4.5 執行結果

  Server A與Server B中的配置和程式碼完全一樣。執行方法:執行任意主機上的ClusterExample.java,將任務加入排程,觀察執行結果:

  執行ServerA,結果如圖4.2所示。

圖4.2 ServerA執行結果1

  

  開啟ServerB後,ServerA與ServerB的輸出如圖4.3、4.4所示。

圖4.3 ServerA執行結果2

 

圖4.4 ServerB執行結果1

  從圖4.3、4.4可以看出,ServerB開啟後,系統自動實現了負責均衡,ServerB接手Job1。關斷ServerA後,ServerB的執行結果如圖4.5所示。

圖4.5 ServerB執行結果2

  從圖4.5中可以看出,ServerB可以檢測出ServerA丟失,將其負責的任務Job2接手,並將ServerA丟失到Server檢測出這段異常時間中需要執行的Job2重新執行了。

5、注意事項

5.1 時間同步問題

  Quartz實際並不關心你是在相同還是不同的機器上執行節點。當叢集放置在不同的機器上時,稱之為水平叢集。節點跑在同一臺機器上時,稱之為垂直叢集。對於垂直叢集,存在著單點故障的問題。這對高可用性的應用來說是無法接受的,因為一旦機器崩潰了,所有的節點也就被終止了。對於水平叢集,存在著時間同步問題。

  節點用時間戳來通知其他例項它自己的最後檢入時間。假如節點的時鐘被設定為將來的時間,那麼執行中的Scheduler將再也意識不到那個結點已經宕掉了。另一方面,如果某個節點的時鐘被設定為過去的時間,也許另一節點就會認定那個節點已宕掉並試圖接過它的Job重執行。最簡單的同步計算機時鐘的方式是使用某一個Internet時間伺服器(Internet Time Server ITS)。

5.2 節點爭搶Job問題

  因為Quartz使用了一個隨機的負載均衡演算法, Job以隨機的方式由不同的例項執行。Quartz官網上提到當前,還不存在一個方法來指派(釘住) 一個 Job 到叢集中特定的節點。

5.3 從叢集獲取Job列表問題

  當前,如果不直接進到資料庫查詢的話,還沒有一個簡單的方式來得到叢集中所有正在執行的Job列表。請求一個Scheduler例項,將只能得到在那個例項上正執行Job的列表。Quartz官網建議可以通過寫一些訪問資料庫JDBC程式碼來從相應的表中獲取全部的Job資訊。

相關文章