当前位置: 首页 > 后端技术 > Java

Quartz-Cluster集群配置,failOver原理

时间:2023-04-02 01:17:40 Java

在使用JDBC-BasedJobStore的前提下,Quartz支持集群部署。每个Scheduler任务调度服务都可以作为集群中的一个节点。节点不相互通信。一般情况下,每个节点只知道自己,不知道其他节点的存在。每个节点都与同一个数据库通信。从而实现集群。集群部署完成后,作业可以被任意一个Scheduler节点调度。当任一节点发生故障或宕机时,该节点负责的任务将委托给其他正常节点接管。Quartz集群配置Quartz集群通过参数org.quartz.jobStore.isCluster进行配置。在Quartz初始化过程中,该参数被StdSchedulerFactory读取并赋值给JobStoreSupport的成员变量isClustered,从而使当前Scheduler成为Quartz集群的一个节点。有必要说一下StdSchedulerFactory读取org.quartz.jobStore.isCluster参数的方式,因为看源码的时候费了点功夫才找到对应的配置,最后在JobStoreSupport看到了:/***

*设置此实例是否是集群的一部分。*

*/@SuppressWarnings("UnusedDeclaration")/*反射调用*/publicvoidsetIsClustered(booleanisClustered){this.isClustered=isClustered;}受那个叫reflectively的评论启发发现,原来的StdSchedulerFactory是通过反射机制设置的:尝试{setBeanProps(js,tProps);}catch(Exceptione){initException=newSchedulerException("JobStoreclass'"+jsClass+"'道具无法配置。",e);抛出异常;}读取org.quartz.jobStore下的所有配置后,调用setBeanProps(js,tProps)通过反射机制为JobStore设置配置文件中的所有相关属性。这种方法至少比硬读一个一个的设置要优雅和灵活。只有集群配置好并启用后,Scheduler启动后才会调用JobStoreSupport的schedulerStarted方法启动ClusterManager线程。ClusterManager线程ClusterManager负责集群节点的心跳检测和故障转移处理。通过设置参数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-BasedJobStore注册自己时,会有两个属性:instanceId:可以配置指定,也可以配置为Auto,Quartz会自动生成一个Id,每个节点必须有一个唯一的instanceIdsched_name:schedulername,不要求唯一,不同节点可以注册为同一个sche_name(从而实现集群)这里需要明确Quartz相关表中这两个字段对应的字段名,instanceId体现在qrtz_scheduler_state表,字段名是instance_name,sched_name体现在几乎所有的Quartz业务表中,比如qrtz_triggers,qrtz_job_details...,字段名是sched_name。在集群环境下,每个节点在注册job和trigger时,都会将当前节点的sched_name写入数据库。每个节点的调度线程在调度作业时,只调度当前节点的作业,即triggers表中sched_name等于当前节点sched_name的trigger。每个节点根据自己的逻辑调度和执行任务。没有中心节点和管理节点,也就没有主动的负载均衡机制。任何具有相同sched_name的节点都可以触发作业,数据库是所有节点之间的纽带。信息共享的唯一渠道。因此,Quartz工作在集群模式下有两个前提条件:一是启用集群参数,二是集群节点的sched_name相同。如果每个节点都配置了不同的sched_name,它们之间就达不到集群的效果!节点负载机制在Quartz集群环境中,Scheduler节点之间不相互通信,没有中心节点,所以Quartz集群没有负载均衡机制。那么Quatz集群环境中节点的负载是如何分布的呢?要了解Quartz集群环境中多个节点之间的负载机制,我们首先需要了解Quartz集群中的“节点”是如何工作的。由于Quartz调度器在单机和集群部署环境下的工作方式是一样的,其实我们在上一篇文章JDBC-BasedJobStore的“JobScheduling”部分已经详细分析了Job的调度过程。节点在集群环境下调度任务时,依靠“共享数据库”和“锁机制”来保证作业的正常调度。作业调度进程在获取Triggers之前先加锁。例如,acquireNextTriggers方法需要锁定qrtz_lock表的“TRIGGER_ACCESS”行。加锁后,其他节点要想获取Triggers,必须等待当前节点释放锁。在当前节点获取Trigger,执行job,执行中和执行后修改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中获取需要触发的trigger。条件主要有两个,一个是trigger的下一次触发时间(30秒以内),一个是state=WAITING,将满足条件的trigger的状态改为ACQUIRED,对得到的trigger进行二次判断待触发,如果确认触发(时间到了,状态保持ACQUIRED不变),则修改触发状态为EXECUTING。trigger写入qrtz_fired_triggers表后,job执行完成,根据trigger下次触发时间和执行结果更新triggers中的trigger状态(如果还需要触发,状态为WAITING),当前trigger从qrtz_fired_triggers表中删除了failOver机制,与上面的job调度过程密切相关:通过findFailedInstances方法获取已经丢失的节点,主要包括两部分:未被checkin的节点在qrtz_scheduler_state表中针对超过时间间隔,以及在qrtz_fired_triggers中存在但在qrtz_scheduler_state中不存在的节点(Quartz称之为孤儿节点)获取这些节点的所有qrtz_fired_triggers中的数据,因为我们知道如果job执行完成,trigger是要从qrtz_fired_triggers中删除,因为节点已经失去连接,那么qrtz_fired_triggers中的数据应该是节点的“未完成的事情”。将qrtz_fired_triggers中的数据一一处理。如果状态为BLOCKED/PAUSED_BLOCKED(获取到的状态由WAITING变为ACQUIRED后,还没来得及执行,被其他job阻塞使用),则释放当前trigger绑定的job相关的所有trigger的状态在触发器表中:PAUSED_BLOCKED->PAUSED,BLOCKED->WAITING如果status是ACQUIRED,说明当前trigger已经获取到,还没有执行完,节点就挂了,所以只要回复当前trigger,triggers表里的status是WAITING就OK了。否则,当前Trigger状态为EXECUTING,就是这种情况比较复杂,因为job已经开始执行了,节点挂了。我们很难知道它是在job执行完成后挂了,来不及更新status,在删除fired_triggers之前,还是job根本没有开始执行,或者这种情况下,Quartz给应用一个选项:设置job的shouldRecover属性,如果设置为true,将为当前trigger生成一个一次性的trigger任务,状态设置为WAITING,等待trigger被调度。如果当前Job设置了DisallowsConcurrentExecution,则释放其他被自身阻塞的触发器(PAUSED_BLOCKED->BLOCKED,BLOCKED->WAITING),并从qrtz_fired_triggers表中删除当前触发器。最后查看qrtz_triggers表中当前触发器的状态是否为COMPLETE,如果是,则从triggers表中删除当前触发器。如果当前trigger绑定的job只是当前trigger,同时从job_details表中删除jobfailOver完成处理。总结节点的checkIn和failOver流程:每个节点定期向数据库报告,检查是否有丢失节点,交接丢失节点的遗留工作。特别要注意的是,工作交接并没有具体说明由哪个节点接手,我也不自己接手。只要恢复状态就可以恢复触发状态。任何节点都可能接管JobStoreSupport#recoverJobs增加一个知识点:recoverJobs,它是JDBC-BasedJobStore的一个特性,指的是最重要的是恢复由于服务导致的错误触发器(比如状态不正常的触发器)停机或重启。recoverJobs在单机Scheduler启动后调用。集群环境的checkIn和failOver流程都包含这个逻辑,所以集群环境不需要处理。逻辑并不复杂:Triggers表中的BLOCKED和ACQUIRED状态恢复为WAITINGTriggers表中的PAUSED_BLOCKED状态,恢复为PAUSEDrecoverMisfiredJobs:调用Misfire逻辑处理错过触发时间的triggerqrtz_fired_triggers中遗留数据的处理table:如果绑定的job设置为IfRECOVERYisneeded,重新生成一个Trigger,写入Triggers表中。删除Triggers表中状态为COMPLETE的记录。清除qrtz_fired_triggers。总结Quartz中涉及的大部分知识点已经从源码的角度进行了分析。填补差距。多谢!上一篇Quartz-基于JDBC的JobStore事务管理和锁机制下一篇Runable和Callable有什么区别?Thread和FutureTask你得搞清楚!