数据平台-调度系统(1)-quartz

在数据平台中,每天都会有上万个任务进行流转,如何准确,实时的完成任务,是非常关键的一步。公司在发展的过程经历了azikaban->airflow->dataflow(自研airflow支持k8s)->kepler的一个过程。目前使用的调度系统任务3w+,日执行10w+。上线0事故,非常稳定的运行。

底层调度器用的是quartz,写调度系统之前肯定要了解调度器的源码。于是记录下。

Quartz是Java领域著名的开源任务调度工具。Quartz提供了极为广泛的特性如持久化任务,集群和分布式任务等,其特点如下:

  • 完全由Java写成,方便集成(Spring)
  • 伸缩性
  • 负载均衡
  • 高可用性
使用方式

我们的使用姿势是 springboot+quartz

配置文件

quartz.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
quartz.org.quartz.threadPool.threadCount=10
quartz.org.quartz.threadPool.threadPriority=5

quartz.org.quartz.scheduler.instanceName=kepler
quartz.org.quartz.scheduler.instanceId=AUTO
quartz.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
quartz.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
quartz.org.quartz.jobStore.useProperties=false
quartz.org.quartz.jobStore.tablePrefix=QRTZ_
quartz.org.quartz.jobStore.isClustered=true
quartz.org.quartz.jobStore.misfireThreshold=1800000
quartz.org.quartz.jobStore.clusterCheckinInterval=5000
quartz.org.quartz.jobStore.dataSource=quartzDataSource

quartz.org.quartz.dataSource.quartzDataSource.provider=hikaricp
quartz.org.quartz.dataSource.quartzDataSource.poolName=hikari-quartz
quartz.org.quartz.dataSource.quartzDataSource.driver=${mysql.driver-class-name}
quartz.org.quartz.dataSource.quartzDataSource.URL=${mysql.jdbc-url}
quartz.org.quartz.dataSource.quartzDataSource.user=${mysql.username}
quartz.org.quartz.dataSource.quartzDataSource.password=${mysql.password}
quartz.org.quartz.dataSource.quartzDataSource.maximumPoolSize=${mysql.maximum-pool-size}
quartz.org.quartz.dataSource.quartzDataSource.connectionTestQuery=${mysql.connection-test-query}
quartz.org.quartz.dataSource.quartzDataSource.connectionTimeout=${mysql.connection-timeout}
quartz.org.quartz.dataSource.quartzDataSource.idleTimeout=${mysql.idle-timeout}
quartz.org.quartz.dataSource.quartzDataSource.maxLifetime=${mysql.max-lifetime}
Scheduler sched = SchedulerFactory.getScheduler(config);
JobDetail jobDetail = JobBuilder.newJob(jobClass)
                             .withIdentity(jobName, HM_JOB)//任务名称和组构成任务key
                            .build();
jobDetail.getJobDataMap().putAll(data);
            // 触发器
SimpleTrigger trigger = TriggerBuilder.newTrigger()
                           .withIdentity(jobName, HM_TRIGGER)//触发器key
                           .startAt(DateBuilder.futureDate(1, IntervalUnit.SECOND))
                           .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                           .withIntervalInSeconds(interval)
                           .repeatForever())
                           .build();

sched.scheduleJob(jobDetail, trigger);
            // 启动
if (!sched.isShutdown()) {
    sched.start();
}
工作原理

Quartz是通过对用户暴露出Scheduler来进行任务的操作,它可以把任务JobDetail和触发器Trigger加入任务池中,可以把任务删除,也可以把任务停止,scheduler把这些任务和触发器放到一个JobStore中,这里jobStore有内存形式的也有持久化形式的,当然也可以自定义扩展成独立的服务。

  1. Job就是自定义业务的接口,里面就一个execute方法,线程运行Job时会把JobDataMap封装到JobExecutionContext里作为execute方法的参数,jobdetail是对job的封装,里面有Job的class,对应的数据, 名称,分组等
  2. Trigger是触发器,job下次什么时候执行存放在trigger中
  3. QuartzSchedulerResources相当于调度的资源存放器,包含了JobStore, ThreadPool等资源,调度都是通过 QuartzSchedulerResources获取相关属性的。
  4. jobStore是任务和触发器存储地方,它里面提供大量类似于增删改的操作任务方法。
  5. QuartzSchedulerThread是一个调度线程,ThreadPool是一个执行线程池。里面有workerThread来执行用户任务
