转载

每日一博 | 基于 quartz 的云调度中心实现

一、背景

作为业务开发人员,会经常需要写一个定时任务。目前,写定时任务应用最广泛最成熟的方案是OpenSymphony开源组织在任务调度领域的一个开源项目quartz,比如要写一个定时数据同步任务,在完成quartz的相关配置后,只需要写一个jobBean A就能基于quartz完成调度;如果要再开发一个任务,那么再写一个jobBean B。

这样做的方式有一个地方不是很好,quartz调度与job业务耦合在一起,当任务数量越来越多,甚至达到成千上万个,对于项目本身的维护也是一个挺大的挑战。这时,会考虑将quartz任务项目拆分出来,但这样每一个任务工程都需要依赖quartz,不方便统一调度管理。为了能 统一管理调度任务 ,又能将 调度和job业务分离 ,我们提出 云调度方案

二、架构设计

云调度中心ferrari的设计目标: - 调度中心本身不执行任何业务代码,只负责管理各个任务。这样的好处是,调度中心与业务job解耦,方便调度中心自身的升级维护; - job无需关心调度中心的内部逻辑,只需关注自身的job业务逻辑,让写一个job像写一个web action一样方便。

基于上述初衷,我们提出的云调度中心设计方案,如图: 每日一博 | 基于 quartz 的云调度中心实现

总共分为三层:调度控制层,调度接入层,业务层。 1. 调度控制层,基于quartz实现,主要用于管理任务调度信息,如调度job的类名、方法名、方法入参、job地址、job执行时间等; 2. 调度接入层,主要用于接收调度控制中心的调度指令(如执行、终止任务命令),并根据接收到的任务信息(类名、方法名、入参)进行反射调用相应的任务类; 3. 业务层,这里主要用于实现job逻辑;

这样,要开发一个任务,基本不用关心调度控制层和接入层的逻辑,只需在业务层实现任务逻辑即可。任务开发完后,在调度控制中心新增一个调度任务信息,便可接收调度中心的调度。

三、实现方案

云调度中心的实现是基于quartz,所以对quartz必须有个清楚的理解。

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,配置如下: ```

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做任何开发,只需开发一个普通的类(称为任务类),然后将任务信息配置到调度控制中心,便可实现调度。

四、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新增任务界面: 每日一博 | 基于 quartz 的云调度中心实现

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上,就能在调度控制中心远程查看业务执行的日志,如图所示: 每日一博 | 基于 quartz 的云调度中心实现

要详细了解云调度中心ferrari的实现源码,请关注本博客,后续开源会在这里同步更新,或请邮件联系本人zjytk05@163.com,谢谢!

参考文档:

  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/
原文  http://my.oschina.net/tkyuan/blog/678001?fromerr=KQ35E2wY
正文到此结束
Loading...