海豚排程調優 | 正在執行的工作流(DAG)如何重新拉起失敗的任務(Task)

海豚调度發表於2024-06-21

💡 本系列文章是DolphinScheduler由淺入深的教程,涵蓋搭建、二開迭代、核心原理解讀、運維和管理等一系列內容。適用於想對 DolphinScheduler瞭解或想要加深理解的讀者。

**祝開卷有益。 **

本系列教程基於 DolphinScheduler 2.0.5 做的最佳化。(穩定版推薦使用3.1.9

file

先丟擲問題

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版本,這是相對比較穩定的版本。

本文由 白鯨開源 提供釋出支援!

相關文章