源码分析

根据上面的使用流程,逐个来分析源码过程

首先获取Scheluer
Scheduler scheduler = StdSchedulerFactory.getScheduler(config);

StdSchedulerFactory根据配置文件来生成Scheduler。
1.

    public Scheduler getScheduler() throws SchedulerException {
    //参数没传的话,则读取默认的配置
        if (cfg == null) {
        //初始化配置文件
            initialize();
        }

        SchedulerRepository schedRep = SchedulerRepository.getInstance();

        Scheduler sched = schedRep.lookup(getSchedulerName());

        if (sched != null) {
            if (sched.isShutdown()) {
                schedRep.remove(getSchedulerName());
            } else {
                return sched;
            }
        }
    // 第二步:初始化,生成scheduler
        sched = instantiate();

        return sched;
    }
  1. instantiate()方法内部做了非常多的初始化,代码将近800行。需要初始化的参数为
        JobStore js = null;
        ThreadPool tp = null;
        QuartzScheduler qs = null;
        DBConnectionManager dbMgr = null;
        String instanceIdGeneratorClass = null;
        Properties tProps = null;
        String userTXLocation = null;
        boolean wrapJobInTx = false;
        boolean autoId = false;
        long idleWaitTime = -1;
        long dbFailureRetry = 15000L; // 15 secs
        String classLoadHelperClass;
        String jobFactoryClass;
        ThreadExecutor threadExecutor;

具体干的内容为

  1. Get Scheduler Properties 获取配置

  2. 判断是不是remote scheduler方式即rmi方式

  3. 判断是不是jmxschedule
    在这里插入图片描述

  4. 生成InstanceIdGenerator
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F84y8adc-1599566634074)(evernotecid://97E9B663-FEA6-4C0C-B3BB-2FFD94DC35AA/appyinxiangcom/16306208/ENResource/p310)]

  5. Get ThreadPool Properties
    在这里插入图片描述

  6. Get JobStore Properties
    在这里插入图片描述

  7. 设置DataSources/SchedulerPlugins/JobListeners/TriggerListeners

  8. Get ThreadExecutor Properties
    在这里插入图片描述

  9. 启动进程 Fire everything up 包括设置QuartzSchedulerResources(包含创建QuartzScheduler实例所需的所有资源(JobStore,ThreadPool等))
    在这里插入图片描述

整个组装需要初始化很多东西,所以用到了FactoryBean的方式。
再祖装的过程中

 qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);

是非常重要的一步
QuartzScheduler 这是Quartz的核心,它是org.quartz.Scheduler接口的间接实现,包含调度org.quartz.Jobs,注册org.quartz.JobListener实例等的方法
点进去发现

 public QuartzScheduler(QuartzSchedulerResources resources,
            long idleWaitTime, @Deprecated long dbRetryInterval)
            throws SchedulerException {
        this.resources = resources;
        if (resources.getJobStore() instanceof JobListener) {
            addInternalJobListener((JobListener) resources.getJobStore());
        }
		//初始化了一个线程,并且放到了ThreadExecutor中执行。这个线程是用来调度任务的线程
        this.schedThread = new QuartzSchedulerThread(this, resources);
        ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
        schedThreadExecutor.execute(this.schedThread);
        if (idleWaitTime > 0) {
            this.schedThread.setIdleWaitTime(idleWaitTime);
        }

        jobMgr = new ExecutingJobsManager();
        addInternalJobListener(jobMgr);
        errLogger = new ErrorLogger();
        addInternalSchedulerListener(errLogger);

        signaler = new SchedulerSignalerImpl(this, this.schedThread);

        getLog().info("Quartz Scheduler v." + getVersion() + " created.");
    }
