Synchronizace scheduleru v clusteru

Na našem projektu Starthead jsme potřebovali implementovat automatické provádění úkolu určeného ke zpracovávání dat v databázi. Ve frameworku Spring, který je pro vývoj použit, je tato úloha jednoduše řešitelná například pomocí TaskScheduler. Zajímavější situace nastává, pokud má aplikace bežet v clusterovém řešení. To znamená, že je potřeba řešit synchronizaci, aby nedocházelo k vícenásobnému spouštění jobů ve stejný čas. Snažili jsme se najít co nejjednodužší řešení, které by splňovalo danou podmínku a tím se v našem případě ukázalo využití Quartz scheduleru s patřičnou clusterovou konfigurací a synchronizováním pomocí ukládání do databáze.
Zde je uveden jednoduchý příklad vytvoření takového úkolu a jeho konfigurace ve Spring frameworku.
Implementace jednoduchého jobu
package com.aspectworks; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.StatefulJob; import org.springframework.scheduling.quartz.QuartzJobBean; public class TestJob extends QuartzJobBean implements StatefulJob { private TestService testService; protected void executeInternal(JobExecutionContext context) throws JobExecutionException { //implementace úkolu } public void setTestService(TestService testService) { this.testService = testService; } }
Definice triggeru úkolu pomocí Cron definice
<bean id="testTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail"> <bean class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.aspectworks.TestJob" /> <property name="name" value="TestJob" /> </bean> </property> <property name="jobDataAsMap"> <map> <entry key="testAttribute" value="value " /> </map> </property> <property name="cronExpression" value="1 * * * * ?" /> </bean>
Konfigurace scheduleru pro clusterové řešení
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="waitForJobsToCompleteOnShutdown" value="true" /> <property name="dataSource" ref="dataSource" /> <property name="overwriteExistingJobs" value="true" /> <property name="autoStartup" value="true" /> <property name="triggers"> <list> <ref bean="testTrigger" /> </list> </property> <property name="quartzProperties"> <props> <prop key="org.quartz.scheduler.instanceName">TestScheduler</prop> <prop key="org.quartz.scheduler.instanceId">AUTO</prop> <prop key="org.quartz.jobStore.misfireThreshold">60000</prop> <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop> <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop> <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop> <prop key="org.quartz.jobStore.isClustered">true</prop> <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop> <prop key="org.quartz.threadPool.threadCount">25</prop> <prop key="org.quartz.threadPool.threadPriority">5</prop> </props> </property> <property name="schedulerContextAsMap"> <map> <entry key="testService" value-ref="testService" /> </map> </property> </bean>
V dané konfiguraci je důležité nastavení atributů obsahujících org.quartz.jobStore a org.quartz.impl.jdbcjobstore. Ty říkají, že úkoly poběží v clusteru, a že se má provést jejich ukládání do tabulek s prefixem QRTZ_. Toto nastavení nám samo zajistí, že po inicializaci se úkol uloží do databáze s příslušnými parametry a ve chvíli vykonávání úkolu smí pouze jeden stroj v clusteru provést jeho spuštění. Právo na vykonání úkolu dostane první z běžících strojů v clusteru, který o to požádá.
Vysvětlení nejdůležitějších atributů:
org.quartz.jobStore.isClustered – definování běhu úkolu v clusteru
org.quartz.scheduler.instanceId – nastavení typu určování id instance (defaultně nastaveno na NON_CLUSTERED)
org.quartz.jobStore.class – definování typu ukládání synchronizace úkolů (v našem případě definuje ukládání do databáze)
org.quartz.jobStore.driverDelegateClass – definování driveru pro ukládání dat do databáze (v našem případě MySQL). Sql soubor pro založení tabulek můžete najít v adresáři doc v konkrétní verzi quartz implementace.
org.quartz.jobStore.tablePrefix – nastavení prefixu Qartz tabulek pro ukládání synchronizace úkolů Pokud se použije ukládání úkolů do databáze, ukládá se společně s JobDetailem i mapa jeho atributů jobDataAsMap. To znamená, že musí být možné je serializovat a také, že by to neměly být žádné objekty s aplikační logikou. Pokud je potřeba v úkolu využít nějakou service spravovanou Springem, je lepší ji předat do úkolu přes schedulerContextAsMap definovanou v SchedulerFactoryBeanu. Tento způsob uloží objekty do contextu Scheduleru a do úkolů je předá až při jejich inicializování, tedy bez nutnosti ukládání do databáze s ostatnímy atributy. Více o Quartz schedulerech je možné najít na stránkách http://quartz-scheduler.org/overview
Iba kratke doplnenie, ze toto riesenie je velmi nachylne na presne zosynchronizovany cas na vsetkych nodoch, co nie je problem zabezpecit, len treba na to mysliet.