自己动手写分布式任务调度框架

分布式任务调度框架是互联网公司的标配,趁着周末下雨天,自己动手写了一个简版,主要是为了体会其中的原理。框架特点和一些基本功能如下:

  • 弹性扩容缩容,理论上可无限扩容,智能负载均衡;
  • Master-Slave 模式,高可用,支持故障转移;
  • 调度精度设置;
  • 使用简便;

其他的比如区分不同项目、任务的动态上下线、任务串行/并行执行、报警重试机制等功能,由于时间原因暂未添加。

整个框架设计非常简单:

在这里插入图片描述

应用在启动后会向注册中心上报任务,调度平台会根据任务生成计划列表,当任务触发后,调度平台会向应用集群下发任务。

整个项目分为 server 和 client 模块:
在这里插入图片描述

一般 client 会作为一个 jar 包提供给业务应用使用,我这里为了简便,直接将 client 作为一个业务应用,同时任务下发也进行了简化,一般在 client jar 包中也会开启一个 Web Server 供调度平台远程调用。

在 client 启动的时候会向注册中心注册任务,这里是简化版本:

package com.dongguabai.dongguabaitask.client.regist;

import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 任务注册(这里简化,直接 Controller 注册,其实还可以使用注解、接口编程方式处理);
 * 一般是需要启动一个 Web Server(Netty)去执行 job
 *
 * @author Dongguabai
 * @Date 创建于 2020-06-20 00:57
 */
@Component
public class SimpleTaskRegist {

    private static final String TASK_ROOT_PATH = "/dongguabai-task/task";

    @Autowired
    private CuratorFramework zkClient;

    public void regist( String taskName, String cron, String requestUrl) {
        String taskPath = TASK_ROOT_PATH + "/" + taskName;
        createPath(taskPath,CreateMode.PERSISTENT,cron.getBytes());
        createPath(taskPath+"/"+requestUrl,CreateMode.EPHEMERAL,new byte[0]);
    }

