xxljob源码阅读——服务端调度

本文深入解析xxljob调度中心的架构,包括基础数据结构如xxlJobInfo、xxlJobGroup和xxlJobLog,客户端心跳检测机制,任务调度的scheduleThread和ringThread,任务触发策略,以及任务失败监控和调度日志的报告与清理。通过对xxljob核心组件的理解,有助于更好地掌握其工作原理。

xxljob调度中心架构解读

本文基于2.1.2版本。

基础数据结构

  • xxlJobInfo(com.xxl.job.admin.core.model.XxlJobInfo)记录任务的所有信息。任务名称,任务cron表达式,任务所属执行器,任务下次执行时间等。
  • xxlJobGroup(com.xxl.job.admin.core.model.XxlJobGroup)记录执行器信息。与调度相关的字段是机器列表,记录了所有存活的机器。
  • xxlJobLog(com.xxl.job.admin.core.model.XxlJobLog) 记录任务调度日志。
  • xxlJobRegistry(com.xxl.job.admin.core.model.XxlJobRegistry)执行机器注册表,记录所有连接到调度中心的客户端。

客户端心跳检测

实现类:com.xxl.job.admin.core.thread.JobRegistryMonitorHelper。该部分负责将xxlJobRegistry注册表中的存活机器(心跳时间没有超过死亡时间的机器)刷新到xxlJobGroup的registryList字段中,提供给其他模块使用。通过该模块,可以大部分情况下确保xxlJobGroup中的registryList全部是存活机器。
注册表里的信息是客户端不断的去注册中心触发心跳,通知注册中心存活。

任务调度

实现类:com.xxl.job.admin.core.thread.JobScheduleHelper。该部分由两个线程配合完成,一个是预读调度线程scheduleThread,一个是触发调度线程ringThread。

ringData

ringData:是一个map结构,key存储的是秒(0-59),value存储的是需要触发的任务list。

private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();

scheduleThread

主要负责预读后五秒的调度任务,将任务插入到ringData中。
针对读取到的任务,有三个处理策略:

  • 已经超过触发时间五秒的任务,直接丢弃。
  • 已经超过触发时间但未超过五秒的任务,直接触发。并且计算下次触发时间,如果在预读范围,插入到ringData中。
  • 未到触发时间的任务,插入到ringData中。该分支也是主要执行分支,上两个分支只有在特殊情况下才会出现。
// schedule thread
        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    //睡眠4-5s 这个睡眠的意义是什么????
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init xxl-job admin scheduler success.");

                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                // 预读数量
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

                while (!scheduleThreadToStop) {

                    // Scan Job
                    long start = System.currentTimeMillis();

                    Connection conn = null;
                    Boolean connAutoCommit = null;
                    PreparedStatement preparedStatement = null;

                    boolean preReadSuc = true;
                    try {

                        conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);
                        //使用数据库锁保证注册中心集群只有一个机器能调度
                        preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                        preparedStatement.execute();

                        // tx start

                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        //直接根据任务下次调度时间从库中读取
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {

                                // time-ring jump
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                                    // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                                    // 任务过期5s直接放弃
                                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());

                                    // fresh next
                                    //刷新下次执行时间
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time
                                    //未超过五秒的过期任务
                                    // 1、trigger
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、fresh next
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    //计算在哪一秒
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

                            // 3、update trigger info
                            for (XxlJobInfo jobInfo: scheduleList) {
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }

                        } else {
                            //预读失败,
                            preReadSuc = false;
                        }

                        // tx stop


                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {

                        // commit
                        if (conn != null) {
                            //提交事务
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            //恢复conn的设置
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            //关闭连接
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

                        // close PreparedStatement
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    //计算耗时
                    long cost = System.currentTimeMillis()-start;


                    // Wait seconds, align second
                    if (cost < 1000) {  // scan-overtime, not wait
                        try {
                            // pre-read period: success > scan each second; fail > skip this period;
                            //预读成功 下一秒继续扫描,其实直接进行下一次循环更好。在多任务的情况下会有更好的表现
                            //预读失败 说明下一个预读时间已经没有需要扫描的,直接睡到下一个5s
                            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                        } catch (InterruptedException e) {
                            if (!scheduleThreadToStop) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
            }
        });

ringThread

负责读取ringData中的数据,进行每秒触发。
在进行任务触发的时候,xxljob有一个机制是会读取当前秒的任务及上一秒的任务,全部去触发。该机制用来解决每次任务触发执行时长超过1s时,会漏掉部分任务。通过该机制来进行补偿。
如果任务数量太多,每次调度时长超过了2s,该补偿机制就会有问题。整个调度还是会漏掉部分任务,不过会在下一分钟的时候触发。

      // ring thread
        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {

                // align second
                try {
                    //这个睡眠有什么意义???
                    TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!ringThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }

                while (!ringThreadToStop) {

                    try {
                        // second data
                        //这样也是不是存在上一个刻度其实刚处理完,他就给塞进来一个
                        List<Integer> ringItemData = new ArrayList<>();
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
                            List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                            if (tmpData != null) {
                                ringItemData.addAll(tmpData);
                            }
                        }

                        // ring trigger
                        logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                        if (ringItemData.size() > 0) {
                            // do trigger
                            for (int jobId: ringItemData) {
                                // do trigger
                                JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null);
                            }
                            // clear
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
                        if (!ringThreadToStop) {
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }

                    // next second, align second
                    try {
                        //如果整个循环不超过1s,不会出啥问题
                        //如果循环1-2s内,下次循环会把上一次的补齐
                        //如果循环超过2s,会漏掉任务,在下一分钟的时候补齐
                        //睡眠到下一秒
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });

任务触发

实现类:com.xxl.job.admin.core.thread.JobTriggerPoolHelper。框架分了两个触发线程池,一个是快触发线程池,一个是慢触发线程池。如果在一分钟内这个任务超过十次触发性能在500ms以上,就会被扔到慢触发线程池中运行。否则就会被扔到快触发线程池中。
调度过程就很简单,会读取xxlJobInfo中的任务信息,以及xxlJobGroup中的存活机器,根据调度策略选择机器,通过xxl-rpc调用客户端的com.xxl.job.core.biz.impl.ExecutorBizImpl#run方法进行任务触发。在触发前会生成一条xxlJobLog存入数据库,触发完成后update该数据存入调度日志。
所以,每一次调度都会存入表一行数据,需要关注mysql的单表性能上限。如果在日志保存时间内调度次数过多会导致性能问题。

任务失败监控

实现类:com.xxl.job.admin.core.thread.JobFailMonitorHelper。
通过读取xxlJobLog表中失败的记录,进行任务重试和监控报警。很简单的模块,不细讲了。

任务调度日志报告及清理

实现类:com.xxl.job.admin.core.thread.JobLogReportHelper。
负责统计每天的任务调度次数存到XxlJobLogReport表中并且兼职清理过期的日志。任务调度报告统计主要用于首页的调度次数统计展示,如果每次访问读取xxlJobLog表会有性能问题,所以他做了一个天维度的统计,来避免管理端访问会影响集群调度性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值