获取到之后scheduleJob

进入scheduleJob内部
在这里插入图片描述

其实就是进入到了上面提到的QuartzScheduler内部中的方法
其中核心代码是
在这里插入图片描述

就是将最新一次的执行时间写入到trigger表,然后让调度线程扫描到之后进行执行任务。

scheduler.start()

在这里插入图片描述

核心代码为:

//启动jobStore
 this.resources.getJobStore().schedulerStarted();        //将线程置位启动状态
 schedThread.togglePause(false);

上面的启动jobstroe的代码为
在这里插入图片描述

主要干了一些恢复任务,设置misfire的处理规则。这个时候任务就真正的调度起来了。刚才调度的任务就会被扫描到。

调度器起来之后是如何扫描的呢

上面也提到了,在初始化Scheluer的时候初始化了一个线程 QuartzSchedulerThread这个线程就是用来调度器。
代码很长,主要就干了三件事

public void run() {
   boolean lastAcquireFailed = false;
   while (!halted.get()) {
     ......

     int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
     if(availThreadCount > 0) { 

     ......

     //调度器在trigger队列中寻找30秒内一定数目的trigger(需要保证集群节点的系统时间一致)
     triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                            now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());

     ......

     //触发trigger
     List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);

     ......

     //释放trigger
     for (int i = 0; i < triggers.size(); i++) {
         qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
     }

   }                
}

由此可知,QuartzScheduler调度线程不断获取trigger,触发trigger,释放trigger.下面分析trigger的获取过程,qsRsrcs.getJobStore()返回对象是JobStore,集群环境配置如下:org.quartz.impl.jdbcjobstore.JobStoreTX

JobStoreTX继承自JobStoreSupport,而JobStoreSupport的acquireNextTriggers、triggersFired、releaseAcquiredTrigger方法负责具体trigger相关操作,都必须获得TRIGGER_ACCESS锁。核心逻辑在executeInNonManagedTXLock方法中:

protected <T> T executeInNonManagedTXLock(
        String lockName, 
        TransactionCallback<T> txCallback, final TransactionValidator<T> txValidator) throws JobPersistenceException {
    boolean transOwner = false;
    Connection conn = null;
    try {
        if (lockName != null) {
            if (getLockHandler().requiresConnection()) {
                conn = getNonManagedTXConnection();
            }

            //获取锁
            transOwner = getLockHandler().obtainLock(conn, lockName);
        }

        if (conn == null) {
            conn = getNonManagedTXConnection();
        }

        final T result = txCallback.execute(conn);
        try {
            commitConnection(conn);
        } catch (JobPersistenceException e) {
            rollbackConnection(conn);
            if (txValidator == null || !retryExecuteInNonManagedTXLock(lockName, new TransactionCallback<Boolean>() {
                @Override
                public Boolean execute(Connection conn) throws JobPersistenceException {
                    return txValidator.validate(conn, result);
                }
            })) {
                throw e;
            }
        }

        Long sigTime = clearAndGetSignalSchedulingChangeOnTxCompletion();
        if(sigTime != null && sigTime >= 0) {
            signalSchedulingChangeImmediately(sigTime);
        }

        return result;
    } catch (JobPersistenceException e) {
        rollbackConnection(conn);
        throw e;
    } catch (RuntimeException e) {
        rollbackConnection(conn);
        throw new JobPersistenceException("Unexpected runtime exception: "
                + e.getMessage(), e);
    } finally {
        try {
            releaseLock(lockName, transOwner);      //释放锁
        } finally {
            cleanupConnection(conn);
        }
    }
}

由上代码可知Quartz集群基于数据库锁的同步操作流程如下图所示:
在这里插入图片描述

一个调度器实例在执行涉及到分布式问题的数据库操作前,首先要获取QUARTZ_LOCKS表中对应的行级锁,获取锁后即可执行其他表中的数据库操作,随着操作事务的提交,行级锁被释放,供其他调度实例获取。集群中的每一个调度器实例都遵循这样一种严格的操作规程。

