Quartz job使用过程中的发现问题与改进

Quartz job使用过程中的发现问题与改进

January 5, 2020

遇到的问题与原因分析

生产环境中遇到的问题:

  • 服务在启动过程中,不时地卡住,后来发生的越来越频繁。看日志分析,卡在初始化quartz job store的时候
  • 服务在运行过程中,不时地出现定时任务不被触发的情况

通过分析,得知问题产生的原因:

  • 目前代码中用的是Quartz scheduler 1.8.3版本,定时框架采用的是行锁,通过执行下面的SQL来锁住特定的记录。在分布式的系统的情况下,节点越多越容易发生锁等待,甚至死锁。Quartz scheduler 的 2.x 版本做了改进,在QRTZ_LOCKS表中多加了一个字段 SCHED_NAME。这有一个好处就是,可以给不同的应用分配不同的scheduler name,这样定时框架在调度的时候,不同的应用分别去尝试锁定不同的行,而不像之前锁的时同一个行,避免死锁的发生。
SELECT * FROM QRTZ_LOCKS WHERE LOCK_NAME = $1 FOR UPDATE;
  • org.quartz.threadPool.threadCount 值设置太小。Quartz scheduler的线程池大小不足,这个也有可能导致定时任务在排队,无法被触发。

另外,在研究现有代码过程中,还发现另外3个问题:

  • group未正确赋值。Quartz中实际上是把job name与group的组合作为一个唯一标识的key,用它来触发和调度一个任务。job name相同,group不同,被当做是不同的任务。如果同一个应用多个节点,需要分成不同的组来分别执行不同的定时任务(比如其中一个组仅统计某个时间段出国的旅游人数,另外一个组仅统计在国内的旅游人数),那就必须给不同的组赋予不同的group值。否则,可能导致其中的一个任务没有执行,因为他们被当做是同一个任务。
  • 应用中的instanceId被写死在配置文件中。由于一个应用往往有多个节点,这个就导致集群中有多个节点的instanceId是相同。然而同一个Quartz scheduler的集群中,任意节点的instanceId必须保证唯一。
  • 目前的系统有个需求,某个任务必须在某个服务的所有节点上执行,比如定时从数据库或者其他地方同步某些配置项到内存中。由于Quartz框架不支持这个功能,原先的代码对job detail等类进行扩展,添加了一个属性,用来表示该任务是在该应用的所有节点上或仅仅选择一台执行。为了支持这个功能,代码改动的比较多,甚至也有可能是因为这些改动,直接或者间接导致前面的那些问题。在网上搜索了一下,发现也有一些人有类似的需求,他们在某网站上面提问,使用Quartz框架时怎样才能支持任务在集群的所有节点上执行。后来仔细想想,Quartz框架支持集群模式与基于内存的RAMJobStore单机模式这两种,如果需要在所有节点上执行,其实使用基于内存的RAMJobStore单机模式就可以了,没有必要再通过扩展集群模式来支持这个功能。

解决方案

最终的解决方案,总结如下:

  • 升级所有应用的quartz scheduler库至2.3.1版本
  • 不同的应用,分别赋予不同的scheduler name;同一个应用中,如果同时有集群模式与单机模式,需要使用不同的scheduler,并赋予不同的name值
  • 对于同一个应用,某个任务仅需要任意一个节点执行的,使用JobStoreCMT的cluster模式。同时需要保证不同节点instanceId不同, 比如将org.quartz.scheduler.instanceId的值设置为AUTO
  • 对于同一个应用,某个任务需要所有节点都执行的,使用基于内存的RAMJobStore
  • org.quartz.threadPool.threadCount 设置成与任务数一致。避免因线程数不够,任务无法执行
  • job detail赋予正确的group名称
  • 如果job不允许并发,则在job对应的类上添加@DisallowConcurrentExecution 注解
  • 如果job对应的类上面有autowire的需求,可以自定义一个job factory,继承SpringBeanJobFactory,并调用AutowireCapableBeanFactory的autowireBean,来避免Job类里面@Autowired无法注入的问题。同时在配置文件中使用自定义的job factory
<property name="jobFactory" ref="myCustomizedJobFactory" />
最后更新于