新的一年、旧的方式,这一次就从一个需求开发的角度和大家分享监控系统的开发。
前段时间与大家分享了定时任务调用平台xxl-job,也简单地讲了讲平台的结构模式、调度方法。
【进阶之路】定时任务调用平台xxl-job
调用任务的过程中,如果xxl-job的代码能够顺利执行,但是本身需要执行的任务没有顺利执行成功,或者因为一些问题导致任务延迟执行甚至没有执行,xxl-job并不会正常报错通知。这个时候,我们就需要用一些其他的方法来协助监控定时任务的执行。
在大佬的要求下,我这边设计了一个方案,如图所示:
定时任务监控体系分为三个部分(其实如果将消息中间件换成异步请求也可以,只是在处理任务比较多又比较集中的时候,对监控系统的压力比较大,监控系统本身业务无关,是不应该占用过多的系统资源的)。
一、定时任务执行系统
代码语言:javascript复制/**
* 根据业务需求、需要对之前的业务进行埋点处理,主要是对定时任务的场景进行处理,
这边采取的方法是结构型设计模式,尽量依托原本的功能、减少代码侵入,
使得消息实现与通知与原本的业务内容少耦合
**/
@Slf4j
public abstract class AbstractTestFileComponent implements ITestFileComponent, TaskWarnService {
//单纯的MQ推送
@Autowired
private QueueSender queueSender;
@Override
public boolean dealFile(Object obj){
//1、记录任务耗时
StopWatch sw = new StopWatch();
sw.start();
try {
//2、这里生成通知信息 dealExe指的是原本执行任务的模块
TaskDetailsDto taskDtl = this.dealExe(obj);
taskDtl.setParcTime(DateUtil.getFormatDate());
taskDtl.setStatus(1);
sw.split();
taskDtl.setConsTime(new Long(sw.getSplitTime()).intValue());
// 3、需要提交时,在子类重写些方法 并填充code 并发送
this.submitResult(taskDtl);
return true;
} catch(ServiceException e){
//记录日志、并且在任务报错的时候记录信息
log.error(this.getMethodDesc(), obj, e);
TaskDetailsDto taskDtl =new TaskDetailsDto();
taskDtl.setParcTime(DateUtil.getFormatDate());
taskDtl.setStatus(3);
sw.split();
taskDtl.setConsTime(new Long(sw.getSplitTime()).intValue());
this.submitResult(taskDtl);
}
return true;
}
@Override
public void submitResult(TaskDetailsDto taskDtl) {
//4、在子类补完信息
this.fillTaskCode(taskDtl);
if (!TextUtils.isEmpty(taskDtl.getIdentify())) {
// MQ发送信息、根据业务要求、如果没用规定执行时间则取当天日期
if (Objects.isNull(taskDtl.getOpeDate())){
taskDtl.setOpeDate(DateUtil.getDate(new Date()));
}
queueSender.send("loanMonitor.taskWarn", JSON.toJSONString(taskDtl));
log.info("loanMonitor.taskWarn mq send taskDetailsDto:{}",taskDtl);
} else {
log.info("taskDtl.getIdentify is null,TaskDetailsDto:{}", taskDtl);
}
}
@Override
public TaskDetailsDto fillTaskCode(TaskDetailsDto taskDtl) {
//在子类中确定执行任务信息、执行时间与主键ID
taskDtl.setGeneralSts(msg);
taskDtl.setIdentify(id);
taskDtl.setOpeDate(date);
log.info("taskDtl:{}",taskDtl);
return taskDtl;
}
public abstract TaskDetailsDto dealExe(String obj);
public abstract String getMethodDesc();
}
public interface TaskWarnService {
/**
* 这里可以理解为获取任务的ID、执行内容等数据、可以在代码中实现
*/
TaskDetailsDto fillTaskCode(TaskDetailsDto taskDtl);
/**
* 上送事件 警告单
* @param taskDtl
*/
void submitResult(TaskDetailsDto taskDtl);
}
执行任务模式,我采用的是桥接模式,将抽象的类与实现类分离,使它们可以独立变化。我们在自己设计的过程中,也可以根据不同的情况采用不同的方法来实现信息推送的功能。
在此模块中,主要目的是要能够准确的获取任务执行情况,然后将任务推送给指定的MQ,内部记录的数据可以根据自己的要求来确定,但是不推荐将那种一天内需要非常多次轮训的任务也进行监控。
二、定时任务监控系统
定时任务监控系统中,主要需要实现以下几个功能:
- 1、接受并处理由MQ中分配而来的任务,包括执行失败时进行通知需要通知的人
- 2、处理在应该收到通知的时没有收到通知的任务
- 3、根据要求生成需要通知的任务清单,用以判断需要执行的任务是否正确、按时、高效的执行
- 4、漂亮的展示页面,可以清晰地展示任务是否处理完毕
1、处理任务
代码语言:javascript复制//首先要获取到MQ的信息,在springboot中可快捷的实现
@JmsListener(destination = "loanMonitor.taskWarn")
public void dealTaskJob(String data) {
TaskDetailsDto taskDetailsDto = JacksonUtils.jsonToObject(data, TaskDetailsDto.class);
//1、参数判断、可以参考我的开源项目温蒂,这个方法就是复用了wendi(温蒂)中的参数处理方法、可以根据不同情况处理不同的需求
checkParamTaskDetails(taskDetailsDto);
log.info("dealTaskJobMq已经启动,Identify:{}", taskDetailsDto.getIdentify());
//2、查询数据库、在各个实际接口中查询是否新增了登记任务、taskTempDto为所执行任务的任务清单
TaskTempDto taskTempDto = new TaskTempDto();
taskTempDto.setIdentify(taskDetailsDto.getIdentify());
taskTempDto = taskTempService.queryByIdentify(taskTempDto);
TaskDetailsDto queryTaskJob = taskDetailsService.queryByIdDate(taskDetailsDto);
/*
3、无需执行的情况:
I、是否需要操作为否 。
II、结果已经处理 。
III、已经报警且结果为正常或者过期、同时执行时间不为空
IV、重复报警通知
*/
if (无需执行情况)
return;
}
//4、如果状态不是正常执行,直接报警
if (条件1) {
//调用邮箱通知
//调用短信通知
//5、如果状态是正常执行,则判断是否需要报警同时
} else if (条件2) {
//根据不同的任务,执行不同的操作、具体由taskTempDto里的数据来确定
}
log.info("dealTaskJobMq执行:{},Identify:{}", b, taskDetailsDto.getIdentify());
}
2、生成清单任务与处理未执行的任务
这一点我主要考虑使用定时任务来解决问题,而且不需要考虑再次监控的问题(不然就无限套娃了)。
生成清单的时候,要考虑不同任务的场景,比如有的任务是一天分批次执行(比如一些批量任务),有的任务是每天执行一次(比如对账任务),还有的任务是几天甚至更长时间才执行一次(比如周的差错)。这个时候,生成任务清单,包括处理任务清单的时候也需要考虑到,这里我就自己的任务给大家分享一下我的任务清单。
代码语言:javascript复制@Data
public class TaskTempDto implements Serializable {
/**
* 主键
*/
private Integer id;
/**
*任务包名称,主要展示用
*/
private String taskName;
/**
*标识、也是每个任务的主键
*/
private String identify;
/**
*类型(1重复/2动态/3周/4月)
*不同的类型决定不同的场景,一般可以采用枚举的方式
*/
private Enum status;
/**
*归属模块、分类
*/
private String moudule;
/**
*是否需要报警 1、报警 2不报警、可以决定是否需要被通知
*/
private Integer alertor;
/**
*是否需要操作 1是 2否、决定任务是否需要执行
*/
private Integer operate;
/**
* 计划执行时间
*/
private String schdTime;
/**
* 执行延时范围、可以用来监控执行是否超时
*/
private Integer rangTime;
/**
* 邮箱,配置通知邮箱
*/
private String mail;
/**
* 配置通知的手机短信,这里可以用[],也可以直接分割
*/
private String mobile;
/**
*操作日期,决定通知来的日期是否正确
*/
private String opeDate;
/**
*操作人
*/
private String oper;
}
3、漂亮的展示与操作页面
众所周知,如果没有一个漂亮并且看让人看一眼就会能完全掌握使用方法的页面,那就代表开发人员需要自己来操作进行模板数据的增减修改,这无疑是很不可取的,所以,一个完善的任务监控系统也需要一个完善的UI控制界面,不仅方便运维人员操作,也可以清晰地展示每个任务的执行情况与执行效率,报警的任务需要负责人员进行处理并手动解除警报,这样,一个土生土长地任务监控系统就完成了。
三、开发过程中遇到的一些问题与经验
1、不一定非要使用MQ
如果任务量小、且多为单机任务单、亦或是项目中没有消息中间件的话,可以尝试使用http请求(针对非分布式)或者声明式的web service(feign),这样只需要将监控系统部署在私服中,再引入需要的项目中即可。
在定时任务执行成功之后,开启一个线程来调用就能解决问题。当然,在设计之初我也考虑了这个问题,所以预留了接口有备无患。
2、业务过于耦合
因为业务过于耦合的问题,可以考虑使用切面进行开发,不过目前的线上的定时任务并不需要24小时执行,所以我没有选择这个方案(偷懒了),但是在开发前期也在部分接口中使用了切面进行开发。
对比桥接模式,切面的开发方法对于代码的侵入大幅下降,但是代码的复用性会降低,因为针对不同的任务需要考虑不同的执行方案。
同时,因为使用切面难以对复杂的定时任务项目进行开发: 业务并不是二极管,只有成功与失败,还有各种各样的情况,就拿我熟悉的还款计划来说,有计划已经执行,有计划改变,甚至某一条计划出现问题,这些情况一样是执行成功,但是使用切面方法就很难掌握具体情况了。
3、任务执行情况的判断
什么时候需要报警,这是一个问题。 之前在代码中,我设计了以下情况作直接忽略报警,而且都是在实际的生产中遇到的一些需要忽略警情的问题:
- I、是否需要操作、是否需要报警为否 。
- II、结果已经处理 。
- III、已经报警且结果为正常或者过期、同时执行时间不为空
- IV、重复报警通知。