💡 本系列文章是DolphinScheduler由淺入深的教程,涵蓋搭建、二開迭代、核心原理解讀、運維和管理等一系列內容。適用於想對 DolphinScheduler瞭解或想要加深理解的讀者。
**祝開卷有益。 **
本系列教程基於 DolphinScheduler 2.0.5 做的最佳化。(穩定版推薦使用3.1.9)
先丟擲問題
1.場景描述
工作流 A 正在執行,裡面有很多節點,依賴關係比較複雜。凌晨使用者接到報警,a 節點失敗了,此時其他分支的任務還在執行。此時工作流是不會是失敗的,需要等待其他分支的任務執行結束,整個工作流才會失敗。
失敗的任務:是失敗狀態且重試次數用完了。
2.目前的處理流程
目前的做法是,先 kill 工作流,等待工作流變成失敗狀態,然後再點選恢復失敗按鈕,即可把失敗的節點重新拉起來。
畫外音:kill 工作流我也做了最佳化,後面會有文章介紹,kill之後,工作流會變成失敗狀態,這樣做是為了可以恢復失敗。
3.困惑
這種做法會影響正在執行的任務,強制把正在跑的任務 kill 掉,對一些執行時間比較久的任務來說,會降低執行效率。跑的好好的,被幹掉了,恢復失敗,又得重新跑。
這非常不划算。
最佳化建議:如何在不停止工作流的情況下,單獨把失敗的節點重新拉起來呢?
解決方案
後端最佳化:
分析了工作流啟動、停止、恢復失敗等操作型別 Master 和 Worker 的原理,打算新增一個操作型別、命令型別列舉值:RUN_FAILED_ONLY。
最佳化後的大致流程如下:
-
使用者在頁面上點選按鈕,提交 executeType = RUN_FAILED_ONLY、processInstanceId=xxxx的請求。
-
API服務收到請求,判斷是 RUN_FAILED_ONLY 操作,就封裝一個 StateEventChangeCommand 命令,進行RPC請求。
-
Master服務的 StateEventProcessor 監聽到命令,提交給 StateEventResponseService ,它負責找到對應的工作流 WorkflowExecuteThread ,然後把這個stateEvent 給這個 WorkflowExecuteThread.
-
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版本,這是相對比較穩定的版本。
本文由 白鯨開源 提供釋出支援!