本系列文章是DolphinScheduler由浅入深的教程,涵盖搭建、二开迭代、核心原理解读、运维和管理等一系列内容。适用于想对 DolphinScheduler了解或想要加深理解的读者。

**祝开卷有益。 **

本系列教程基于 DolphinScheduler 2.0.5 做的优化。(稳定版推荐使用3.1.9

先抛出问题

1.场景描述

工作流 A 正在运行,里面有很多节点,依赖关系比较复杂。凌晨用户接到报警,a 节点失败了,此时其他分支的任务还在运行。此时工作流是不会是失败的,需要等待其他分支的任务运行结束,整个工作流才会失败。

失败的任务:是失败状态且重试次数用完了。

2.目前的处理流程

目前的做法是,先 kill 工作流,等待工作流变成失败状态,然后再点击恢复失败按钮,即可把失败的节点重新拉起来。

画外音:kill 工作流我也做了优化,后面会有文章介绍,kill之后,工作流会变成失败状态,这样做是为了可以恢复失败。

3.困惑

这种做法会影响正在运行的任务,强制把正在跑的任务 kill 掉,对一些运行时间比较久的任务来说,会降低执行效率。跑的好好的,被干掉了,恢复失败,又得重新跑。

这非常不划算。

优化建议:如何在不停止工作流的情况下,单独把失败的节点重新拉起来呢?

解决方案

后端优化:

分析了工作流启动、停止、恢复失败等操作类型 Master 和 Worker 的原理,打算新增一个操作类型、命令类型枚举值:RUN_FAILED_ONLY。

优化后的大致流程如下:

  1. 用户在页面上点击按钮,提交 executeType  = RUN_FAILED_ONLY、processInstanceId=xxxx的请求。

  2. API服务收到请求,判断是 RUN_FAILED_ONLY 操作,就封装一个 StateEventChangeCommand 命令,进行RPC请求。

  3. Master服务的 StateEventProcessor 监听到命令,提交给 StateEventResponseService ,它负责找到对应的工作流 WorkflowExecuteThread ,然后把这个stateEvent 给这个 WorkflowExecuteThread.

  4. WorkflowExecuteThread处理 stateEvent,判断这个 stateEvent 的 StateEventType  是 RUN_FAILED_ONLY_EVENT ,进行下面的处理:

找到改工作流失败且重试次数用完的任务列表,然后依次处理它们的执行记录(标记为失效,从失败列表移除,添加到待提交队列),最后提交到等待队列。

后端流程结束。

前端页面优化:

比较简单:新增一个按钮,文案是【重新失败节点】,在工作流列表上展示,用户可以点击。

源码

其中,新增了三个枚举类的值:

org.apache.dolphinscheduler.api.enums.ExecuteType

RUN_FAILED_ONLY

org.apache.dolphinscheduler.common.enums.CommandType

RUN_FAILED_ONLY(44, "run failed only");

org.apache.dolphinscheduler.common.enums.StateEventType

RUN_FAILED_ONLY_EVENT(4, "run failed only event");

几个关键步骤的代码,为了方便查看,上下文的代码、涉及改动的方法也会贴出来。

提示:序号对应上面的流程。

②  org.apache.dolphinscheduler.api.service.impl.ExecutorServiceImpl#execute

switch (executeType) {
    case REPEAT_RUNNING:
        result = insertCommand(loginUser, processInstanceId, processDefinition.getCode(), processDefinition.getVersion(), CommandType.REPEAT_RUNNING, startParams);
        break;
    case RECOVER_SUSPENDED_PROCESS:
        result = insertCommand(loginUser, processInstanceId, processDefinition.getCode(), processDefinition.getVersion(), CommandType.RECOVER_SUSPENDED_PROCESS, startParams);
        break;
    // 新增 9-11 行代码
    case RUN_FAILED_ONLY:
        result = sendRunFailedOnlyMsg(processInstance, CommandType.RUN_FAILED_ONLY);
        break;
    case START_FAILURE_TASK_PROCESS:
        result = insertCommand(loginUser, processInstanceId, processDefinition.getCode(), processDefinition.getVersion(), CommandType.START_FAILURE_TASK_PROCESS, startParams);
        break;
    case STOP:
        if (processInstance.getState() == ExecutionStatus.READY_STOP) {
            putMsg(result, Status.PROCESS_INSTANCE_ALREADY_CHANGED, processInstance.getName(), processInstance.getState());
        } else {
            result = updateProcessInstancePrepare(processInstance, CommandType.STOP, ExecutionStatus.READY_STOP);
        }
        break;
    case PAUSE:
        if (processInstance.getState() == ExecutionStatus.READY_PAUSE) {
            putMsg(result, Status.PROCESS_INSTANCE_ALREADY_CHANGED, processInstance.getName(), processInstance.getState());
        } else {
            result = updateProcessInstancePrepare(processInstance, CommandType.PAUSE, ExecutionStatus.READY_PAUSE);
        }
        break;
    default:
        logger.error("unknown execute type : {}", executeType);
        putMsg(result, Status.REQUEST_PARAMS_NOT_VALID_ERROR, "unknown execute type");         break;
}

sendRunFailedOnlyMsg 方法的逻辑,封装 stateEventChangeCommand,提交 RPC 请求。

 /**
     * send msg to master, run failed only
     *
     * @param processInstance process instance
     * @param commandType command type
     * @return update result
     */
    private Map<String, Object> sendRunFailedOnlyMsg(ProcessInstance processInstance, CommandType commandType) {
        Map<String, Object> result = new HashMap<>();
        String host = processInstance.getHost();
        String address = host.split(":")[0];
        int port = Integer.parseInt(host.split(":")[1]);
        StateEventChangeCommand stateEventChangeCommand = new StateEventChangeCommand(
                processInstance.getId(), 0, processInstance.getState(), processInstance.getId(), 0,StateEventType.RUN_FAILED_ONLY_EVENT
        );
        stateEventCallbackService.sendResult(address, port, stateEventChangeCommand.convert2Command());
        putMsg(result, Status.SUCCESS);
        return result;
    }

这里也给 StateEventChangeCommand 家了一个状态类型的字段,对应枚举类:StateEventType

方便下游判断状态。

查看下面 ③ 处的代码,用这个状态赋值给 stateEvent 的 type。

③ org.apache.dolphinscheduler.server.master.processor.StateEventProcessor#process 用上面 ② 处的 StateEventType,赋值给 stateEvent 的 type,然后提交给 stateEventResponseService 的 BlockingQueueeventQueue 队列。

@Override
    public void process(Channel channel, Command command) {
        Preconditions.checkArgument(CommandType.STATE_EVENT_REQUEST == command.getType(), String.format("invalid command type: %s", command.getType()));         StateEventChangeCommand stateEventChangeCommand = JSONUtils.parseObject(command.getBody(), StateEventChangeCommand.class);
        StateEvent stateEvent = new StateEvent();
        stateEvent.setKey(stateEventChangeCommand.getKey());
        if (stateEventChangeCommand.getSourceProcessInstanceId() != stateEventChangeCommand.getDestProcessInstanceId()) {
            stateEvent.setExecutionStatus(ExecutionStatus.RUNNING_EXECUTION);
        } else {
            stateEvent.setExecutionStatus(stateEventChangeCommand.getSourceStatus());
        }
        stateEvent.setProcessInstanceId(stateEventChangeCommand.getDestProcessInstanceId());
        stateEvent.setTaskInstanceId(stateEventChangeCommand.getDestTaskInstanceId());
        // TODO 修改
        StateEventType stateEventType = stateEventChangeCommand.getStateEventType();
        if (stateEventType != null){
            stateEvent.setType(stateEventType);
        }else {
            StateEventType type = stateEvent.getTaskInstanceId() == 0 ? StateEventType.PROCESS_STATE_CHANGE : StateEventType.TASK_STATE_CHANGE;
            stateEvent.setType(type);
        }         logger.info("received command : {}", stateEvent);
        stateEventResponseService.addResponse(stateEvent);
    }

StateEventResponseWorker 线程一直扫描这个队列,拿到 stateEvent,找到要处理的工作流对应的 WorkflowExecuteThread 线程,把这个事件提交给 WorkflowExecuteThread 线程。

 /**
 * task worker thread
 */
class StateEventResponseWorker extends Thread {     @Override
    public void run() {         while (Stopper.isRunning()) {
            try {
                // if not task , blocking here
                StateEvent stateEvent = eventQueue.take();
                persist(stateEvent);
            } catch (InterruptedException e) {
                logger.warn("persist task error", e);
                Thread.currentThread().interrupt();
                break;
            }
        }
        logger.info("StateEventResponseWorker stopped");
    }
} private void persist(StateEvent stateEvent) {
    try {
        if (!this.processInstanceMapper.containsKey(stateEvent.getProcessInstanceId())) {
            writeResponse(stateEvent, ExecutionStatus.FAILURE);
            return;
        }         WorkflowExecuteThread workflowExecuteThread = this.processInstanceMapper.get(stateEvent.getProcessInstanceId());
        workflowExecuteThread.addStateEvent(stateEvent);
        writeResponse(stateEvent, ExecutionStatus.SUCCESS);
    } catch (Exception e) {
        logger.error("persist event queue error, event: {}", stateEvent, e);
    }
}

④WorkflowExecuteThread 内部循环扫描事件列表。

private void handleEvents() {
    while (!this.stateEvents.isEmpty()) {         try {
            StateEvent stateEvent = this.stateEvents.peek();
            if (stateEventHandler(stateEvent)) {
                this.stateEvents.remove(stateEvent);
            }
        } catch (Exception e) {
            logger.error("state handle error:", e);         }
    }
}

stateEventHandler 处理 RUN_FAILED_ONLY_EVENT 类型的事件,处理方法是:runFailedHandler

private boolean stateEventHandler(StateEvent stateEvent) {
        logger.info("process event: {}", stateEvent.toString());         if (!checkStateEvent(stateEvent)) {
            return false;
        }
        boolean result = false;
        switch (stateEvent.getType()) {
            case RUN_FAILED_ONLY_EVENT:
                result = runFailedHandler(stateEvent);
                break;
            case PROCESS_STATE_CHANGE:
                result = processStateChangeHandler(stateEvent);
                break;
            case TASK_STATE_CHANGE:
                result = taskStateChangeHandler(stateEvent);
                break;
            case PROCESS_TIMEOUT:
                result = processTimeout();
                break;
            case TASK_TIMEOUT:
                result = taskTimeout(stateEvent);
                break;
            default:
                break;
        }         if (result) {
            this.stateEvents.remove(stateEvent);
        }
        return result;
    }

runFailedHandler 的内部逻辑如下:找到改工作流失败且重试次数用完的任务列表,然后依次处理它们的执行记录(标记为失效,从失败列表移除,添加到待提交队列),最后提交到等待队列。

private boolean runFailedHandler(StateEvent stateEvent) {
    try {
        logger.info("process:{} will do {}", processInstance.getId(), stateEvent.getExecutionStatus());
        // find failed tasks with max retry times and init these tasks
        List<Integer> failedList = processService.queryTaskByProcessIdAndStateWithMaxRetry(processInstance.getId(), ExecutionStatus.FAILURE);
        logger.info("run failed task size is : {}", failedList.size());
        for (Integer taskId : failedList) {
            logger.info("run failed task id is : {}", taskId);
            TaskInstance taskInstance = processService.findTaskInstanceById(taskId);
            taskInstance.setFlag(Flag.NO);
            // remove it from errorTaskList
            errorTaskList.remove(Long.toString(taskInstance.getTaskCode()));
            processService.updateTaskInstance(taskInstance);
            // submit current task nodes
            if (readyToSubmitTaskQueue.contains(taskInstance)) {
                continue;
            }
            logger.info("run failed task ,submit current task nodes : {}", taskInstance.toString());
            addTaskToStandByList(taskInstance);
        }
        submitStandByTask();
//            updateProcessInstanceState();
    } catch (Exception e) {
        logger.error("process only run failed task error:", e);
    }
    return true;
}

最终效果

再次回到文章开头的场景:

工作流 A 正在运行,里面有很多节点,依赖关系比较复杂。凌晨用户接到报警,a 节点失败了,此时其他分支的任务还在运行。

此时用户可以直接点击【重新拉起失败任务】按钮,失败的任务就会重新进入等待队列,后续流程就像任务正常运行一样,也会继续拉起下游任务。

画外音:本次优化简化了失败任务运维的复杂度,提高了效率。

作者从1.x开始使用海豚调度,那是还叫做 Easy Scheduler,是一个忠实用户,我们基于 2.x版本做了很多内部的改造,后续会分享出来,同样社区也推荐大家使用3.1.9版本,这是相对比较稳定的版本。

本文由 白鲸开源 提供发布支持!

海豚调度调优 | 正在运行的工作流(DAG)如何重新拉起失败的任务(Task)的更多相关文章

  1. kube-scheduler 调度调优

    文章转载自:https://www.kuboard.cn/learning/k8s-advanced/schedule/tuning.html kube-scheduler 是 Kubernetes ...

  2. SQL Server 性能调优 之运行计划(Execution Plan)调优

    运行计划中的三种 Join 策略 SQL Server 存在三种 Join 策略:Hash Join,Merge Join,Nested Loop Join. Hash Join:用来处理没有排过序/ ...

  3. hadoop MapReduce - 从作业、任务(task)、管理员角度调优

    Hadoop为用户作业提供了多种可配置的参数,以允许用户根据作业特点调整这些参数值使作业运行效率达到最优. 一 应用程序编写规范 1.设置Combiner         对于一大批MapReduce ...

  4. Spark 调优

    资源调优 (1). 在部署 spark 集群中指定资源分配的默认参数 在 spark 安装包的 conf 下的 spark-env.sh SPARK_WORKER_CORES SPARK_WORKER ...

  5. PHP 性能分析第三篇: 性能调优实战

    注意:本文是我们的 PHP 性能分析系列的第三篇,点此阅读 PHP 性能分析第一篇: XHProf & XHGui 介绍 ,或  PHP 性能分析第二篇: 深入研究 XHGui. 在本系列的 ...

  6. Cloudera Hadoop 5& Hadoop高阶管理及调优课程(CDH5,Hadoop2.0,HA,安全,管理,调优)

    1.课程环境 本课程涉及的技术产品及相关版本: 技术 版本 Linux CentOS 6.5 Java 1.7 Hadoop2.0 2.6.0 Hadoop1.0 1.2.1 Zookeeper 3. ...

  7. 【Spark】Sparkstreaming-性能调优

    Sparkstreaming-性能调优 Spark Master at spark://node-01:7077 sparkstreaming 线程 数量_百度搜索 streaming中partiti ...

  8. Spark的job调优(1)

    本文翻译之cloudera的博客,本系列有两篇,第二篇看心情了 概论 当我们理解了transformation,action和rdd后,我们就可以写一些基础的spark的应用了,但是如果需要对应用进行 ...

  9. Spark性能优化:shuffle调优

    调优概述 大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO.序列化.网络数据传输等操作.因此,如果要让作业的性能更上一层楼,就有必要对shuffle过程进行 ...

  10. JVM 调优之 Eclipse 启动调优实战

    本文是我12年在学习<深入理解Java虚拟机:JVM高级特性与最佳实践>时,做的一个 JVM 简单调优实战笔记,版本都有些过时,不过调优思路和过程还是可以分享给大家参考的. 环境基础配置 ...

随机推荐

  1. Java代码忽略https证书:解决No subject alternative names present问题 HttpURLConnection https请求

    Java代码忽略https证书:解决No subject alternative names present问题 import org.slf4j.Logger; import org.slf4j.L ...

  2. es应用服务初始化步骤

    第一步:创建索引+settings+mappingtestes_v1 PUT { "settings": { "index.mapping.total_fields.li ...

  3. Web运作原理探析

    Web运作原理探析 1.1 web的 概念 Web是一种分布式的应用架构,旨在共享分布在网络上的各个Web服务器中的所有互相链接的信息. 1.2 HTML是指超文本标记语言. 1.3 URL简介 UR ...

  4. HBase 中的 JVM 与 GC

    HBase中JVM基本配置 在JVM中,默认情况下会设置minimum heap size 为 1/64 可用物理内存,并为maximum heap size设置 1/4 的物理可用内存(不过在Jav ...

  5. Linux内核驱动:cdev、misc以及device三者之间的联系和区别

    Linux内核驱动:cdev.misc以及device三者之间的联系和区别 背景 我想在cdev中使用dev_err等log打印函数,但是跟踪了一下cdev中的原型,发现并不是我想要的. 常见的驱动是 ...

  6. LaravelLumen 分组求和问题 where groupBy sum

    在Laravel中使用分组求和,如果直接使用Laravel各数据库操作方法,应该会得出来如下代码式: DB::table('table_a') ->where('a','=',1) ->g ...

  7. linux实现SSH免密登录设置,以及shell脚本实现

    分享/朱季谦 最近在搭建linux集群,做了SSH免密登录的设置,正好把过程记录一下: 一.用搭建好的两台虚拟机做演示,A机器:192.168.200.129,B机器:192.168.200.128 ...

  8. Java-文件下载案例

    文件下载需求 1.页面显示超链接 2.点击超链接后弹出下载框 3.完成图片文件下载 分析 1.超链接指向的资源如果能够被浏览器解析,则在浏览器中展示,如果不能解析,则弹出下载提示框.不满足需求 2.任 ...

  9. Python 生成条形码、二维码 (Code 128、EAN-13、QR code等)

    条形码和二维码是现代信息交换和数据存储的重要工具,它们将信息以图形的形式编码,便于机器识别和数据处理,被广泛应用于物流.零售.医疗.教育等各领域.本文将介绍如何使用Python快速生成各种常见的条形码 ...

  10. oeasy教您玩转linux010206toilet

    我们来回顾一下 上一部分我们都讲了什么? 用apt查询并下载了figlet 玩了一下字符画 设置了字符画的字体 但是没有修改颜色 这次我们来找找另一个命令toilet apt search toile ...