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表会有性能问题,所以他做了一个天维度的统计,来避免管理端访问会影响集群调度性能。

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

被折叠的 条评论
为什么被折叠?



