在采纳JDBC-Based JobStore 的前提下,Quartz反对集群部署。每一个Scheduler任务调度服务都能够作为集群中的一个节点,节点之间并不相互通信,失常状况下每一个节点只晓得本人、并不知道其余节点的存在,各节点都通过和同一个数据库通信从而实现集群。
集群部署后,作业能够被任意一个Scheduler节点调度,任一个节点失败或down机后,该节点负责的工作会委派给其余失常节点接管。
Quartz集群的配置
Quratz集群通过参数org.quartz.jobStore.isCluster配置,Quartz初始化的过程中参数被StdSchedulerFactory读取之后赋值给JobStoreSupport的成员变量isClustered,从而使以后Scheduler变成Quartz集群的一个节点。
StdSchedulerFactory读取org.quartz.jobStore.isCluster参数的形式有必要说一下,因为在读源码找相应配置的时候还是费了点周折的,最初还是在JobStoreSupport中看到了:
/** * <p> * Set whether this instance is part of a cluster. * </p> */ @SuppressWarnings("UnusedDeclaration") /* called reflectively */ public void setIsClustered(boolean isClustered) { this.isClustered = isClustered; }
受那句正文called reflectively启发才找到,原来StdSchedulerFactory是通过反射机制设置的:
tProps = cfg.getPropertyGroup(PROP_JOB_STORE_PREFIX, true, new String[] {PROP_JOB_STORE_LOCK_HANDLER_PREFIX}); try { setBeanProps(js, tProps); } catch (Exception e) { initException = new SchedulerException("JobStore class '" + jsClass + "' props could not be configured.", e); throw initException; }
读取到org.quartz.jobStore下的所有配置后,调用setBeanProps(js, tProps)通过反射机制为JobStore设置了配置文件中所有的相干属性。这种形式起码比一个个属性硬读进来设置要来的优雅多了,也灵便多了。
只有配置启用集群,Scheduler启动之后才会通过调用JobStoreSupport的schedulerStarted办法启动ClusterManager线程。
ClusterManager线程
ClusterManager负责进行集群节点的心跳检测、failover解决。
通过设置参数org.quartz.jobStore.isCluster=true启用集群后,Scheduler启动实现后会调用JobStoreSupport的schedulerStarted办法启动ClusterManager线程。
每一个节点都能够通过参数指定clusterCheckinInterval - 心跳检测周期,默认7500L毫秒。
节点以clusterCheckinInterval频率向数据库报到:更新qrtz_scheduler_state表以后节点的最初checkin工夫。
在checkin的同时会查看qrtz_scheduler_state表中超过约定工夫周期依然未向数据库“报到”的节点,标记为failover节点,交给failover解决环节。
集群节点的根底属性
咱们首先须要对“节点”做一个根底的理解。
Quartz集群环境下的节点其实就是调度器Scheduler,因为节点并不知道其余节点的存在,所以每一个节点不论是单机部署、还是集群部署,工作形式其实没有区别。单机和集群的区别在于:集群形式下会启动集群治理线程ClusterManager,单机不启动。
每一个节点在向JDBC-Based JobStore注册本人的时候,都会有两个属性:
- instanceId:能够配置指定,也能够配置为Auto,由Quartz主动生成一个Id,每一个节点必须有惟一的instanceId
- sched_name:调度器name,不要求惟一,不同节点能够注册为雷同的sche_name(从而实现集群)
这里须要明确一下这两个字段在Quartz相干表中对应的字段名,instanceId体现在qrtz_scheduler_state表中,字段名是instance_name,sched_name体现在qrtz_triggers、qrtz_job_details...等简直所有的Quartz业务表中,字段名sched_name。
集群环境下每一节点在注册job和trigger的时候,以以后节点的sched_name写入数据库中。
每一个节点的调度线程在调度作业的时候,只调度以后节点的作业,也就是triggers表中sched_name等于以后节点的sched_name的触发器。
每一个节点都依照本人的逻辑调度执行工作,不存在一个核心节点或者治理节点,因而也就不存在被动的负载平衡机制,作业能够被具备雷同sched_name的任一节点触发执行,数据库是所有节点之间惟一的信息共享渠道。
所以Quartz以集群形式工作的前提条件有两个:一个是开启集群参数,另一个是集群节点的sched_name雷同。如果每个节点都以不同的sched_name配置的话,他们之间是达不到集群的成果的!
节点的负载机制
Quartz集群环境下Scheduler节点之间并不通信,不存在核心节点,所以Quartz集群并没有load balance的机制。
那么Quratz集群环境下节点的负载是怎么调配的呢?
要理解Quartz集群环境下的多个节点之间的负载机制,咱们首先须要理解Quartz集群下的“节点”具体是怎么工作的。
因为Quartz调度器在单机和集群部署环境下的工作形式没有区别,所以咱们其实在后面的文章 JDBC-Based JobStore 中的“作业的调度”局部曾经详细分析过作业的调度过程了。
集群环境下节点在调度工作的时候,靠的就是“共享的数据库”以及“锁机制”来确保作业的失常调度的。
作业调度过程在获取Triggers前首先加锁,比方acquireNextTriggers办法须要对qrtz_lock表的“TRIGGER_ACCESS”行上锁,上锁之后其余节点如果要获取Triggers的话就必须期待以后节点开释锁。在以后节点获取Trigger、执行作业、执行过程中以及执行实现后批改Triggers状态、执行前插入以及执行后删除fired_triggers表,都是在锁定qrtz_lock表的状态下执行的。直到作业最初执行实现、所有数据库操作都完结之后,才会最终开释锁。
所以,咱们能够看到,在整个作业执行过程中,其余集群节点是没有机会参加的,包含Cluater_manager的failOver操作也被锁定在外、必须期待的。
Quartz的集群环境就是在这个“共享数据库”+锁机制这样一套机制下维持失常运行的。
只不过不同的操作须要不同的锁,作业调度过程可能会锁定qrtz_lock表的“TRIGGER_ACCESS”行,Cluster_manager线程的Checkin操作可能会锁定qrtz_lock表的“STATE_ACCESS”行,其余操作可能会锁表。具体获取什么样的锁是须要综合思考性能和平安问题的。
每一个节点就这样不辞劳苦去和数据库“抢活”,如果资源被锁定了就期待,否则如果能拿到锁就开始干活!
所以咱们能够说,不存在一个核心节点进行协调、调配负载的状况下,Quartz集群下的各节点靠着本人的“盲目”(其实是每个节点的负载)抢活干,谁抢到是谁的!
集群的failover解决
理解failOver机制之前,咱们须要再温习一遍Quartz的作业调度过程:
- 以以后调度器sched_name获取triggers中须要被触发的触发器,有两个次要条件,一个是触发器的下次触发工夫(在30秒内),另一个是状态=WAITING
- 将满足条件的触发器状态批改为ACQUIRED
- 对获取到的待触发的触发器做二次判断,如果确认触发(工夫满足、状态放弃ACQUIRED没变动),则批改触发器状态为EXECUTING
- 触发器写入qrtz_fired_triggers表
- 作业执行实现后,依据触发器的下次触发工夫、以及执行后果更新triggers中的触发器状态(如果依然须要被触发的话状态为WAITING),以后触发器从qrtz_fired_triggers表删除
failOver机制和上述作业调度过程密切相关:
- 通过办法findFailedInstances获取曾经失联的节点,次要包含两局部内容:超过工夫距离要求没有在qrtz_scheduler_state表进行checkIn的节点,以及在qrtz_fired_triggers中存在、然而在qrtz_scheduler_state不存在的节点(Quartz称之为孤儿节点)
- 获取到这些节点的所有的qrtz_fired_triggers中的数据,因为咱们晓得如果作业执行实现的话,触发器是要从qrtz_fired_triggers中删除的,既然节点曾经失联那么qrtz_fired_triggers中的数据应该就是该节点的“未尽事业”
- 逐条解决qrtz_fired_triggers中的数据,如果状态是BLOCKED/PAUSED_BLOCKED(获取进去状态从WAITING变为ACQUIRED之后,还没来得及执行,被其余作业阻塞了),则开释以后trigger绑定的作业相干的所有触发器在triggers表中的状态:PAUSED_BLOCKED->PAUSED,BLOCKED->WAITING
- 如果状态是ACQUIRED,阐明以后触发器被获取到之后、还没有执行,节点就挂了,因而只有回复以后触发器再triggers表中的状态为WAITING就OK了
- 否则,以后Trigger的状态就是EXECUTING,这种状况比较复杂,因为作业曾经开始执行了、节点挂了,咱们很难晓得他是在作业执行实现之后、没来得及更新状态、没来得及删除fired_triggers挂掉的,还是作业基本就没有开始执行、或者执行到一半挂掉了。
- 这种状况下Quartz给利用一个选项:设置作业的shouldRecover属性,设置为true的话则为以后触发器再生成一个一次性触发工作,状态设置为WAITING期待触发器调度。
- 如果以后Job设置了DisallowsConcurrentExecution,则开释被本人BLOCK的其余触发器(PAUSED_BLOCKED->BLOCKED,BLOCKED->WAITING)
- 从qrtz_fired_triggers表中删除以后trigger
- 最初查看以后trigger在qrtz_triggers表中的状态是否是COMPLETE,如果是的话,从triggers表中删除以后trigger,如果以后trigger绑定的job只有以后trigger这一个trigger的话,同时从job_details表中删除该job
failOver就解决完了。
总结一下节点的checkIn和failOver过程:
- 每一个节点向数据库定时报到
- 报到的同时查看是否有失联的节点
- 对失联节点的遗留工作做移交解决
- 须要特地留神的是,工作移交并不会指定哪一个节点接手,本人也不去接手,只是复原触发器状态
- 只有复原状态其实就够了,任一节点都有可能接手
JobStoreSupport#recoverJobs
补充一个知识点:recoverJobs,这是JDBC-Based JobStore的一个个性,指的是因为服务停机或重启导致谬误的触发器(比方触发器的状态不失常)的复原。
recoverJobs在单机Scheduler启动后调用,集群环境的checkIn和failOver过程蕴含了这一逻辑,所以集群环境不须要解决。
逻辑不简单:
- Triggers表中的状态BLOCKED、ACQUIRED复原为WAITING
- Triggers表中的状态PAUSED_BLOCKED复原为PAUSED
- recoverMisfiredJobs:调用Misfire逻辑,解决错过触发工夫的触发器
- qrtz_fired_triggers表中的遗留数据处理:如果绑定的job设置为须要RECOVERY,则从新生成一个Trigger写入Triggers表
- 删除Triggers表中状态为COMPLETE的记录
- 清空qrtz_fired_triggers
小结
Quartz波及到的大部分知识点都从源码角度剖析过了,前面如果发现有什么脱漏的话,再查漏补缺。
Thanks a lot!
上一篇 Quartz - JDBC-Based JobStore事务管理及锁机制