getLockHandler()方法返回的对象类型是Semaphore,获取锁和释放锁的具体逻辑由该对象维护

public interface Semaphore {

     boolean obtainLock(Connection conn, String lockName) throws LockException;

     void releaseLock(String lockName) throws LockException;

     boolean requiresConnection();
}

该接口的实现类完成具体操作锁的逻辑,在JobStoreSupport的初始化方法中注入的Semaphore具体类型是StdRowLockSemaphore
StdRowLockSemaphore的源码

public class StdRowLockSemaphore extends DBSemaphore {
//锁定SQL语句
public static final String SELECT_FOR_LOCK = "SELECT * FROM "
        + TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_LOCK_NAME
        + " = ? FOR UPDATE";

public static final String INSERT_LOCK = "INSERT INTO " + TABLE_PREFIX_SUBST 
        + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " 
        + COL_LOCK_NAME + ") VALUES (" + SCHED_NAME_SUBST + ", ?)"; 

//指定锁定SQL
protected void executeSQL(Connection conn, String lockName, String expandedSQL) throws LockException {
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        ps = conn.prepareStatement(expandedSQL);
        ps.setString(1, lockName);
        ......
        rs = ps.executeQuery();
        if (!rs.next()) {
            throw new SQLException(Util.rtp(
                "No row exists in table " + TABLE_PREFIX_SUBST +
                TABLE_LOCKS + " for lock named: " + lockName, getTablePrefix()));
        }
    } catch (SQLException sqle) {

    } finally {
      ...... //release resources
    }
  }
}

//获取QRTZ_LOCKS行级锁
public boolean obtainLock(Connection conn, String lockName) throws LockException {
    lockName = lockName.intern();

    if (!isLockOwner(conn, lockName)) {
        executeSQL(conn, lockName, expandedSQL);

        getThreadLocks().add(lockName);
    }
    return true;
}

//释放QRTZ_LOCKS行级锁
public void releaseLock(Connection conn, String lockName) {
    lockName = lockName.intern();

    if (isLockOwner(conn, lockName)) {
        getThreadLocks().remove(lockName);
    }
    ......
}

每当要进行与某种业务相关的数据库操作时,先去QRTZ_LOCKS表中查询操作相关的业务对象所需要的锁,在select语句之后加for update来实现。例如,TRIGGER_ACCESS表示对任务触发器相关的信息进行修改、删除操作时所需要获得的锁。这时,执行查询这个表数据的SQL形如:

select * from QRTZ_LOCKS t where t.lock_name='TRIGGER_ACCESS' for update

扫描的源码在acquireNextTriggers中
在这里插入图片描述
真正执行的sql是

SELECT
    TRIGGER_NAME,
    TRIGGER_GROUP,
    NEXT_FIRE_TIME,
    PRIORITY
FROM
    qrtz_TRIGGERS
WHERE
    SCHED_NAME = 'schedulerFactoryBean'
AND TRIGGER_STATE = 'WAITING'
AND NEXT_FIRE_TIME <= (now + idleWaitTime)
AND (
    MISFIRE_INSTR = -1
    OR (
        MISFIRE_INSTR != -1
        AND NEXT_FIRE_TIME >= (now - misfireThreshold)
    )
)
ORDER BY  NEXT_FIRE_TIME ASC, PRIORITY DESC
这个SQL语句是要查询出:未来30s内将要执行的任务,且MISFIRE_INSTR为-1(MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY),或者MISFIRE_INSTR不为-1,但是,NEXT_FIRE_TIME错过的执行时间不能超过阀值60s。影响misFire执行策略的另一个参数就是misfireThreshold,配置文件quartz.properties中,对应org.quartz.jobStore.misfireThreshold: 60000,单位毫秒。也就是说:如果【错过时间】不超过60s都不算是misFire,不执行misFire策略,依次执行错过的任务时间点;【错过时间】超过60s按misFire策略执行。
#### 参考
1. [Quartz应用与集群原理分析](https://tech.meituan.com/2014/08/31/mt-crm-quartz.html)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值