分布式任务框架--Elastic-job

一、简介
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%3
1的用户;
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<List> {

    @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"/>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值