Quartz - 集群Cluster的配置、failOver原理

在采用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注册自己的时候,都会有两个属性:

1. instanceId:可以配置指定,也可以配置为Auto,由Quartz自动生成一个Id,每一个节点必须有唯一的instanceId

2. 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](https://segmentfault.com/a/1190000043453014) 中的“作业的调度”部分已经详细分析过作业的调度过程了。

集群环境下节点在调度任务的时候,靠的就是“共享的数据库”以及“锁机制”来确保作业的正常调度的。

作业调度进程在获取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的作业调度过程:

1. 以当前调度器sched_name获取triggers中需要被触发的触发器,有两个主要条件,一个是触发器的下次触发时间(在30秒内),另一个是状态=WAITING

2. 将满足条件的触发器状态修改为ACQUIRED

3. 对获取到的待触发的触发器做二次判断,如果确认触发(时间满足、状态保持ACQUIRED没变化),则修改触发器状态为EXECUTING

4. 触发器写入qrtz_fired_triggers表

5. 作业执行完成后,根据触发器的下次触发时间、以及执行结果更新triggers中的触发器状态(如果仍然需要被触发的话状态为WAITING),当前触发器从qrtz_fired_triggers表删除

***failOver机制和上述作业调度过程密切相关:***

1. 通过方法findFailedInstances获取已经失联的节点,主要包括两部分内容:超过时间间隔要求没有在qrtz_scheduler_state表进行checkIn的节点,以及在qrtz_fired_triggers中存在、但是在qrtz_scheduler_state不存在的节点(Quartz称之为孤儿节点)

2. 获取到这些节点的所有的qrtz_fired_triggers中的数据,因为我们知道如果作业执行完成的话,触发器是要从qrtz_fired_triggers中删除的,既然节点已经失联那么qrtz_fired_triggers中的数据应该就是该节点的“未尽事业”

3. 逐条处理qrtz_fired_triggers中的数据,如果状态是BLOCKED/PAUSED_BLOCKED(获取出来状态从WAITING变为ACQUIRED之后,还没来得及执行,被其他作业阻塞了),则释放当前trigger绑定的作业相关的所有触发器在triggers表中的状态:PAUSED_BLOCKED->PAUSED,BLOCKED->WAITING

4. 如果状态是ACQUIRED,说明当前触发器被获取到之后、还没有执行,节点就挂了,因此只要回复当前触发器再triggers表中的状态为WAITING就OK了

5. 否则,当前Trigger的状态就是EXECUTING,***这种情况比较复杂,因为作业已经开始执行了、节点挂了,我们很难知道他是在作业执行完成之后、没来得及更新状态、没来得及删除fired_triggers挂掉的,还是作业根本就没有开始执行、或者执行到一半挂掉了。***

6. ***这种情况下Quartz给应用一个选项:设置作业的shouldRecover属性,设置为true的话则为当前触发器再生成一个一次性触发任务,状态设置为WAITING等待触发器调度。***

7. 如果当前Job设置了DisallowsConcurrentExecution,则释放被自己BLOCK的其他触发器(PAUSED_BLOCKED->BLOCKED,BLOCKED->WAITING)

8. 从qrtz_fired_triggers表中删除当前trigger

9. 最后检查当前trigger在qrtz_triggers表中的状态是否是COMPLETE,如果是的话,从triggers表中删除当前trigger,如果当前trigger绑定的job只有当前trigger这一个trigger的话,同时从job_details表中删除该job

failOver就处理完了。

总结一下节点的checkIn和failOver过程:

1. ***每一个节点向数据库定时报到***

2. ***报到的同时检查是否有失联的节点***

3. ***对失联节点的遗留工作做移交处理***

4. ***需要特别注意的是,工作移交并不会指定哪一个节点接手,自己也不去接手,只是恢复触发器状态***

5. ***只要恢复状态其实就够了,任一节点都有可能接手***

#### JobStoreSupport#recoverJobs

补充一个知识点:recoverJobs,这是JDBC-Based JobStore的一个特性,指的是由于服务停机或重启导致错误的触发器(比如触发器的状态不正常)的恢复。

recoverJobs在单机Scheduler启动后调用,集群环境的checkIn和failOver过程包含了这一逻辑,所以集群环境不需要处理。

逻辑不复杂:

1. Triggers表中的状态BLOCKED、ACQUIRED恢复为WAITING

2. Triggers表中的状态PAUSED_BLOCKED恢复为PAUSED

3. recoverMisfiredJobs:调用Misfire逻辑,处理错过触发时间的触发器

4. qrtz_fired_triggers表中的遗留数据处理:如果绑定的job设置为需要RECOVERY,则重新生成一个Trigger写入Triggers表

5. 删除Triggers表中状态为COMPLETE的记录

6. 清空qrtz_fired_triggers

#### 小结

Quartz涉及到的大部分知识点都从源码角度分析过了,后面如果发现有什么遗漏的话,再查漏补缺。

Thanks a lot!

上一篇 [Quartz - JDBC-Based JobStore事务管理及锁机制](https://segmentfault.com/a/1190000043461939)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值