基于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/
相关文章
相关标签/搜索