一、开源分布式调度系统发展历程:
1.1 初期:
- 使用方式:在Linux操作系统环境下输入crontab -e即可使用
- 使用场景:简单的定时邮件,定时查询等
- 不足:无接口复用、失败重试、依赖挂载等功能
1.2 发展期:
Python、JDK Timer逐渐丰富了接口复用、失败自动重试等功能
1.3 现状:
Quartz、Airflow、Dolphin-Scheduler、xxl-job等优秀开源项目不断在伸缩性、扩展性、负载均衡、高可用性进行探索并实现。在快速部署、任务监控层面也取得一定成果
二、设计理念:
开源调度系统整体基本都是master/slave主从结构,调度引擎为master,执行引擎为slave。其中,将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的JobHandler,交由"执行引擎executor"统一管理,executor负责接收调度请求并执行对应的JobHandler中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。
2.1 核心组件:
- master:调度引擎,也可称为’调度中心’。其作用为作业初始化,根据任务节点基础属性以及依赖关系进行构建任务依赖DAG,生成各类参数的实值。
- executor:执行引擎,根据调度引擎生成的具体任务实例和配置信息,分配CPU、内存、运行节点等资源,在任务对应的执行环境中运行节点代码。
2.2 通信关系模型:
三、运行原理:
3.1 基本模块
以Quartz为例:它是由OpenSymphony提供的、开源的、Java编写的强大任务调度框架。
基本构成:job模块、trigger模块、scheduler模块。
3.2 Quartz基本表说明
表名 | 描述 | 备注 |
---|---|---|
qrtz_triggers | 存储已配置的 触发器 (Trigger) 的信息 | status:'waiting’为待触发,'pause’为暂停执行;next_fire_time为‘时间’类型任务的‘下一次触发’时间 |
qrtz_cron_triggers | 存储 Cron Trigger,包括 Cron 表达式和时区信息 | 调度相关描述信息 |
qrtz_fired_triggers | 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 | 数据库悲观锁,用来保证部署分布式调度服务时,多个scheduler服务只提交一次任务 |
qrtz_job_details | 存储每一个已配置的 Job 的详细信息(jobDetail) | 调度相关描述信息 |
qrtz_job_listeners | 存储有关已配置的 Job 监听器 的信息 | 调度相关描述信息 |
qrtz_simple_triggers | 存储简单的 Trigger,包括重复次数、间隔、以及已触的次数 | 调度相关描述信息 |
qrtz_calendars | 以Blob类型存储Quartz的Calendar信息 | 可以和Cron配合使用,用Cron表达式指定一个触发时间规律,用Calendar指定一个范围 |
qrtz_locks | 存储程序的锁的信息 | 调度相关描述信息 |
3.3 Quartz的触发时间的配置:
- cron方式:采用cronExpression表达式配置时间。【OneData】
- simple方式:和JavaTimer差不多,可以指定一个开始时间和结束时间外加一个循环时间。
- calendars方式:可以和cron配合使用,用cron表达式指定一个触发时间规律,用calendar指定一个范围
3.4 Quartz触发原理
- 调度线程从扫描触发器列表,获取30s内将要执行的triggertask列表,其中waiting状态的为等待被调起的任务。
- 获取trigger对应的分布式数据库锁,更新fire trigger状态,由原来的waiting转为exeuting,并更新next_fire_time。
- 将任务放入worker线程池进行提交运行,待完成后释放锁并更新状态。
1. build job
2. build triggerjobtrigger1n
3. build schedulertriggerjob
// 1. define the job and tie it to our HelloJob class
JobDetail job = newJob(HelloJob.class)
.withIdentity("job1", "group1")
.build();
// 2. Trigger the job to run now, and then repeat every 40
seconds
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
// 3. Tell quartz to schedule the job using our trigger
scheduler.scheduleJob(job, trigger);
3.5 任务提交策略:
通常提交策略有以下3种:
- at most once至多一次:事件被保证只会被所有算子最多处理一次。即调度系统中任务提交上去,不管返回成功与否,不再重新发 起请求。
- at least once至少一次:事件被保证会被所有算子都至少处理一次,即所有’调度分发器’都尝试提交任务,且任务失败进行重 试。
- exactly once:倘若发生各种故障,事件也会被确保只会被所有算子"恰好"处理"一次"。即任务执行最终结果不会发生任何重复。
我们一般都设置at least once避免任务未得到正确执行,那么在这个情况下如何保证任务exactly once地提交任务?
3.5.1 单机服务:
任务层面: 保证重复执行时结果仍然幂等。如hive sql清洗表时使用insert overwrite覆盖写,重新执行时会将上一次计算结果擦 除。
服务层面: 避免重复提交,如本地消息表,存储当前任务的运行状态,下图为任务状态机模型。只有任务失败时才进行重试,并且 成功时对该任务周期当前批次不再自动提交,并天然地解决了任务循环依赖问题。
3.5.2 分布式服务: 分布式服务间避免任务重复提交,如分布式锁。
分布式锁的几种实现方式:
基于数据库: 基于mysql行级别的悲观锁,使得只有一个线程能获得该锁,并在线程执行完毕后释放该锁 基于mvcc原理的数据库版本号乐观锁
基于redis缓存实现分布式锁 基于zookeeper
大多分布式服务大多使不推荐使用数据库分布式锁,且它存在数据库单点问题,为什么调度系统大多却偏偏选择它?
调度任务为读多写少的场景。 任务需要存储job与trigger等元数据信息。 此方式能可承载百万级调度,但是不可支持分钟级别百万调度,如对性能要求过高,不建议使用quartz作为分布式调度系统组件。
四、QUARTZ的使用
4.1 依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>3.1.2</version>
</dependency>
4.2 数据库配置
org:
quartz:
scheduler:
instanceName: scheduler
instanceId: AUTO
# instanceName: CLUSTERED_JOB_SCHEDULER
# instanceId: AUTO
# instanceIdGenerator.class: org.quartz.simpl.SimpleInstanceIdGenerator
skipUpdateCheck: true
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 40
threadPriority: 5
jobStore:
misfireThreshold: 300000
class: org.quartz.impl.jdbcjobstore.JobStoreTX
useProperties: false
dataSource: myDS
# tablePrefix: QRTZ_
tablePrefix: qrtz_
isClustered: true # Set cluster mode
txIsolationLevelSerializable: true # Quartz isolation level in cluster mode: higher than the repeatable read isolation level
clusterCheckinInterval: 20000
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
dataSource:
myDS:
driver: com.mysql.jdbc.Driver
URL: ${org.quartz.dataSource.myDS.url}
user: ${org.quartz.dataSource.myDS.user}
password: ${org.quartz.dataSource.myDS.password}
maxConnections: 5
validationQuery: select 0
# Configure Plugins
plugin:
shutdownHook:
class: org.quartz.plugins.management.ShutdownHookPlugin
cleanShutdown: true
triggHistory:
class: org.quartz.plugins.history.LoggingJobHistoryPlugin
核心api
// Core API: Job creation demo and scheduling
// 1. Define the job and tie it to our HelloJob class
JobDetail job = newJob(HelloJob.class)
.withIdentity("job1", "group1")
.build();
// 2. Trigger the job to run now, and then repeat every 40 seconds
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(40)
.repeatForever())
.build();
// 3. Schedule the job using the trigger
scheduler.scheduleJob(job, trigger);
// Demo for querying, pausing, resuming, and deleting jobs
@Resource
StdScheduler scheduler;
/**
* Get a list of all scheduled jobs
*
* @return List of ScheduleJob objects
* @throws SchedulerException
*/
public List<ScheduleJob> getAllJob() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
List<ScheduleJob> jobList = new ArrayList<ScheduleJob>();
for (JobKey jobKey : jobKeys) {
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger : triggers) {
ScheduleJob job = new ScheduleJob();
job.setJobName(jobKey.getName());
job.setJobGroup(jobKey.getGroup());
job.setDescription("Trigger: " + trigger.getKey());
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
job.setJobStatus(triggerState.name());
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
String cronExpression = cronTrigger.getCronExpression();
job.setCronExpression(cronExpression);
}
jobList.add(job);
}
}
return jobList;
}
/**
* Get a list of all running jobs
*
* @return List of ScheduleJob objects
* @throws SchedulerException
*/
public List<ScheduleJob> getRunningJob() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
List<ScheduleJob> jobList = new ArrayList<ScheduleJob>(executingJobs.size());
for (JobExecutionContext executingJob : executingJobs) {
ScheduleJob job = new ScheduleJob();
JobDetail jobDetail = executingJob.getJobDetail();
JobKey jobKey = jobDetail.getKey();
Trigger trigger = executingJob.getTrigger();
job.setJobName(jobKey.getName());
job.setJobGroup(jobKey.getGroup());
job.setDescription("Trigger: " + trigger.getKey());
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
job.setJobStatus(triggerState.name());
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
String cronExpression = cronTrigger.getCronExpression();
job.setCronExpression(cronExpression);
}
jobList.add(job);
}
return jobList;
}
/**
* Pause a job
*
* @param scheduleJob The job to pause
* @throws SchedulerException
*/
public void pauseJob(ScheduleJob scheduleJob) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
scheduler.pauseJob(jobKey);
}
/**
* Resume a job
*
* @param scheduleJob The job to resume
* @throws SchedulerException
*/
public void resumeJob(ScheduleJob scheduleJob) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
scheduler.resumeJob(jobKey);
}
/**
* Delete a job
*
* @param scheduleJob The job to delete
* @throws SchedulerException
*/
public void deleteJob(ScheduleJob scheduleJob) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
scheduler.deleteJob(jobKey);
}
/**
* Execute a job immediately
*
* @param scheduleJob The job to run
* @throws SchedulerException
*/
public void runAJobNow(ScheduleJob scheduleJob) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
scheduler.triggerJob(jobKey);
}
/**
* Update the cron expression of a job
*
* @param scheduleJob The job to update
* @throws SchedulerException
*/
public void updateJobCron(ScheduleJob scheduleJob) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression());
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
scheduler.rescheduleJob(triggerKey, trigger);
}