分布式调度系统与QUARTZ执行原理

一、开源分布式调度系统发展历程:

1.1 初期:

  1. 使用方式:在Linux操作系统环境下输入crontab -e即可使用
  2. 使用场景:简单的定时邮件,定时查询等
  3. 不足:无接口复用、失败重试、依赖挂载等功能

1.2 发展期:

Python、JDK Timer逐渐丰富了接口复用、失败自动重试等功能

1.3 现状:

Quartz、Airflow、Dolphin-Scheduler、xxl-job等优秀开源项目不断在伸缩性、扩展性、负载均衡、高可用性进行探索并实现。在快速部署、任务监控层面也取得一定成果
在这里插入图片描述

二、设计理念:

开源调度系统整体基本都是master/slave主从结构,调度引擎为master,执行引擎为slave。其中,将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的JobHandler,交由"执行引擎executor"统一管理,executor负责接收调度请求并执行对应的JobHandler中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。

2.1 核心组件:

  1. master:调度引擎,也可称为’调度中心’。其作用为作业初始化,根据任务节点基础属性以及依赖关系进行构建任务依赖DAG,生成各类参数的实值。
  2. 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的触发时间的配置:

  1. cron方式:采用cronExpression表达式配置时间。【OneData】
  2. simple方式:和JavaTimer差不多,可以指定一个开始时间和结束时间外加一个循环时间。
  3. calendars方式:可以和cron配合使用,用cron表达式指定一个触发时间规律,用calendar指定一个范围

3.4 Quartz触发原理

  1. 调度线程从扫描触发器列表,获取30s内将要执行的triggertask列表,其中waiting状态的为等待被调起的任务。
  2. 获取trigger对应的分布式数据库锁,更新fire trigger状态,由原来的waiting转为exeuting,并更新next_fire_time。
  3. 将任务放入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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值