    private void createPath(String path, CreateMode model,byte[] data) {
        try {
            zkClient.create().creatingParentsIfNeeded()
                    .withMode(model).forPath(path,data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

注册任务主要是将任务的基本信息,如 Cron 表达式,调用的具体方法,当前客户端的信息等进行上传。

调度中心在启动的时候会进行选举,选出 Master 节点,我这里由于功能简单,Master 节点主要是进行故障转移:

    private void vote() throws Exception {
        List<String> list = zkClient.getChildren().forPath(SERVER_PATH);
        servers = list.stream().sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.toList());
        if (servers.size() == 1) {
            NODE.setMaster(true);
            NODE.setSeq(0);
            return;
        }
        String nodePath = NODE.getNodePath();

        int index = list.indexOf(nodePath);
        NODE.setSeq(index);
        NODE.setMaster(index == 0);
    }

调度中心选举完成后会从任务注册中心中拉取任务,生成计划列表:

    private void jobSchedule() {
        SCHEDULED_EXECUTOR_SERVICE.scheduleWithFixedDelay(() -> {
            NODE.setGenJob(true);
            long currentMs = System.currentTimeMillis();
            try {
                //获取任务列表
                List<String> nodeTasks = getNodeTasks(NODE.getSeq());
                List<String> dataList = nodeTasks.stream().map(taskName -> {
                    try {
                        byte[] bytes = zkClient.getData().forPath(TASK_ROOT_PATH + "/" + taskName);
                        return new String(bytes);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return null;
                    }
                }).collect(Collectors.toList());
                jobService.genJobs(nodeTasks,dataList,NODE);
            } catch (Exception e) {
                e.printStackTrace();
            }
            NODE.setGenJob(false);
        }, 3, 60, TimeUnit.SECONDS);
    }

这里会触发优雅停机(关于优雅停机可参看“从 Java 程序优雅停机到 Linux 信号机制初窥”)。当调度节点发生宕机,之前节点需要处理的计划会转移到 Master 节点(也可以实现均衡负载),同时刷新节点列表,重新选举:

 PathChildrenCache childrenCache = new PathChildrenCache(zkClient, SERVER_PATH , true);
            childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
            childrenCache.getListenable().addListener((curatorFramework, changeEvent) -> {
                PathChildrenCacheEvent.Type eventType = changeEvent.getType();
                switch (eventType) {
                    case CHILD_REMOVED:
                        log.info("CHILD_REMOVED....");
                        String removedPath = changeEvent.getData().getPath();
                        vote();
                        if (NODE.isMaster() && NODE.isAvailable()){
                            jobService.masterTakeOver(NODE.getNodePath(),removedPath);
                        }
                        break;
                    case CHILD_ADDED:
                        log.info("CHILD_REMOVED....");
                        vote();
                        break;
                    default:
                }

            });

接下来,简单看一下效果。

我这里开启了业务服务,有四个节点,总共有 5 个任务:a,b,c,d,e:

package com.dongguabai.dongguabaitask.client;

import com.dongguabai.dongguabaitask.client.regist.SimpleTaskRegist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author Dongguabai
 * @Description todo 提供jar包,引用上传Task
 * @Date 创建于 2020-06-21 08:51
 */
@RestController
public class RegistController implements ApplicationListener<WebServerInitializedEvent> {

    private int port;

    @Autowired
    private SimpleTaskRegist simpleTaskRegist;

    @RequestMapping("a")
    public void a(@RequestParam("node")String node){
        print(node,"a");
    }

    private void print(String node,String methodName) {
        System.out.println("------------");
        System.out.println("Time:"+new Date().toLocaleString());
        System.out.println("NODE:"+node);
        System.out.println("Method:"+methodName);
        System.out.println("Port:"+port);
        System.out.println("------------");
    }

    @RequestMapping("b")
    public void b(@RequestParam("node")String node){
        print(node,"b");
    }

    @RequestMapping("c")
    public void c(@RequestParam("node")String node){
        print(node,"c");
    }

    @RequestMapping("d")
    public void d(@RequestParam("node")String node){
        print(node,"d");
    }

    @RequestMapping("e")
    public void e(@RequestParam("node")String node){
        print(node,"e");
    }


    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        port = event.getWebServer().getPort();
        simpleTaskRegist.regist("a","0 */1 * * * ?","127.0.0.1:"+port+"-a");
        simpleTaskRegist.regist("b","0 */2 * * * ?","127.0.0.1:"+port+"-b");
        simpleTaskRegist.regist("c","0 */1 * * * ?","127.0.0.1:"+port+"-c");
        simpleTaskRegist.regist("d","0 */2 * * * ?","127.0.0.1:"+port+"-d");
        simpleTaskRegist.regist("e","0 */1 * * * ?","127.0.0.1:"+port+"-e");
    }
}

在这里插入图片描述
再启动两个调度节点:
在这里插入图片描述

[zk: 127.0.0.1:2181(CONNECTED) 78] ls /dongguabai-task/server
[node0000000063, node0000000064]

其中 node0000000063 节点会被选举为 Master 节点。

因为这里有 5 个任务,根据均衡复杂的策略,a、b、e 任务会由 node0000000063 执行,c、d 任务会由 node0000000064 触发执行,具体执行会随机选择存活的业务节点:
在这里插入图片描述
这时候再开启一个 Server 节点:

java -jar -Dserver.port=9082 /Users/dongguabai/IdeaProjects/dongguabai-task/dongguabai-task-server/target/dongguabai-task-server-0.0.1-SNAPSHOT.jar                   
[zk: 127.0.0.1:2181(CONNECTED) 79] ls /dongguabai-task/server
[node0000000063, node0000000064, node0000000065]

再根据负载策略,a 任务会由 node0000000063 执行,b、e 任务会由 node0000000064 触发执行,c、d 任务会由 node0000000065 执行:
在这里插入图片描述
接下来关闭 node0000000064 节点:
在这里插入图片描述
还有几个任务需要该节点去执行:
在这里插入图片描述
此时该节点未执行的任务将会转移到 node0000000063 机器:
在这里插入图片描述
至此就演示了任务的负载均衡调用、调度服务的弹性扩容缩容和故障转移。

源码地址:https://github.com/dongguabai/dongguabai-task

References

  • https://github.com/jmrozanec/cron-utils

欢迎关注公众号
​​​​​​在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值