分布式任务调度框架是互联网公司的标配,趁着周末下雨天,自己动手写了一个简版,主要是为了体会其中的原理。框架特点和一些基本功能如下:
- 弹性扩容缩容,理论上可无限扩容,智能负载均衡;
- 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
欢迎关注公众号