任务调度是特定业务场景下的定时任务处理,在分布式架构下,分布式调度框架的设计显得尤为重要。本文简要介绍了两种常用的分布式调度框架Quartz和xxl-job的特性、基本架构和参数配置,以加深了解。
1、分布式调度框架设计
定时任务是区别于联机业务的实时处理,需要在特定的时刻去处理特定的任务,比如金融核心账务系统每天定时账务批次处理、电商系统整点抢购、购票系统超时订单的定时回收处理、动账短信通知的定时发送等。
核心功能:定时调度、任务管理和可观测日志
高可用:集群和任务分片的高可用以及失败后的处理
高性能:分布式锁的实现
扩展功能:可视化运维、多语言支持和任务编排
1.1 分布式任务调度框架整体架构
分布式架构:分布式部署架构中各个节点之间可以无状态和无限的水平扩展,支持动态扩缩容和任务动态上下线
任务调度和执行:具体任务请求的分发和接收、任务的动态均衡分配、任务的执行和任务状态管理(失败或超时重试、断点重提)
配置监控中心:感知整个集群的状态并进行监控、任务信息注册登记、任务运行日志和任务进度监控
调度控制台:负责调度任务的可视化编排配置、任务流程和状态动态展示、任务运行的关键日志和报错信息展示、任务超时控制和失败重试次数配置
任务接入:将调度控制台的任务转化下发给调度器,并且向注册中心注册任务
调度器:接收接入下发的调度任务,进行任务拆分下发,在注册中心找执行器,然后任务下发到执行器执行,同时也注册到注册中心
执行器:接收调度任务,并且上报状态给注册中心。执行器是批量运行程序,即实际运行作业逻辑的进程
注册中心:集群中节点和任务状态的同步更新、任务信息注册
监控中心:集成在调度控制台,实现任务进度实时监控、任务超时和失败告警
1.2 分布式任务调度核心概念
执行器:批量运行程序,即实际运行作业逻辑的进程;
执行器组:逻辑概念,是多个执行器的集合。每个作业都需要绑定一个执行器组,根据作业所配置的路由策略进行派发;
作业JOB:作业是批量逻辑的载体,是批量运行的最小单位;在Java里,作业对应的就是一个方法函数;在shell里,作业对应的就是一个sh脚本。
任务Task:任务是作业编排的容器,是多个作业如何有序运行的工作流定义。
1.3 常见分布式调度方案
While + Sleep:通过循环加休眠的方式定时执行
Timer和TimerTask实现:JDK自带的定时任务,可以实现简单的间隔执行任务(在指定时间点执行某一任务,也能定时的周期性执行),无法实现按日历去调度执行任务。
ScheduledExecutorService:Java并发包下,JDK1.5出现,是比较理想的定时任务实现方案。Eureka就使用的是它
QuartZ:使用Quartz,它是一个异步任务调度框架,功能丰富,可以实现按日历调度,支持持久化。
使用Spring Task:Spring 3.0后提供Spring Task实现任务调度,支持按日历调度,相比Quartz功能稍简单,但是在开发基本够用,支持注解编程方式。
SpringBoot中的Schedule:通过@EnableScheduling+@Scheduled最实现定时任务,底层使用的是Spring Task
开源中常见的分布式调度框架有以下几种,本文主要介绍Quartz和xxl-job。https://www.oschina.net/project/tag/327/task-schedule
2、Quartz作业调度框架
2.1 Quartz基本介绍
Quartz是OpenSymphony开源组织的一个Java开源项目,完全基于Java实现。
Quartz是一个功能丰富的开源作业调度框架,几乎可以集成到任何Java应用程序中——从最小的独立应用程序到最大的电子商务系统。Quartz可用于创建简单或复杂的调度来执行数十个、数百个甚至数万个作业;其任务被定义为标准Java组件的作业,这些组件几乎可以执行编写的任何程序。Quartz Scheduler包含许多企业级功能,例如对JTA事务和集群的支持
灵活的应用方式,例如支持作为独立程序运行也可以嵌入到另一个独立的应用程序中运行、可以在独立服务器或者容器中运行
强大的调度功能,例如支持精确到毫秒的调度、特定日期和重复特定次数的调度。丰富多样的调度方法,可以满足各种常规及特殊需求。
Job持久化,通过JobStore接口实现作业和触发器的持久化存储;
事务处理能力,支持通过使用JobStoreCMT参与JTA事务;
分布式和集群能力,例如集群故障转移和负载均衡
监听器和插件扩展功能,通过监听器接口来捕获调度事件以监视或控制作业/触发器行为
2.2 Quartz基本架构
Scheduler:核心调度器,实现任务调度和分发。Scheduler由SchedulerFactory创建,主要有三种,RemoteMBeanScheduler、RemoteScheduler和StdScheduler。
Trigger:触发器,用于定义任务调度的时间规则,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger、CronTirgger、DateIntervalTrigger和NthIncludedDayTrigger。
Job:代表具体被调度的任务。一个job可以被多个trigger关联,但是一个trigger只能关联一个job,job和trigger是1:N关系
Job有两种类型:无状态stateless和有状态stateful。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行
Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。
JobDetail:任务描述,描述job的静态消息,调度器需要的数据
JobStore:Quartz中的trigger和job需要存储下来才能被使用,Quartz中有两种存储方式:RAMJobStore和JDBCJobStore,RAMJobStore的存取速度非常快,但是在系统异常宕机后所有的数据都会丢失,所以在集群应用中,必须使用JDBCJobStore。
RAMJobStore:将trigger和job存储在内存中
JDBCJobStore:基于jdbc将trigger和job存储到数据库中
2.2.1 Quartz中Trigger类型
在任务调度框架Quartz中,主要有四种触发器:SimpleTrigger、CalendarIntervelTrigger、DailyTimeIntervalTrigger和CronTrigger。
1)SimpleTrigger
简单触发器。指定从某一个时间开始,以一定的时间间隔执行的任务,精度可以做到毫秒级。
simpleSchedule()
//.withIntervalInHours(1) //每小时执行一次
.withIntervalInMinutes(1) //每分钟执行一次
//.repeatForever() //次数不限
.withRepeatCount(10) //次数为10次
.build();//构建复制
2)CalendarIntervelTrigger
基于日历的触发器。比简单触发器更多时间单位,支持非固定时间的触发,间隔单位有秒、分钟、小时、天、月、年、星期。即使每年的月数和每个月的天数不是固定的也适用
calendarIntervalSchedule()
.withIntervalInDays(1) //每天执行一次
//.withIntervalInWeeks(1) //每周执行一次
.build();复制
3)DailyTimeIntervalTrigger
基于日期的触发器。支持指定时间间隔、每天的某个时间段。例如:每天早上9点到晚上9点,每隔1个小时执行一次,并且只在周一到周五执行。
dailyTimeIntervalSchedule()
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //每天9:00开始
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(21, 0)) //21:00 结束
.onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //周一至周五执行
.withIntervalInHours(1) //每间隔1小时执行一次
.withRepeatCount(100) //最多重复100次(实际执行100+1次)
.build();复制
4)CronTrigger
基于Cron表达式的触发器,是最常用的触发器类型;Cron表达式是基于Linux的crontab基础上移植出的表达式,用来定义时间维度的调度规则。
cronSchedule("0 0/3 9-15 * * ?") // 每天9:00-15:00,每隔3分钟执行一次
.build();
cronSchedule("0 30 9 ? * MON") // 每周一,9:30执行一次
.build();
weeklyOnDayAndHourAndMinute(MONDAY,9, 30) //等同于 0 30 9 ? * MON
.build();复制
2.2.2 Listener监听器
Listener监听器用来监听每个任务的运行状态和执行进度,运行结束或者失败后能够及时的通知到管理员。Quartz中提供了三种Listener:监听Scheduler的SchedulerListener、监听Trigger的TriggerListener和监听Job的JobListener。
Listener使用观察者模式,来定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并自动更新。只需要创建类实现相应的接口,并在Scheduler上注册Listener,便可实现对核心对象的监听。
2.2.3 JobStore存储
Quartz中Job运行状态这些信息有两种存储方式:RAMJobStore和JDBCJobStore,默认是存储在内存中的,但是存在问题是如果任务执行到一半的时候,调度器服务重启,原来任务中运行的信息会全部丢失。
1)RAMJobStore
JobStore默认存储方式,也就是把任务和触发器信息运行的信息存储在内存中,用到了HashMap、TreeSet、HashSet等等数据结构。如果程序崩溃或重启,所有存储在内存中的数据都会丢失,如果想要进行数据的恢复,就需要把这些数据持久化到磁盘。
2)JDBCJobStore
通过JDBC接口,将任务运行数据保存在数据库中,具备以下表:
qrtz_blob_triggers:Trigger作为Blob类型存储;
qrtz_calendars:存储Quartz的Calendar信息;
qrtz_cron_triggers:存储CronTrigger,包括Cron表达式和时区信息;
qrtz_fired_triggers:存储与已触发的Trigger相关的状态信息,以及相关Job的执行信息;
qrtz_job_details:存储每一个已配置的Job的详细信息;
qrtz_locks:存储程序的悲观锁的信息;
qrtz_paused_trigger_grps:存储已暂停的Trigger组的信息;
qrtz_scheduler_state:存储少量的有关Scheduler的状态信息,和别的Scheduler实例;
qrtz_simple_triggers:存储SimpleTrigger的信息,包括重复次数、间隔、以及已触的次数;
qrtz_simprop_triggers:存储CalendarIntervalTrigger和DailyTimeIntervalTrigger类型的触发器;
qrtz_triggers:存储已配置的Trigger的信息;复制
3、xxl-job分布式任务调度平台
3.1 xxl-job介绍
XXL-JOB是大众点评员工徐雪里于2015年发布的分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
3.1.1 调度和任务解耦
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。
3.1.2 xxl-job的特性
图形化操作界面简单便捷,支持任务可视化编辑、动态修改任务状态、启停任务、超时控制、重试次数配置、任务失败告警
调度平台和执行器分离,调度中心采用集中式管理、执行器分布式执行任务
注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行
弹性扩缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务
丰富的路由策略:执行器部署时配置不同的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等
任务动态分片:以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度
可视化观测:实时查看任务调度日志和执行的结果、实时监控任务进度
3.2 xxl-job整体架构
调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
执行器:负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。
任务:设置任务、执行策略、分片机制、执行器等属性,其中执行器与执行器管理中选择一个执行器名称,这样就可以分配到对应appName的执行器。
3.2.1 调度中心高可用
调度中心的高可用是基于数据库的集群方案,数据库选用Mysql;集群分布式并发环境中进行定时任务调度时,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。
3.2.2 并行调度
xxl-job调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。
xxl-job调度模块默认采用并行机制,在多线程调度的情况下,调度模块被阻塞的几率很低,大大提高了调度系统的承载量。
xxl-job的不同任务之间并行调度、并行执行。
xxl-job的单个任务,针对多个执行器是并行运行的,针对单个执行器是串行执行的。
3.2.3 执行器高可用
执行器如果是集群部署,调度中心将会感知到在线的所有执行器,如“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”。
当任务“路由策略”选择为“故障转移(FAILOVER)”时,调度中心在每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。
心跳检测成功的执行器会被选定为“目标执行器”,然后对“目标执行器”发送调度请求,调度流程结束,等待执行器回调执行结果。
调度成功后,可在日志监控界面查看“调度备注”,“调度备注”可以看出本地调度运行轨迹,执行器的“注册方式”、“地址列表”和任务的“路由策略”。
3.3 xxl-job配置
3.3.1 路由策略
FIRST(第一个):固定选择第一个机器;
LAST(最后一个):固定选择最后一个机器;
ROUND(轮询):依次选择机器分发;
RANDOM(随机):随机选择在线的机器;
CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
3.3.2 子任务
xxl-job中每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),同时每个任务支持设置属性“子任务ID”,因此通过任务ID可以匹配任务的依赖关系。当父任务执行结束并且执行成功时,将会根据“子任务ID”匹配子任务依赖,如果匹配到子任务,将会主动触发一次子任务的执行。
3.3.3 调度过期策略
忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间。
立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间。
3.3.4 阻塞处理策略
单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
3.3.5 任务超时时间
自定义任务超时时间,任务运行超时将会主动中断任务。
3.3.6 失败重试次数
自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试。
3.3.7 分片广播和动态分片
执行器集群部署时,任务路由策略选择“分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数。“分片广播”以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
3.4 xxl-job使用体验
3.4.1 xxl-job源码
下载项目源码并解压,使用IDEA工具导入项目,源码仓库地址https://github.com/xuxueli/xxl-job
3.4.2 基于docker安装mysql
1)执行命令安装mysql
[root@tango-rac01 local]# docker pull mysql
复制
2)启动mysql镜像
docker run --privileged=true -e MYSQL_ROOT_PASSWORD=123456qaz -p 3306:3306 -v opt:/opt mysql --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
复制
启动成功:
2023-01-24T16:02:54.083215Z 0 [System] [MY-010931] [Server] usr/sbin/mysqld: ready for connections. Version: '8.0.32' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
复制
查看docker进程
[root@tango-rac01 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bd3a912091d6 mysql "docker-entrypoint..." 2 minutes ago Up 2 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp jovial_jepsen
[root@tango-rac01 ~]#复制
3.4.3 初始化调度数据库
打开项目代码,获取“调度数据库初始化SQL脚本”并执行即可。“调度数据库初始化SQL脚本” 位置为: xxl-job/doc/db/tables_xxl_job.sql,数据库名为xxl_job
xxl_job_lock:任务调度锁表;
xxl_job_group:执行器信息表,维护任务执行器信息;
xxl_job_info:调度扩展信息表:用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
xxl_job_log:调度日志表:用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
xxl_job_user:系统用户表;复制
1)进入docker,连接到mysql
[root@tango-rac01 ~]# docker exec -it bd3a912091d6 bin/bash
bash-4.4# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.32 MySQL Community Server - GPL
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.01 sec)复制
2)执行命令创建表
mysql> source opt/tables_xxl_job.sql
Query OK, 1 row affected (0.00 sec)
mysql> use xxl_job;
Database changed
mysql> show tables;
+--------------------+
| Tables_in_xxl_job |
+--------------------+
| xxl_job_group |
| xxl_job_info |
| xxl_job_lock |
| xxl_job_log |
| xxl_job_log_report |
| xxl_job_logglue |
| xxl_job_registry |
| xxl_job_user |
+--------------------+
8 rows in set (0.00 sec)复制
3)设置权限。默认已经root用户设置了远程访问,也就是%的那条记录。但是密码和localhost的不一样。因此,需要修改密码,并刷新权限。
mysql> select Host,User,authentication_string from mysql.user;
+-----------+------------------+------------------------------------------------------------------------+
| Host | User | authentication_string |
+-----------+------------------+------------------------------------------------------------------------+
| % | root | $A$005$g%HDbViP>?/!GB5j5KTf2lQk7MFNIPcYqS2syBvIT5H6NyrmRI/Xy11BiB |
| localhost | mysql.infoschema | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| localhost | mysql.session | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| localhost | mysql.sys | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| localhost | root | $A$005$|v@\<vbsk@+d8jjfGL7rLXRuED4217eSmh0qApP76tLMx3Xd/2HWTO1Bp9 |
+-----------+------------------+------------------------------------------------------------------------+
5 rows in set (0.00 sec)复制
修改密码
mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456qaz';
Query OK, 0 rows affected (0.01 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)复制
3.4.4 docker安装xxl-job
1)执行命令,必须指定tag,因为官方没有打latest的tag标签,所以会提示下载失败!
[root@tango-rac01 local]# docker pull xuxueli/xxl-job-admin:2.3.1
复制
2)启动镜像
[root@tango-rac01 ~]# docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.112.135:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 --spring.datasource.username=root --spring.datasource.password=123456qaz" -p 8080:8080 -v /tmp:/opt/applogs --name xxl-job-admin xuxueli/xxl-job-admin:2.3.1
复制
启动成功
3.4.5 访问任务调度中心
访问地址:http://192.168.112.135:8080/xxl-job-admin
4、xxl-job和Quartz对比
不适合大量的短任务以及不适合过多节点部署;
解决了高可用的问题,并没有解决任务分片的问题,存在单机处理的极限,即不能实现水平扩展
需要把任务信息持久化到到底层数据表中,系统侵入性相当严重
调度逻辑和执行逻辑并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免地会相互影响。
quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡机制。
xxl-job解决了以上问题,下表是xxl-job和Quartz的特性对比。
特性 | xxl-job | Quartz |
---|---|---|
定时调度 | Cron | Cron |
集群部署 | 支持调度器和执行器集群 | 只支持数据库集群部署 |
容器部署 | 支持docker部署 | 不支持 |
任务编排 | 支持 | 不支持 |
弹性扩缩容 | 支持 | 不支持 |
动态分片 | 支持 | 不支持 |
多语言 | Java、Shell、Python | Java |
可观测 | 运行报表、运行日志 | 无 |
报警监控 | 邮件 | 无 |
性能 | 由Master节点调度,Master节点压力大 | 调度通过DB,DB节 点压力大 |
顺祝2023年阖家欢乐、学习兔飞猛进、事业大展宏兔!
参考资料:
http://www.quartz-scheduler.org/overview/
https://blog.csdn.net/FeenixOne/article/details/128264129
https://www.jianshu.com/p/d525c9f95c61
https://www.cnblogs.com/zhenyuyaodidiao/p/4755649.html
https://cloud.tencent.com/developer/article/1995153
https://www.xuxueli.com/xxl-job/