分布式定时任务系列6:XXL-job触发日志过大引发的CPU告警
xxl-job,日志过大,故障
目录
传送门
分布式定时任务系列5:XXL-job中blockingQueue的应用
问题出现
前几天上班的时候,收到运维的告警通知,安装XXL-job的服务器CPU飙高告警,让看一下。
查了下xxl-job-admin的日志文件,目录在logback.xml:
发现在告警时间点之前报了一个db事物超时的错误。
通过讯问得知,有人手动清理过任务,即将无效的任务手动删除了:在xxl-job控制台创建了几个任务,后来业务变更,把代码里面的任务删除了,但是控制台任务没有删除,所以做任务清理。以前面的例子来进行说明
场景说明
开发第一个jobHandler
@Component
public class JobHandler {
@XxlJob("first_job")
public void firstJob() {
System.out.println("========================= first job========================= ");
}
}
若以上配置成功,则可以在控制台看到注册的客户端机器
新建任务
测试任务
经过以上设置,在平台手动触发一次任务,并且可以查看执行日志
删除代码中的任务
执行一段时间后,代码中删除first_job,但是控制台任务不删除,执行一段时间后现删除。
但是这个正常的变更操作为什么会引起db出错呢?因为很有可能由于业务变动,先将代码中对应的任务删除的,而后再来清理XXL-job控制台的任务。
问题重现
要解决这个问题,首先还是要定位到原因。
结合报错日志,翻看了一下源码,发现从XXL-job控制台删除任务的逻辑:
- 界面删除选中的任务,发起URL请求:xxl-job-admin/jobinfo/remove
- 后端接口,开始同步处理,代码如下
- 跟踪代码,会发现问题代码出在清理任务对应的触发日志那个步骤
而这个Dao方法本身也平平无奇:
<delete id="delete" >
delete from xxl_job_log
WHERE job_id = #{jobId}
</delete>
那么到底是为什么这行sql会报错呢?
原因说穿了也很简单,因为这个任务对应的任务数据量太大了:
- xxl_job_log表,在经过系统接近半年运行,这张表已经存储超过了1kw+数据量,这对mysql来说单表的数据量算比较大了,甚至会造成从界面查询调度日志都成问题
- 单个任务删除时,在删除任务对应的调度日志时,数据量也是10w+(甚至可能更多,取决于配置的触发频率),执行这个sql的时候,对db来说不仅IO消耗大,且成了一个长事物,久久不能结束最后事物超时+CPU飙高
问题解决
对于调试日志太大的问题,官网已经给出了解决方案
手动清理
可以通过看源码,看出它是根据界面选择的清理策略,发起一个http同步请求,做循环删除,也是比较粗暴:
自动清理
原理就是启动异步线程,执行删除,具体可以看下源码:
启动自动清理任务入口
XXL-job控制台(xxl-job-admin这个服务)在启动的时候,会通过实现Spring的InitializingBean接口的方式来进行一些初始化的工作,其中就包括日志自动清理服务:
初始化自动清理任务
继续跟进com.xxl.job.admin.core.scheduler.XxlJobScheduler#init,这个方法就相当于整个XXL-job调试器的核心入口:
执行自动清理任务
而具体的日志清理执行,可以继续跟com.xxl.job.admin.core.thread.JobLogReportHelper#start:
public void start(){
logrThread = new Thread(new Runnable() {
@Override
public void run() {
// last clean log time
long lastCleanLogTime = 0;
while (!toStop) {
// 1、log-report refresh: refresh log report in 3 days
try {
for (int i = 0; i < 3; i++) {
// today
Calendar itemDay = Calendar.getInstance();
itemDay.add(Calendar.DAY_OF_MONTH, -i);
itemDay.set(Calendar.HOUR_OF_DAY, 0);
itemDay.set(Calendar.MINUTE, 0);
itemDay.set(Calendar.SECOND, 0);
itemDay.set(Calendar.MILLISECOND, 0);
Date todayFrom = itemDay.getTime();
itemDay.set(Calendar.HOUR_OF_DAY, 23);
itemDay.set(Calendar.MINUTE, 59);
itemDay.set(Calendar.SECOND, 59);
itemDay.set(Calendar.MILLISECOND, 999);
Date todayTo = itemDay.getTime();
// refresh log-report every minute
XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
xxlJobLogReport.setTriggerDay(todayFrom);
xxlJobLogReport.setRunningCount(0);
xxlJobLogReport.setSucCount(0);
xxlJobLogReport.setFailCount(0);
Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo);
if (triggerCountMap!=null && triggerCountMap.size()>0) {
int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0;
int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0;
int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0;
int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;
xxlJobLogReport.setRunningCount(triggerDayCountRunning);
xxlJobLogReport.setSucCount(triggerDayCountSuc);
xxlJobLogReport.setFailCount(triggerDayCountFail);
}
// do refresh
int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport);
if (ret < 1) {
XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport);
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e);
}
}
// 2、log-clean: switch open & once each day
if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0
&& System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) {
// expire-time
Calendar expiredDay = Calendar.getInstance();
expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays());
expiredDay.set(Calendar.HOUR_OF_DAY, 0);
expiredDay.set(Calendar.MINUTE, 0);
expiredDay.set(Calendar.SECOND, 0);
expiredDay.set(Calendar.MILLISECOND, 0);
Date clearBeforeTime = expiredDay.getTime();
// clean expired log
List<Long> logIds = null;
do {
logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
if (logIds!=null && logIds.size()>0) {
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds);
}
} while (logIds!=null && logIds.size()>0);
// update clean time
lastCleanLogTime = System.currentTimeMillis();
}
try {
TimeUnit.MINUTES.sleep(1);
} catch (Exception e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, job log report thread stop");
}
});
logrThread.setDaemon(true);
logrThread.setName("xxl-job, admin JobLogReportHelper");
logrThread.start();
}
大致逻辑就是以下步骤:
- 通过调用start()方法,开启一个线程(new Thread),并马上启动
- 通过一个while(true),类似死循环的方式,不停的执行刚才那个线程,除非被中止(toStop变为true)
- 首先进行3天内的日志统计,然后根据配置清理对应logretentiondays天以前的触发日志(一天触发一次)
而logretentiondays正是前面官网提到配置,具体的代码配置计算如下:
结束语
通过上面的源码及类图分析,可能看到XXL-job里面这种结合Spring的InitializingBean接口进行初始化+多线程的方式,在XXL-job源码里面极为普遍。下一节,就结合源码来分析一下XXL-job的任务触发,相对来说会比日志清理复杂许多,也是XXL-job的核心功能,里面会涉及到任务调度触发+服务注册+服务路由+远程Rpc调用,以及以前分析过的通过阻塞队列进行任务执行。
更多推荐
所有评论(0)