一、简介
Elastic-job是一个分布式调度解决方案,由2个相互独立的子项目Elastic-job-Lite和Elastic-Job-cloud组成;
Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务;Elastic-Job-Cloud采用当当网自研Mesos Framework的解决方案,额外提供资源治理,应用分发以及进程隔离等功能;
官方开源资料:https://github.com/dangdangdotcom/elastic-job
二、基本概念
我们开发定时任务一般都是使用quartz或者spring-task(ScheduledExecutorService),无论是使用quartz还是spring-task,我们都会至少遇到两个痛点:
1.不敢轻易跟着应用服务多节点部署,可能会重复多次执行而引发系统逻辑的错误。
2.quartz的集群仅仅只是用来单机锁表防止重复多次执行,节点数量的增加并不能给我们的每次执行效率带来提升,即不能实现水平扩展。
Elastic-Job主要的设计理念是无中心化的分布式定时调度框架,思路来源于Quartz的基于数据库的高可用方案。但数据库没有分布式协调功能,所以在高可用方案的基础上增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。
三、Elastic-Job-Lite原理
Elastic-Job在2.x之后,出了两个产品线:Elastic-Job-Lite和Elastic-Job-Cloud。我们一般使用Elastic-Job-Lite就能够满足需求,本文也是以Elastic-Job-Lite为主。1.x系列对应的就只有Elastic-Job-Lite,并且在2.x里修改了一些核心类名,差别虽大,原理类似,建议使用2.x系列
举个典型的job场景,比如余额宝里的昨日收益,系统需要job在每天某个时间点开始,给所有余额宝用户计算收益。如果用户数量不多,我们可以轻易使用quartz来完成,我们让计息job在某个时间点开始执行,循环遍历所有用户计算利息,这没问题。可是,如果用户体量特别大,我们可能会面临着在第二天之前处理不完这么多用户。另外,我们部署job的时候也得注意,我们可能会把job直接放在我们的webapp里,webapp通常是多节点部署的,这样,我们的job也就是多节点,多个job同时执行,很容易造成重复执行,比如用户重复计息,为了避免这种情况,我们可能会对job的执行加锁,保证始终只有一个节点能执行,或者干脆让job从webapp里剥离出来,独自部署一个节点。
Elastic-job就可以帮着我们解决上面的问题,Elastic底层的任务调度还是使用了quartz,通过zookkeeper来动态给job节点分片;
我们来看问题点:
1、存在大数据量的用户需要再特定的时间段内完成计息
我们肯定希望我们的任务可以通过集群达到水平扩展,集群里面的每一个节点都处理部分用户,不管用户数量有多大,我们只要增加机器就可以了,比如说单个机器在特定的时间内能处理N个用户的计息,2台机器处理2N个用户,3台3N….,再多的用户也不怕了。
使用elastic-job开发的作业都是zookeeper的客户端,比如我希望3台机器跑job,我们将任务分成3片,框架通过zk的协调,最终会让3台机器分别分配到0,1,2的任务片,比如:
server0–>0,server1–>1,server2–>2,
1、当server0执行时,可以只查询id%30的用户;
2、server1执行时,只查询id%31的用户;
3、server2执行时,只查询id%3==2的用户。
在上面的基础上,我们再增加server3,此时,server3分不到任务分片,因为只有3片,已经分完了。没有分到任务分片的作业程序将不执行。如果此时server2挂了,那么server2的分片项会分配给server3,server3有了分片,就会替代server2执行。如果此时server3也挂了,只剩下server0和server1了,框架也会自动把server3的分片随机分配给server0或者server1,可能会这样,server0–>0,server1–>1,2。这种特性称之为弹性扩容,即elastic-job名称的由来。
四、作业类型
elastic-job提供了三种类型的作业:Simple类型作业、Dataflow类型作业、Script类型作业。这里主要讲解前两者。Script类型作业意为脚本类型作业,支持shell,python,perl等所有类型脚本,使用不多,可以参见github文档。
SimpleJob需要实现SimpleJob接口,意为简单实现,未经过任何封装,与quartz原生接口相似,比如示例代码中所使用的job。
Dataflow类型用于处理数据流,需实现DataflowJob接口。该接口提供2个方法可供覆盖,分别用于抓取(fetchData)和处理(processData)数据。
可通过DataflowJobConfiguration配置是否流式处理。
流式处理数据只有fetchData方法的返回值为null或集合长度为空时,作业才停止抓取,否则作业将一直运行下去; 非流式处理数据则只会在每次作业执行过程中执行一次fetchData方法和processData方法,随即完成本次作业。
实际开发中,Dataflow类型的job还是很有好用的。
五、分片策略
AverageAllocationJobShardingStrategy
全路径:
io.elasticjob.lite.api.strategy.impl.AverageAllocationJobShardingStrategy
策略说明:
基于平均分配算法的分片策略,也是默认的分片策略。
如果分片不能整除,则不能整除的多余分片将依次追加到序号小的服务器。如:
如果有3台服务器,分成9片,则每台服务器分到的分片是:1=[0,1,2], 2=[3,4,5], 3=[6,7,8]
如果有3台服务器,分成8片,则每台服务器分到的分片是:1=[0,1,6], 2=[2,3,7], 3=[4,5]
如果有3台服务器,分成10片,则每台服务器分到的分片是:1=[0,1,2,9], 2=[3,4,5], 3=[6,7,8]
OdevitySortByNameJobShardingStrategy
全路径:
io.elasticjob.lite.api.strategy.impl.OdevitySortByNameJobShardingStrategy
策略说明:
根据作业名的哈希值奇偶数决定IP升降序算法的分片策略。
作业名的哈希值为奇数则IP升序。
作业名的哈希值为偶数则IP降序。
用于不同的作业平均分配负载至不同的服务器。
RotateServerByNameJobShardingStrategy
全路径:
io.elasticjob.lite.api.strategy.impl.RotateServerByNameJobShardingStrategy
策略说明:
根据作业名的哈希值对服务器列表进行轮转的分片策略。
六、代码示例
1、引入maven依赖
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.0.5</version>
</dependency>
<!-- 使用springframework自定义命名空间时引入 -->
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
<version>2.0.5</version>
</dependency>
2、Simple作业开发
意为简单实现,未经任何封装的类型。需实现SimpleJob接口。该接口仅提供单一方法用于覆盖,此方法将定时执行。与Quartz原生接口相似,但提供了弹性扩缩容和分片等功能。
public class MySimpleJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
int shardIndx = shardingContext.getShardingItem();
if (shardIndx == 0) {
//处理id为奇数的商家
System.out.println(String.format("------Thread ID: %s, Total number of task slices: %s, current fragmentation items: %s",
Thread.currentThread().getId(), shardingContext.getShardingTotalCount(), shardingContext.getShardingItem()));
} else {
//处理id为偶数的商家
System.out.println(String.format("------Thread ID: %s, Total number of task slices: %s, current fragmentation items: %s",
Thread.currentThread().getId(), shardingContext.getShardingTotalCount(), shardingContext.getShardingItem()));
}
/**
* 实际开发中,有了任务总片数和当前分片项,就可以对任务进行分片执行了
* 比如 SELECT * FROM user WHERE status = 0 AND MOD(id, shardingTotalCount) = shardingItem
*/
}
}
3、作业配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:reg="http://www.dangdang.com/schema/ddframe/reg"
xmlns:job="http://www.dangdang.com/schema/ddframe/job"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.dangdang.com/schema/ddframe/reg
http://www.dangdang.com/schema/ddframe/reg/reg.xsd
http://www.dangdang.com/schema/ddframe/job
http://www.dangdang.com/schema/ddframe/job/job.xsd">
<!--配置作业注册中心 -->
<reg:zookeeper id="regCenter" server-lists="172.16.150.247:2181" namespace="dd-job"
base-sleep-time-milliseconds="1000" max-sleep-time-milliseconds="3000" max-retries="3"/>
<!-- 配置作业-->
<job:simple id="mySimpleJob" class="com.liuxl.elastic.MySimpleJob" registry-center-ref="regCenter"
sharding-total-count="2" cron="0/2 * * * * ?" overwrite="true"/>
<!--failover:是否开启任务执行失效转移,开启表示如果作业在一次任务执行中途宕机,允许将该次未完成的任务在另一作业节点上补偿执行
description:作业描述
overwrite:本地配置是否可覆盖注册中心配置,如果可覆盖,每次启动作业都以本地配置为准
event-trace-rdb-data-source:作业事件追踪的数据源Bean引用
—>
</beans>
4、单机运行
本机运行
5、集群运行
本机运行、132机器
6、Dataflow作业开发
public class MyElasticJob implements DataflowJob {
@Override
public List<List<Object>> fetchData(ShardingContext shardingContext) {
int shardIndx = shardingContext.getShardingItem();
File file = new File("/Users/liuxl/Desktop/worker/wsd/20181012.xlsx");
List<List<Object>> reslut = new ArrayList<>();
try {
List<ExcelSheetPO> pos = ExcelUtil.readExcel(file, 400, 3);
if (shardIndx == 0) {
List<List<Object>> list = pos.get(0).getDataList();
//不要id
for (List<Object> objects : list) {
objects.remove(1);
}
reslut = list;
} else {
List<List<Object>> list = pos.get(0).getDataList();
// 不要名字
for (List<Object> objects : list) {
objects.remove(0);
}
reslut = list;
}
} catch (IOException e) {
e.printStackTrace();
}
return reslut;
}
@Override
public void processData(ShardingContext shardingContext, List<List<Object>> list) {
List<Object> objects = list.get(0);
System.out.println(
String.format("------Thread ID: %s, Total number of task slices: %s, current fragmentation items: %s,content : %s", Thread.currentThread().getId(), shardingContext.getShardingTotalCount(), shardingContext.getShardingItem(), JSONObject.toJSON(objects)
));
}
}
7、作业配置
<job:dataflow id="myDataFlowJob" class="com.liuxl.elastic.MyElasticJob" registry-center-ref=“regCenter" sharding-total-count="2" cron="0/2 * * * * ?" streaming-process="true" overwrite=“true" job-exceptionhandler=“com.liuxl.elastic.exception.MyJobExceptionHandler"/>
七、任务日志捕捉
elastic-job允许用户在任务调度异常时指定处理异常的异常处理器,异常处理器由接口JobExceptionHandler定义,定义如下
public interface JobExceptionHandler {
void handleException(String var1, Throwable var2);
}
如果没有指定自己的异常处理器elastic-job默认将使用DefaultJobExceptionHandler处理异常,其定义如下:
public final class DefaultJobExceptionHandler implements JobExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(DefaultJobExceptionHandler.class);
public DefaultJobExceptionHandler() {
}
public void handleException(String jobName, Throwable cause) {
log.error(String.format("Job '%s' exception occur in job processing", jobName), cause);
}
}
以下是一个自定义的异常处理器的示例:
public class MyJobExceptionHandler implements JobExceptionHandler {
private static final Logger logger = Logger.getLogger(MyJobExceptionHandler.class);
@Override
public void handleException(String jobName, Throwable cause) {
logger.error(String.format("任务[%s]调度异常", jobName), cause);
}
}
异常处理器的配置是通过job-exception-handler属性指定的,所有作业类型的异常处理器的配置是通用的:
<job:dataflow id="myDataFlowJob" class="com.liuxl.elastic.MyElasticJob" registry-center-ref="regCenter"
sharding-total-count="2" cron="0/2 * * * * ?" streaming-process="true" overwrite="true"
job-exception-handler="com.liuxl.elastic.exception.MyJobExceptionHandler"/>