1 elastic-job
ElasticJob-job 定位为轻量级无中心化解决方案,使用 jar 的形式提供分布式任务的协调服务。
应用内部定义任务类,实现SimpleJob接口,编写自己任务的实际业务流程即可。
ruby
public class MyElasticJob implements SimpleJob {
@Override
public void execute(ShardingContext context) {
switch (context.getShardingItem()) {
case 0:
// do something by sharding item 0
break;
case 1:
// do something by sharding item 1
break;
case 2:
// do something by sharding item 2
break;
// case n: ...
}
}
}
举例:应用A有五个任务需要执行,分别是A,B,C,D,E。任务E需要分成四个子任务,应用部署在两台机器上。
应用A在启动后, 5个任务通过 Zookeeper 协调后被分配到两台机器上,通过Quartz Scheduler 分开执行不同的任务。
ElasticJob 从本质上来讲 ,底层任务调度还是通过 Quartz ,相比Redis分布式锁 或者 Quartz 分布式部署 ,它的优势在于可以依赖 Zookeeper 这个大杀器 ,将任务通过负载均衡算法分配给应用内的 Quartz Scheduler容器。
从使用者的角度来讲,是非常简单易用的。但从架构来看,调度器和执行器依然在同一个应用方JVM内,而且容器在启动后,依然需要做负载均衡。应用假如频繁的重启,不断的去选主,对分片做负载均衡,这些都是相对比较重的操作。
另外,ElasticJob 的控制台是比较粗糙的,通过读取注册中心数据展现作业状态,更新注册中心数据修改全局任务配置。
2 XXL-JOb
XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
我们重点剖析下架构图 :
▍ 网络通讯 server-worker 模型
调度中心和执行器 两个模块之间通讯是 server-worker 模式。调度中心本身就是一个SpringBoot 工程,启动会监听8080端口。
执行器启动后,会启动内置服务( EmbedServer )监听9994端口。这样双方都可以给对方发送命令。
那调度中心如何知道执行器的地址信息呢 ?上图中,执行器会定时发送注册命令 ,这样调度中心就可以获取在线的执行器列表。
通过执行器列表,就可以根据任务配置的路由策略选择节点执行任务。常见的路由策略有如下三种:
- 随机节点执行:选择集群中一个可用的执行节点执行调度任务。适用场景:离线订单结算。
-
广播执行:在集群中所有的执行节点分发调度任务并执行。适用场景:批量更新应用本地缓存。
-
分片执行:按照用户自定义分片逻辑进行拆分,分发到集群中不同节点并行执行,提升资源利用效率。适用场景:海量日志统计。
▍ 调度器
调度器是任务调度系统里面非常核心的组件。XXL-JOB 的早期版本是依赖Quartz。
但在v2.1.0版本中完全去掉了Quartz的依赖,原来需要创建的 Quartz表也替换成了自研的表。
核心的调度类是:JobTriggerPoolHelper 。调用start方法后,会启动两个线程:scheduleThread 和 ringThread 。
首先 scheduleThread 会定时从数据库加载需要调度的任务,这里从本质上还是基于数据库行锁保证同时只有一个调度中心节点触发任务调度。
ini
Connection 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();
# 触发任务调度 (伪代码)
for (XxlJobInfo jobInfo: scheduleList) {
// 省略代码
}
# 事务提交
conn.commit();
调度线程会根据任务的「下次触发时间」,采取不同的动作:
已过期的任务需要立刻执行的,直接放入线程池中触发执行 ,五秒内需要执行的任务放到 ringData 对象里。
ringThread 启动后,定时从 ringData 对象里获取需要执行的任务列表 ,放入到线程池中触发执行。
# 3 总结
首先我们将任务调度开源产品和商业产品 SchedulerX 放在一起,生成一张对照表:
Quartz 和 ElasticJob从本质上还是属于框架的层面。
中心化产品从架构上来讲更加清晰,调度层面更灵活,可以支持更复杂的调度(mapreduce动态分片,工作流)。
XXL-JOB 从产品层面已经做到极简,开箱即用,调度模式可以满足大部分研发团队的需求。简单易用 + 能打,所以非常受大家欢迎。
其实每个技术团队的技术储备不尽相同,面对的场景也不一样,所以技术选型并不能一概而论。
不管是使用哪种技术,在编写任务业务代码时,还是需要注意两点:
- 幂等。当任务被重复执行的时候,或者分布式锁失效的时候,程序依然可以输出正确的结果;
- 任务不跑了,千万别惊慌。查看调度日志,JVM层面使用Jstack命令查看堆栈,网络通讯要添加超时时间 ,一般能解决大部分问题。