共计 5363 个字符,预计需要花费 14 分钟才能阅读完成。
在采纳 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 事务管理及锁机制