本文基于 Elastic-Job V2.1.5 版本分享
1. 概述
本文主要分享 Elastic-Job-Cloud 调度主流程。对应到 Elastic-Job-Lite 源码解析文章如下:
《Elastic-Job-Lite 源码分析 —— 作业初始化》
《Elastic-Job-Lite 源码分析 —— 作业执行》
《Elastic-Job-Lite 源码分析 —— 作业分片》
如果你阅读过以下文章,有助于对本文的理解:
另外,笔者假设你已经对 《Elastic-Job-Lite 源码分析系列》 有一定的了解。
本文涉及到主体类的类图如下( 打开大图 ):
Elastic-Job-Cloud 基于 Mesos 实现分布式作业调度,或者说 Elastic-Job-Cloud 是 Mesos 上的 框架( Framework )。
一个 Mesos 框架由两部分组成:
Elastic-Job-Cloud 由两个项目组成:
2. 作业执行类型
在 Elastic-Job-Cloud,作业执行分成两种类型:
常驻作业是作业一旦启动,无论运行与否均占用系统资源; 常驻作业适合初始化时间长、触发间隔短、实时性要求高的作业,要求资源配备充足。
瞬时作业是在作业启动时占用资源,运行完成后释放资源。
瞬时作业适合初始化时间短、触发间隔长、允许延迟的作业,一般用于资源不太充分,或作业要求的资源多,适合资源错峰使用的场景。
Elastic-Job-Cloud 不同于 Elastic-Job-Lite 去中心化执行调度,转变为 Mesos Framework 的中心节点调度。这里不太理解,没关系,下文看到具体代码就能明白了。
常驻作业、瞬时作业在调度中会略有不同,大体粗略流程如下:
下面,我们针对每个过程一节一节解析。
3. Producer 发布任务
在上文《Elastic-Job-Cloud 源码分析 —— 作业配置》的「3.1.1 操作云作业配置」可以看到添加云作业配置后,Elastic-Job-Cloud-Scheduler 会执行作业调度,实现代码如下:
// ProducerManager.java
/**
* 调度作业.
*
* @param jobConfig 作业配置
*/
public void schedule(final CloudJobConfiguration jobConfig) {
// 应用 或 作业 被禁用,不调度
if (disableAppService.isDisabled(jobConfig.getAppName()) || disableJobService.isDisabled(jobConfig.getJobName())) {
return;
}
if (CloudJobExecutionType.TRANSIENT == jobConfig.getJobExecutionType()) { // 瞬时作业
transientProducerScheduler.register(jobConfig);
} else if (CloudJobExecutionType.DAEMON == jobConfig.getJobExecutionType()) { // 常驻作业
readyService.addDaemon(jobConfig.getJobName());
}
}
3.1 常驻作业
常驻作业在调度时,直接添加到待执行作业队列。What?岂不是马上就运行了!No No No,答案在「5. TaskExecutor 执行任务」,这里先打住。
// ReadyService.java
/**
* 将常驻作业放入待执行队列.
*
* @param jobName 作业名称
*/
public void addDaemon(final String jobName) {
if (regCenter.getNumChildren(ReadyNode.ROOT) > env.getFrameworkConfiguration().getJobStateQueueSize()) {
log.warn("Cannot add daemon job, caused by read state queue size is larger than {}.", env.getFrameworkConfiguration().getJobStateQueueSize());
return;
}
Optional cloudJobConfig = configService.load(jobName);
if (!cloudJobConfig.isPresent() || CloudJobExecutionType.DAEMON != cloudJobConfig.get().getJobExecutionType() || runningService.isJobRunning(jobName)) {
return;
}
// 添加到待执行队列
regCenter.persist(ReadyNode.getReadyJobNodePath(jobName), "1");
}
// ReadyNode.java
final class ReadyNode {
static final String ROOT = StateNode.ROOT + "/ready";
private static final String READY_JOB = ROOT + "/%s"; // %s = ${JOB_NAME}
}
[zk: localhost:2181(CONNECTED) 4] ls /elastic-job-cloud/state/ready
[test_job_simple]
[zk: localhost:2181(CONNECTED) 5] get /elastic-job-cloud/state/ready/test_job_simple
1
FROM http://elasticjob.io/docs/elastic-job-cloud/03-design/roadmap/
Redis Based Queue Improvement
3.2 瞬时作业
瞬时作业在调度时,使用发布瞬时作业任务的调度器( TransientProducerScheduler )调度作业。当瞬时作业到达作业执行时间,添加到待执行作业队列。
3.2.1 TransientProducerScheduler
TransientProducerScheduler,发布瞬时作业任务的调度器,基于 Quartz 实现对瞬时作业的调度。初始化代码如下:
// TransientProducerScheduler.java
void start() {
scheduler = getScheduler();
try {
scheduler.start();
} catch (final SchedulerException ex) {
throw new JobSystemException(ex);
}
}
private Scheduler getScheduler() {
StdSchedulerFactory factory = new StdSchedulerFactory();
try {
factory.initialize(getQuartzProperties());
return factory.getScheduler();
} catch (final SchedulerException ex) {
throw new JobSystemException(ex);
}
}
private Properties getQuartzProperties() {
Properties result = new Properties();
result.put("org.quartz.threadPool.class", SimpleThreadPool.class.getName());
result.put("org.quartz.threadPool.threadCount", Integer.toString(Runtime.getRuntime().availableProcessors() * 2)); // 线程池数量
result.put("org.quartz.scheduler.instanceName", "ELASTIC_JOB_CLOUD_TRANSIENT_PRODUCER");
result.put("org.quartz.plugin.shutdownhook.class", ShutdownHookPlugin.class.getName());
result.put("org.quartz.plugin.shutdownhook.cleanShutdown", Boolean.TRUE.toString());
return result;
}
3.2.2 注册瞬时作业
调用 TransientProducerScheduler#register(…) 方法,注册瞬时作业。实现代码如下:
// TransientProducerScheduler.java
private final TransientProducerRepository repository;
synchronized void register(final CloudJobConfiguration jobConfig) {
String cron = jobConfig.getTypeConfig().getCoreConfig().getCron();
// 添加 cron 作业集合
JobKey jobKey = buildJobKey(cron);
repository.put(jobKey, jobConfig.getJobName());
// 调度 作业
try {
if (!scheduler.checkExists(jobKey)) {
scheduler.scheduleJob(buildJobDetail(jobKey), buildTrigger(jobKey.getName()));
}
} catch (final SchedulerException ex) {
throw new JobSystemException(ex);
}
}
final class TransientProducerRepository {
/**
* cron 作业集合
* key:作业Key
*/
private final ConcurrentHashMap<JobKey, List<String>> cronTasks = new ConcurrentHashMap<>(256, 1);
synchronized void put(final JobKey jobKey, final String jobName) {
remove(jobName);
List<String> taskList = cronTasks.get(jobKey);
if (null == taskList) {
taskList = new CopyOnWriteArrayList<>();
taskList.add(jobName);
cronTasks.put(jobKey, taskList);
return;
}
if (!taskList.contains(jobName)) {
taskList.add(jobName);
}
}
}
private JobDetail buildJobDetail(final JobKey jobKey) {
JobDetail result = JobBuilder.newJob(ProducerJob.class) // ProducerJob.java
.withIdentity(jobKey).build();
result.getJobDataMap().put("repository", repository);
result.getJobDataMap().put("readyService", readyService);
return result;
}
JobBuilder#newJob(…) 的参数是 ProducerJob,下文会讲解到。
3.2.3 ProducerJob
ProducerJob,当 Quartz Job 到达 cron 执行时间( 即作业执行时间),将相应的瞬时作业添加到待执行作业队列。实现代码如下:
public static final class ProducerJob implements Job {
private TransientProducerRepository repository;
private ReadyService readyService;
@Override
public void execute(final JobExecutionContext context) throws JobExecutionException {
List jobNames = repository.get(context.getJobDetail().getKey());
for (String each : jobNames) {
readyService.addTransient(each);
}
}
}
final class TransientProducerRepository {
/**
* cron 作业集合
* key:作业Key
*/
private final ConcurrentHashMap<JobKey, List<String>> cronTasks = new ConcurrentHashMap<>(256, 1);
List<String> get(final JobKey jobKey) {
List<String> result = cronTasks.get(jobKey);
return null == result ? Collections.<String>emptyList() : result;
}
}
/**
* 将瞬时作业放入待执行队列.
*
* @param jobName 作业名称
*/
public void addTransient(final String jobName) {
//
if (regCenter.getNumChildren(ReadyNode.ROOT) > env.getFrameworkConfiguration().getJobStateQueueSize()) {
log.warn("Cannot add transient job, caused by read state queue size is larger than {}.", env.getFrameworkConfiguration().getJobStateQueueSize());
return;
}
//
Optional cloudJobConfig = configService.load(jobName);
if (!cloudJobConfig.isPresent() || CloudJobExecutionType.TRANSIENT != cloudJobConfig.get().getJobExecutionType()) {
return;
}
//
String readyJobNode = ReadyNode.getReadyJobNodePath(jobName);
String times = regCenter.getDirectly(readyJobNode);
if (cloudJobConfig.get().getTypeConfig().getCoreConfig().isMisfire()) {
regCenter.persist(readyJobNode, Integer.toString(null == times ? 1 : Integer.parseInt(times) + 1));
} else {
regCenter.persist(ReadyNode.getReadyJobNodePath(jobName), "1");
}
}
添加瞬时作业到待执行作业队列 和 添加常驻作业到待执行作业队列基本是一致的。
当作业配置允许 misfire,则不断累积作业可执行次数。
3.3 小结
无论是常驻作业还是瞬时作业,都会加入到待执行作业队列。目前我们看到瞬时作业的每次调度是TransientProducerScheduler 负责。那么常驻作业的每次调度呢?「5. TaskExecutor 执行任务」会看到它的调度,这是 Elastic-Job-Cloud 设计巧妙有趣的地方。
在此我向大家推荐一个架构学习交流群。交流学习群号:993070439 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系,还能领取免费的学习资源。