BI分析-调度核心

调度管理

日常业务中,BI数据产出需求较多,通过定时任务执行跑批任务,在离线数仓中产出数据

当任务数量到达一定数量,业务复杂度达到一定难度,对定时任务就有管理需求

因为平台从0到1建设和历史原因,所以自研的调度管理,对定时任务的管理需关注以下问题

  1. 任务的启停(止血与复产)
  2. 可变更cron
  3. 可传参
  4. 感知运行状态和执行历史
  5. 自动重试
  6. 运行结果通知与警告
  7. 增量需求历史数据的补偿和修复

模型设计

  1. conf_info_classify:类别配置
  2. conf_info_group:任务组配置
  3. conf_info_task:任务配置
  4. conf_info_script:脚本配置
  5. conf_rely_group:组间依赖
  6. conf_rely_task:任务间依赖
  7. depend_info:运行时依赖
  8. 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;
}
posted @ 2023-06-28 22:36  ylc0x01  阅读(61)  评论(0)    收藏  举报