xxl-job 的调度中心详解:
调度中心的核心代码即为 XxlJobScheduler 类中初始化的几个线程池,下面进行详细的介绍:
XxlJobAdminConfig 类:
// 配置类属性注入完成后,执行初始化方法
@Override
public void afterPropertiesSet() throws Exception {
adminConfig = this;
xxlJobScheduler = new XxlJobScheduler();
xxlJobScheduler.init();
}
XxlJobScheduler 类:
public void init() throws Exception {
initI18n();
// 开启两个线程池: fastTriggerPool 和 slowTriggerPool
// 优先fast,如果一分钟内任务超过 10 次执行时间超过 500ms,则交给 slow 执行。
JobTriggerPoolHelper.toStart();
// 开启一个线程池 registryOrRemoveThreadPool
// 开启一个守护线程用于动态维护执行器的注册表信息
JobRegistryHelper.getInstance().start();
// 开启一个守护线程用于失败重试
JobFailMonitorHelper.getInstance().start();
// 开启一个线程池 callbackThreadPool
// 开启一个守护线程进行任务治理,对超时任务标记为失败
JobCompleteHelper.getInstance().start();
// 开启一个守护线程 logrThread,维护日志报表信息
JobLogReportHelper.getInstance().start();
// 开启两个守护线程:scheduleThread 和 ringThread
JobScheduleHelper.getInstance().start();
}
我们详细分析一下 JobScheduleHelper 部分的代码,先通过源码分析一下 scheduleThread 做了什么:
scheduleThread = new Thread(new Runnable() {
@Override
public void run() {
// 等待服务启动
try {
// 线程休眠 5s,取余操作是使线程的执行与每秒钟的整数倍对齐。
TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
// 计算每次批量调度任务数量
// 其中 fast + slow 默认有 300 个核心线程,每个触发器花费时间为 50 ms,则每个线程单位时间内可处理 1000 / 50 = 20 个任务
// 批量调度任务数量默认为 300 * 20 = 6000 个
int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
while (!scheduleThreadToStop) {
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();
// 开启事务
long nowTime = System.currentTimeMillis();
// 查询未来 5s 内要执行的任务
List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
if (scheduleList!=null && scheduleList.size()>0) {
for (XxlJobInfo jobInfo: scheduleList) {
if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
// 过期时间超过 5s 的任务
MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
}
refreshNextValidTime(jobInfo, new Date());
} else if (nowTime > jobInfo.getTriggerNextTime()) {
// 过期时间在 5s 内的任务
JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
refreshNextValidTime(jobInfo, new Date());
if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
pushTimeRing(ringSecond, jobInfo.getId());
refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
}
} else {
// 还未过期并将在未来 5s 内执行的任务
int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
pushTimeRing(ringSecond, jobInfo.getId());
refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
}
}
// 上述代码片段详细分析看后续的流程图
for (XxlJobInfo jobInfo: scheduleList) {
XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
}
} else {
preReadSuc = false;
}
} catch (Exception e) {
if (!scheduleThreadToStop) {
// 错误处理
}
} finally {
// 提交事务,关闭资源
}
long cost = System.currentTimeMillis()-start;
if (cost < 1000) {
try {
// 如果执行成功,下一秒继续执行。执行失败或没查询出数据,则 5s 执行一次。
TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
}
}
});
在分析上述代码时,未过期且在未来 5s 内将要执行的任务将会放入 ringMap 中,其中 ringThread 就是周期性的从 map 中获取任务并执行,下面通过代码详细分析:
ringThread = new Thread(new Runnable() {
@Override
public void run() {
while (!ringThreadToStop) {
try {
// 线程休眠 1s,取余操作是使线程的执行与每秒钟的整数倍对齐。
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
try {
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);
}
}
if (ringItemData.size() > 0) {
for (int jobId: ringItemData) {
JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
}
ringItemData.clear();
}
} catch (Exception e) {
if (!ringThreadToStop) {
logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
}
}
}
}
});
xxl-job 的执行器详解:
xxl-job 的架构比较清晰,由调度中心+执行器组成。
在之前中我们对调度中心进行了流程分析,下面我们通过源码来分析执行器,这里重点关注 jobHandlerRepository 是什么时候被插入数据的。
在执行器写入业务代码有三种方式:实现类模式,注解模式,GLUE 模式。
- 实现类模式:继承
IJobHandler这个抽象类,实现其中方法。然后手动注册至框架中XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler()) - 注解模式:业务方法上添加
@XxlJob(value = "业务jobHandler名称", init = "初始化方法", destroy = "销毁方法")注解。(注解后续本质也是调用 registJobHandler) - GLUE 模式:业务代码维护在调度中心,支持在线 IDE 动态修改,实时编译和生效。
1、实现类模式流程分析
开发一个继承自 IJobHandler 的类 DemoJobHandler,实现其中 execute、init、destroy 方法,然后手动通过如下方式注入到执行器容器中:XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
// 注册进 concurrentHashMap 以便后续使用
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler) {
return jobHandlerRepository.put(name, jobHandler);
}
2、注解模式流程分析
XxlJobSpringExecutor 实现了 SmartInitializingSingleton 接口,该接口中的afterSingletonsInstantiated() 方法将会在所有的非惰性单实例 Bean 初始化完成之后进行回调。
@Override
public void afterSingletonsInstantiated() {
initJobHandlerMethodRepository(applicationContext);
GlueFactory.refreshInstance(1);
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
其中 initJobHandlerMethodRepository 方法就是获取所有 Bean 中方法判断是否存在 @XxlJob 注解,如果存在则调用registJobHandler(xxlJob, bean, executeMethod)方法进行注册。
注册部分源码:
// 省略通过反射获取方法实例的代码
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
// 注册进 concurrentHashMap 以便后续使用
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
return jobHandlerRepository.put(name, jobHandler);
}
其中 name 是业务 jobHandler 名称,jobHandler 包含相应的 execute()、init()、destroy() 的方法实例,后续可以通过反射调用。
注解模式中 IJobHandler 抽象类的实现类为 MethodJobHandler:
@Override
public void execute() throws Exception {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length > 0) {
method.invoke(target, new Object[paramTypes.length]); // method-param can not be primitive-types
} else {
method.invoke(target);
}
}
@Override
public void init() throws Exception {
if(initMethod != null) {
initMethod.invoke(target);
}
}
@Override
public void destroy() throws Exception {
if(destroyMethod != null) {
destroyMethod.invoke(target);
}
}
3、GLUE 模式流程分析
xxl-job 调度中心推送任务给执行器:
手动触发、cron 触发、固定周期触发最终都会交给 fast 和 slow 线程池进行任务推送。
public void addTrigger(final int jobId,
final TriggerTypeEnum triggerType,
final int failRetryCount,
final String executorShardingParam,
final String executorParam,
final String addressList) {
// 优先fast,如果一分钟内任务超过 10 次执行时间超过 500ms,则交给 slow 执行。
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
triggerPool_ = slowTriggerPool;
}
triggerPool_.execute(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
try {
// 进行任务推送
XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
long minTim_now = System.currentTimeMillis()/60000;
if (minTim != minTim_now) {
minTim = minTim_now;
jobTimeoutCountMap.clear();
}
long cost = System.currentTimeMillis()-start;
if (cost > 500) {
AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
if (timeoutCount != null) {
timeoutCount.incrementAndGet();
}
}
}
}
});
}
xxl-job-admin 模块,XxlJobTrigger 类:
public static void trigger(int jobId,
TriggerTypeEnum triggerType,
int failRetryCount,
String executorShardingParam,
String executorParam,
String addressList) {
// load data
XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
if (jobInfo == null) {
logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
return;
}
if (executorParam != null) {
jobInfo.setExecutorParam(executorParam);
}
int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());
// cover addressList
if (addressList!=null && addressList.trim().length()>0) {
group.setAddressType(1);
group.setAddressList(addressList.trim());
}
int[] shardingParam = null;
if (executorShardingParam!=null){
String[] shardingArr = executorShardingParam.split("/");
if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
shardingParam = new int[2];
shardingParam[0] = Integer.valueOf(shardingArr[0]);
shardingParam[1] = Integer.valueOf(shardingArr[1]);
}
}
// 判断路由策略是否是分片广播
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
&& group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
&& shardingParam==null) {
for (int i = 0; i < group.getRegistryList().size(); i++) {
processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
}
} else {
if (shardingParam == null) {
shardingParam = new int[]{0, 1};
}
processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
}
}
路由策略如下:
- 第一个(FIRST):当选择该策略时,会选择执行器注册地址的第一台机器执行,如果第一台机器出现故障,则调度任务失败。
- 最后一个(LAST):当选择该策略时,会选择执行器注册地址的第二台机器执行,如果第二台机器出现故障,则调度任务失败。
- 轮询(ROUND):当选择该策略时,会按照执行器注册地址轮询分配任务,如果其中一台机器出现故障,调度任务失败,任务不会转移。
- 随机(RANDOM):当选择该策略时,会按照执行器注册地址随机分配任务,如果其中一台机器出现故障,调度任务失败,任务不会转移。
- 一致性HASH(CONSISTENT_HASH):当选择该策略时,每个任务按照Hash算法固定选择某一台机器。如果那台机器出现故障,调度任务失败,任务不会转移。
- 最不经常使用(LFU):当选择该策略时,会优先选择使用频率最低的那台机器,如果其中一台机器出现故障,调度任务失败,任务不会转移。
- 最近最久未使用(LRU):当选择该策略时,会优先选择最久未使用的机器,如果其中一台机器出现故障,调度任务失败,任务不会转移。
- 故障转移(FAILOVER):当选择该策略时,按照顺序依次进行心跳检测,如果其中一台机器出现故障,则会转移到下一个执行器,若心跳检测成功,会选定为目标执行器并发起调度。
- 忙碌转移(BUSYOVER):当选择该策略时,按照顺序依次进行空闲检测,如果其中一台机器出现故障,则会转移到下一个执行器,若空闲检测成功,会选定为目标执行器并发起调度。
- 分片广播(SHARDING_BROADCAST):当选择该策略时,广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务。如果其中一台机器出现故障,则该执行器执行失败,不会影响其他执行器。
通过路由策略选择机器后发送 rpc 请求至执行器(xxl-job-core,ExecutorBizClient 类):
public class ExecutorBizClient implements ExecutorBiz {
public ExecutorBizClient() {
}
public ExecutorBizClient(String addressUrl, String accessToken) {
this.addressUrl = addressUrl;
this.accessToken = accessToken;
// valid
if (!this.addressUrl.endsWith("/")) {
this.addressUrl = this.addressUrl + "/";
}
}
private String addressUrl ;
private String accessToken;
private int timeout = 3;
@Override
public ReturnT<String> beat() {
return XxlJobRemotingUtil.postBody(addressUrl+"beat", accessToken, timeout, "", String.class);
}
@Override
public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam){
return XxlJobRemotingUtil.postBody(addressUrl+"idleBeat", accessToken, timeout, idleBeatParam, String.class);
}
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
}
@Override
public ReturnT<String> kill(KillParam killParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killParam, String.class);
}
@Override
public ReturnT<LogResult> log(LogParam logParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logParam, LogResult.class);
}
}
执行器这边会启动一个 netty 服务端来接受 rpc 请求,netty 服务的启动在 start 方法中:
@Override
public void afterSingletonsInstantiated() {
initJobHandlerMethodRepository(applicationContext);
GlueFactory.refreshInstance(1);
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void start() throws Exception {
// init logpath
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
// 初始化 netty 服务端
initEmbedServer(address, ip, port, appname, accessToken);
}
分析一下 netty 服务端的核心代码:
@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
String requestData = msg.content().toString(CharsetUtil.UTF_8);
String uri = msg.uri();
HttpMethod httpMethod = msg.method();
boolean keepAlive = HttpUtil.isKeepAlive(msg);
String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
bizThreadPool.execute(new Runnable() {
@Override
public void run() {
// 执行任务
Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);
String responseJson = GsonTool.toJson(responseObj);
// 响应客户端
writeResponse(ctx, keepAlive, responseJson);
}
});
}
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
if (HttpMethod.POST != httpMethod) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
}
if (uri == null || uri.trim().length() == 0) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
}
if (accessToken != null
&& accessToken.trim().length() > 0
&& !accessToken.equals(accessTokenReq)) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
}
// services mapping
try {
switch (uri) {
case "/beat":
return executorBiz.beat();
case "/idleBeat":
IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
case "/run":
TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
// 这里的 run 方法会从 jobHandlerRepository 中拿取对应方法实例进行调用
return executorBiz.run(triggerParam);
case "/kill":
KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
return executorBiz.kill(killParam);
case "/log":
LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
return executorBiz.log(logParam);
default:
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found.");
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
}
}
private void writeResponse(ChannelHandlerContext ctx, boolean keepAlive, String responseJson) {
// write response
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8)); // Unpooled.wrappedBuffer(responseJson)
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); // HttpHeaderValues.TEXT_PLAIN.toString()
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.writeAndFlush(response);
}