BI分析-调度核心
调度管理
日常业务中,BI数据产出需求较多,通过定时任务执行跑批任务,在离线数仓中产出数据
当任务数量到达一定数量,业务复杂度达到一定难度,对定时任务就有管理需求
因为平台从0到1建设和历史原因,所以自研的调度管理,对定时任务的管理需关注以下问题
- 任务的启停(止血与复产)
- 可变更cron
- 可传参
- 感知运行状态和执行历史
- 自动重试
- 运行结果通知与警告
- 增量需求历史数据的补偿和修复
模型设计
conf_info_classify:类别配置conf_info_group:任务组配置conf_info_task:任务配置conf_info_script:脚本配置conf_rely_group:组间依赖conf_rely_task:任务间依赖depend_info:运行时依赖sign_info_task:任务执行记录
使用说明
内网应用,没有图例
主要分为:
- 类别配置
- 类别列表
- 编辑/新增类别
- 脚本配置
- 脚本列表
- 编辑/新增脚本,重要属性:
- 脚本参数
- 脚本地址
- 执行代码,java动态代码执行,spring容器运行时注入
- 任务配置
- 任务列表
- 编辑/新增任务,重要属性:
- 关联脚本
- 任务重试次数
- 任务重试间隔
- 任务结果通知对象邮件
- 执行日志
- 执行日志列表
- 执行信息明细
- 任务组配置
- 组间依赖:有向无环图
- 任务组启停状态
- 任务组可选执行操作
- Quartz:任务组新增到Quart容器内,或刷新任务组属性操作
- 编辑
- 任务组配置操作
- 是否支持历史模式
- cron表达式配置
- 组内任务依赖:有向无环图
- 节点的新增删除操作
- 任务组配置操作
- 新增
- 新增任务组节点,可选多个任务组节点作为依赖任务组
- 点击新增后跳转到编辑页面,配置任务组参数和组内任务
- 删除
- 只能选择顶层任务组节点删除
- 执行
- 常规模式:执行任务
- 历史模式:从选择时间开始执行任务,一直执行到当前时间
- 停用/启用
- 启用/停用任务组
调度核心
调度模型,组间关系和任务间关系都是有向无环图结构
服务上线初期,组间依赖和任务间依赖执行都是靠线程while循环查询依赖任务执行结果然后判断自己是否执行,所以就存在查询时间间隔问题,间隔长短都是不是优解。
优化该逻辑,实现无额外消耗,上层依赖任务执行完成,本层任务自动执行,同级任务自动并发
定义及术语
图的定义:图是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G = (V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
图按照有无方向分为无向图和有向图,有向无环图是一个无回路的有向图
度:顶点的度是指和该顶点关联的边的数目。
入度:有向图中以顶点(v)为头的弧的数目,称为(v)的入度。
出度:有向图中以顶点(v)为尾的弧的数目,称为(v)的出度。
相关伪代码
节点
// 节点定义
public class Node {
/**
* 节点值域
*/
private String nodeVal;
/**
* 入度的弧
*/
private Map<String, Boolean> inArc;
/**
* 出度的弧
*/
private Map<String, Boolean> outArc;
/**
* 出度个数
*/
private Integer outCounter;
/**
* 入度个数
*/
private Integer inCounter;
/**
* 当前节点状态
*/
private Boolean done;
/**
* 任务
*/
private EtlTask task;
}
// 创建节点
public static Node from(List<String> relTaskIds, String taskId, _) {
Node node = new Node();
//任务id
node.setNodeval(taskId);
//初始化出度计数
node.setInCounter(0);
//初始化入度计数
node.setOutCounter(0);
//完成标识
node.setDone(false);
//初始化出度的弧
node.setInArc(new HashMap<>(16));
//初始化入度的弧
node.setoutArc(new HashMap<>(16));
//配置入度及入度状态
for (String id : relTaskIds) {
node.getInArc().put(id, false);
node.setInCounter(node.getInCounter() + 1);
}
return node;
}
// 添加节点
public Boolean addTask(String nodeName, List<String> relTaskIds){
Map<String,Node> graph = getGraph();
if(!CollectionUtils.isEmpty(graph) && objects.nonNull(graph.get(task.getTaskId()))){
return false;
}
graph.put(nodeName, Node.from(relTaskIds,nodeName,_);
return true;
}
图
// 图的定义
public class TaskGraph {
/**
* 图
*/
private Map<String, Node> graph;
/**
* 待处理
*/
private Set<String> toDo;
}
// 创建图
public static TaskGraph newGraph(String graphName){
TaskGraph taskGraph = new TaskGraph();
taskGraph.setGraph(new ConcurrentHashMap<>(16));
taskGraph.setToDo(new ConcurrentSkipListSet<>());
...
return taskGraph;
}
// 初始化图
public Boolean initGraph(){
Map<String, Node> graph = getGraph();
// 逆拓扑栈,通过栈遍历图
Stack<String> topStack = new Stack<>();
// 每个节点入度个数
Map<String,Integer> tmpInCounter = new HashMap<>(16);
// 检查节点入度
for(String nodeName:graph.keySet()) {
// 节点
Node node = graph.get(nodeName);
// 循环该节点的入度
for (String in : node.getInArc().keySet()) {
// node节点的入度节点
Node inNode = graph.get(in);
if (objects.isNull(inNode)) {
// 依赖不出存在
return false;
}
// 入度节点的出度及状态
inNode.getOutArc().put(nodeName, false);
// 入度节点的出度计数+1
inNode.setOutCounter(inNode.getOutCounter() + 1);
// node节点的入度计数
tmpInCounter.put(nodeName, node.getInCounter());
// 如果当前节点入度为,当前节点为根节点
if (node.getInCounter() == 0) {
// 压入栈顶,根据该节点遍历整张图
topstack.push(nodeName);
}
}
}
// 检查是否有环
// 没有根节点,topStack为空
// 有根节点,图中有回路,则必包含一个子图为回路,即该子图中所有顶点入度不为0且至少有边指向另外的顶点
// 计算:
// 1:遍历每个子节点关联的边并统计每个几点的入度
// 2:删除所有入度为零的顶点及其出度的边,并将删除边所指向的顶点的入度减1
// 重复步骤2,直到:
// 1:所有顶点都被删除,则没有回路
// 2:还有入度不为的顶点存在,则存在回路
int vertices = 0;
// 栈不为空持续循环
while(!topStack.empty()) {
vertices++;
// 根据栈顶元素获取节点,循环该节点的出度
for (string out : graph.get(topStack.pop()).getOutArc().keySet()) {
// node节点的出度为out节点,out节点的入度计数-1
tmpInCounter.put(out, tmpInCounter.get(out) - 1);
// 检查完out节点所有的入度,将out放入栈中检查出度
if (tmpInCounter.get(out) == 0) {
topStack.push(out);
}
}
}
if(vertices != graph.size()){
return false;
}
// 生成初始待办任务
graph.forEach((iter,node)->{
if(node.getInCounter() == 0) {
getToDo().add(iter);
}
});
return true;
}
执行
// 任务组执行流程
public void doGroupExecute(_, String jobGroupId, _, _, _, _){
...
try {
...
// 构建图
String taskGraphKey = StringUtils.removeAll(UUID.randomUUID().toString(), "-").toUpperCase() + "|" + jobGroupId;
TaskGraph taskGraph = TaskGraph.newGraph(taskGraphKey);
...
//添加任务
relyTasksMapList.forEach(tasksMap -> taskGraph.addTask(buildJavaTask(_, _, _, _)));
if(!taskGraph.initGraph()){
logger.error("构建图失败!");
return;
}
//执行任务
taskGraph.doTasks(taskResults);
...
//清空图
TaskGraph.clear(taskGraph,taskGraphKey);
...
} catch(ExecutionException | InterruptedException e){
logger.error("reason for failure:"+e.getMessage());
}
}
// 执行任务
public void doTasks(_) throws ExecutionException,InterruptedException {
while (true) {
// 当前待办
List<Object> toDo = Arrays.asList(getToDo().toArray());
if (toDo.size() <= 0) {
//当前没有待办跳出循环
break;
}
// 主线程阻塞器
CountDownLatch countDownLatch = new CountDownLatch(toDo.size());
for (Object taskName : toDo) {
Node node = graph.get(taskName);
//异步执行
taskResults.add(CompletableFuture.supplyAsync(() -> {
EtlTask task = node.getTask();
task.setCountDownLatch(countDownLatch);
return task.get();
}));
}
//阻塞等待
countDownLatch.await();
}
}
// 任务完成
public class EtlTask implements Supplier<Boolean> {
...
/**
* 任务组,任务计数器
*/
private CountDownLatch countDownLatch;
@Override
public Boolean get() {
try {
...
return doRun().result;
} catch(ExecutionException e){
log.error(javaScript.getName()+"...task执行失败!",e);
return false;
}finally{
Optional<TaskGraph> taskGraph = TaskGraph.getGraph(taskGraphKey);
taskGraph.ifPresent(t -> t.markNodeDone(taskId));
countDownLatch.countDown();
}
}
}
// 节点标记完成
private Lock lock = new ReentrantLock();
public Boolean markNodeDone(String nodeName) {
lock.lock();
try {
if(!getToDo().contains(nodeName)){
return false;
}
// 删除todo任务
getToDo().remove(nodeName);
// 当前节点
Node node = getGraph().get(nodeName);
// 当前任务节点设置完成状态
node.setDone(true);
// 当前节点的出度
for(String outName : node.getOutArc().keySet()) {
// 当前节点的出度节点
Node out = getGraph().get(outName);
// 出度节点的入度标识为标志位完成
out.getInArc().put(nodeName, true);
// 出度节点的入度个数-1
out.setInCounter(out.getInCounter() - 1);
// out节点入度个数为,out节点进入待办
if (out.getInCounter() == 0) {
getToDo().add(outName);
}
}
// 当前节点入度
for(String inName : node.getInArc().keySet()){
// 当前节点的入度节点
Node in = getGraph().get(inName);
// 入度节点的出度标记为完成
in.getOutArc().put(nodeName, true);
// 入度节点的出度个数-1
in.setOutCounter(in.getOutCounter() - 1);
}
} finally{
lock.unlock();
}
return true;
}

浙公网安备 33010602011771号