dolphineScheduler设计点分享

660 阅读2分钟

背景

公司打算使用 dolphinscheduler 框架作为中台项目使用,因为其中任务调度,工作流,数据源,资源中心等功能比较健全,可以用其统一好好管理公司的资源。

既然要使用 dolphinscheduler,第一步当然是要看看其源码,了解它的架构设计,方便后续扩展开发啦。

ds架构源码解析

整体流程图:

image.png

API 模块

一 command 生成

整体逻辑图为:

image.png

ProcessScheduleJob 放入 quartz 中,核心执行代码即是 生成 command

   public class ProcessScheduleJob extends QuartzJobBean { 
     private static final Logger logger = LoggerFactory.getLogger(ProcessScheduleJob.class); 
     @Autowired 
     private ProcessService processService; 
     @Autowired 
     private QuartzExecutor quartzExecutor; 
   
     @Counted(value = "quartz_job_executed") 
     @Timed(value = "quartz_job_execution", percentiles = {0.5, 0.75, 0.95, 0.99}, histogram = true) 
     @Override 
     protected void executeInternal(JobExecutionContext context) { 
       JobDataMap dataMap = context.getJobDetail().getJobDataMap(); 
       int projectId = dataMap.getInt(Constants.PROJECT_ID); 
       int scheduleId = dataMap.getInt(Constants.SCHEDULE_ID); 
       Date scheduledFireTime = context.getScheduledFireTime(); 
       Date fireTime = context.getFireTime(); 
       logger.info("scheduled fire time :{}, fire time :{}, process id :{}", scheduledFireTime, fireTime, scheduleId); 
       // query schedule 
       Schedule schedule = processService.querySchedule(scheduleId); 
       if (schedule == null || ReleaseState.OFFLINE == schedule.getReleaseState()) { 
          logger.warn("process schedule does not exist in db or process schedule offline,delete schedule job in quartz, projectId:{}, scheduleId:{}", projectId, scheduleId); deleteJob(context, projectId, scheduleId); return; } 
           ProcessDefinition processDefinition = processService.findProcessDefinitionByCode(schedule.getProcessDefinitionCode()); 
           // release state : online/offline 
           ReleaseState releaseState = processDefinition.getReleaseState(); 
           if (releaseState == ReleaseState.OFFLINE) { 
           logger.warn("process definition does not exist in db or offline,need not to create command, projectId:{}, processId:{}", projectId,         processDefinition.getId()); 
          return; 
        } 
        
      Command command = new Command(); 
      command.setCommandType(CommandType.SCHEDULER); 
      command.setExecutorId(schedule.getUserId()); 
      command.setFailureStrategy(schedule.getFailureStrategy()); 
      command.setProcessDefinitionCode(schedule.getProcessDefinitionCode()); 
      command.setScheduleTime(scheduledFireTime); command.setStartTime(fireTime); 
      command.setWarningGroupId(schedule.getWarningGroupId()); String workerGroup = 
      StringUtils.isEmpty(schedule.getWorkerGroup()) ? Constants.DEFAULT_WORKER_GROUP : schedule.getWorkerGroup(); 
      command.setWorkerGroup(workerGroup); 
      command.setWarningType(schedule.getWarningType()); 
      command.setProcessInstancePriority(schedule.getProcessInstancePriority()); 
      command.setProcessDefinitionVersion(processDefinition.getVersion()); 
      processService.createCommand(command); 
   } 
 }
    

master模块

通俗讲,负责发号施令,布置任务,接受成果

专业讲,负责 DAG 任务切分,任务提交监控,同时监控其他 master 和 worker的健康状态

采用 分布式无中心的设计理念

整体代码逻辑图:

image.png

从图中可知,整个 master 架构可分为三大模块,分别是核心定时任务处理客户端,接收请求服务端,stateEvent 处理中心。

一 client 客户端

image.png 逻辑图:

核心类 为 MasterSchedulerService,通过 slot 从数据库拉取 command,进行任务提交

WorkerflowExecuteThread 负责工作流的整个任务执行过程

ITaskProcessor 有多个实现类,不同类型的 Task,选用合适的 processor 进行task提交进 TaskPriorityQueue 队列中

如图:

image.png

二 server 服务端

核心处理对象为 TaskEvent

几大处理procesor,如图所示:

image.png

整体流程图如下:

image.png

TaksExecuteThread 中 TaskEvent 存储代码:

   public class TaskExecuteThread { 
   
   private static final Logger logger = LoggerFactory.getLogger(TaskExecuteThread.class); 
   
   private final int processInstanceId; 
   
   private final ConcurrentLinkedQueue<TaskEvent> events = new ConcurrentLinkedQueue<>(); 
   }

WorkflowExecuteThread 中 StateEvent 存储代码逻辑:

   private ConcurrentLinkedQueue<StateEvent> stateEvents = new ConcurrentLinkedQueue<>(); 
   public void submitStateEvent(StateEvent stateEvent) { 
   WorkflowExecuteThread workflowExecuteThread = processInstanceExecCacheManager.getByProcessInstanceId(stateEvent.getProcessInstanceId()); 
   if (workflowExecuteThread == null) { 
   logger.warn("workflowExecuteThread is null, stateEvent:{}", stateEvent); return; 
   } 
   workflowExecuteThread.addStateEvent(stateEvent); 
   }

三 stateEvent 处理中心

作用:及时更新全局中的缓存,推进 process

全局的缓存有:

image.png

核心代码在 EventExecuteService

   public class EventExecuteService extends Thread { 
   @Autowired 
   private ProcessInstanceExecCacheManager processInstanceExecCacheManager; 
   /** * workflow exec service */ 
   @Autowired 
   private WorkflowExecuteThreadPool workflowExecuteThreadPool; 
   
   @Override 
   public synchronized void start() { 
   super.setName("EventServiceStarted"); 
   super.start(); 
   } 
   
   @Override 
   public void run() { 
   logger.info("Event service started"); 
   while (Stopper.isRunning()) { 
      try { 
            eventHandler();  
            TimeUnit.MILLISECONDS.sleep(Constants.SLEEP_TIME_MILLIS_SHORT); 
          } catch (Exception e) { 
          logger.error("Event service thread error", e); 
          } 
          } 
        } 
        
 private void eventHandler() { 
      for (WorkflowExecuteThread workflowExecuteThread : this.processInstanceExecCacheManager.getAll()) { 
      workflowExecuteThreadPool.executeEvent(workflowExecuteThread); 
      } 
     }
  }

WorkflowExecuteThread 依据 stateEvent 修改缓存,推进 process

   private boolean stateEventHandler(StateEvent stateEvent) { 
   logger.info("process event: {}", stateEvent.toString()); 
   if (!checkProcessInstance(stateEvent)) { 
       return false; 
    } 
    
    boolean result = false; 
    switch (stateEvent.getType()) { 
    case PROCESS_STATE_CHANGE: 
        result = processStateChangeHandler(stateEvent); 
        break; 
    case TASK_STATE_CHANGE: 
        // finish 的时候,开始后一个节点task 
        result = taskStateChangeHandler(stateEvent); 
        break; 
    case PROCESS_TIMEOUT: 
        result = processTimeout(); 
        break; 
    case TASK_TIMEOUT: 
        result = taskTimeout(stateEvent); 
        break; 
    case WAIT_TASK_GROUP: 
        result = checkForceStartAndWakeUp(stateEvent); 
        break; 
    case TASK_RETRY: 
        result = taskRetryEventHandler(stateEvent); 
        break; 
    case PROCESS_BLOCKED: 
        result = processBlockHandler(stateEvent); 
        break; 
    default: 
        break; 
    } 
    
    if (result) { 
    this.stateEvents.remove(stateEvent); 
    } 
    return result; 
  }

StateWheelExecuteThread 健康检查task,process状态

   public class StateWheelExecuteThread extends Thread { 
   @Override 
   public void run() { 
    while (Stopper.isRunning()) { 
      try { 
      //检查task是否超时 
      checkTask4Timeout(); 
      //检查 task 是否需要重试 
      checkTask4Retry(); 
     //检查 task 是否需要更改state 
     checkTask4State(); 
     //检查 process 是否超时 
     checkProcess4Timeout(); 
     } catch (Exception e) { 
     logger.error("state wheel thread check error:", e); 
     } 
     ThreadUtil.sleepAtLeastIgnoreInterrupts((long) masterConfig.getStateWheelInterval() * Constants.SLEEP_TIME_MILLIS); } }

worker 模块

通俗讲,负责真正干活

专业讲,主要负责任务的执行和提供日志服务。

注册服务于 ZK:workerRegistryClient   采用 分布式无中心的设计理念

整体代码逻辑图:

image.png

1 master 根据 commandType 从 NettyClientHandler 中获取一个 对应的 NettyRequeProcessor,从 zk 中获取该 处理器的ip,请求发送过去

2 任务逻辑真正执行代码 TaskExecuteThread.run() --→  创建 task -→ 执行 task.handle()

逻辑详解:

背景:

框架中,支持多种类型的 Task,例如 sql,python,shell 等等...

image.png

备注:AbstractTask 抽象接口,统一定义执行方法名称 handle()

 AbstractTaskExecutor 负责打印日志,参数校验等

框架具体实现 一个 AbstractTask ,从 上下文变量 TaskExecutorContent 中取参数创建

//工厂模式 + 策略模式 //两层工厂 
TaskChannel taskChannel = taskPluginManager.getTaskChannelMap().get(taskExecutionContext.getTaskType()); AbstractTask task = taskChannel.createTask(taskExecutionContext); 
//task实际执行逻辑 
this.task.handle(); 
//表示该任务执行成功,不修改状态,则默认失败 
task.setExitStatusCode(0);

自定义插件分享

这里分享一个 自定义插件,插入 dolphinscheduler使用的案例。

step 一

在 dolphinscheduler-task-plugin 下新建一个子插件模块

其中实现 TaskChannelFactory 和 TaskChannel

@AutoService(TaskChannelFactory.class)
public class XXXTaskChannelFactory implements TaskChannelFactory {
    @Override
    public String getName() {
        return "XXX";
    }

    @Override
    public List<PluginParams> getParams() {
        return null;
    }

    @Override
    public TaskChannel create() {
        return new XXXTaskChannel();
    }
}

public class XXXTaskChannel implements TaskChannel {
    @Override
    public void cancelApplication(boolean status) {

    }

    @Override
    public AbstractTask createTask(TaskExecutionContext taskRequest) {
        return new XXXTask(taskRequest);
    }

    @Override
    public AbstractParameters parseParameters(ParametersNode parametersNode) {
        return JSONUtils.parseObject(parametersNode.getTaskParams(), XXXParameters.class);
    }

    @Override
    public ResourceParametersHelper getResources(String parameters) {
        return null;
    }
}

step 二

实现 AbstractTask 具体执行核心的逻辑在 重新 实现 handle() 方法里

   public class XXXTask extends AbstractTaskExecutor {
  
     @Override
     public void init() {
     }
     
   
     //具体执行逻辑
     @Override
     public void handle() throws Exception {
        .......
     }
     
     @Override
     public AbstractParameters getParameters() {
          return this.XXXParameters;
     }

step 三

修改 总插件 pom 依赖文件 dolphinscheduler-task-all

<dependency>
    <groupId>org.apache.dolphinscheduler</groupId>
    <artifactId>dolphinscheduler-task-XXX</artifactId>
    <version>${project.version}</version>
</dependency>

总结

总的来看,dolphinscheduler 的核心逻辑还是在 master 模块上,理解起来略微有些费劲。但是其中的一些设计方法还是值得好好学习的,例如用发布事件,异步推动工作流的进展等设计。

公司后期打算运用dolphinscheduler,统一公司的大部分项目接口的用法,下线现有的一些内部服务,全部改为从dolphinscheduler 入口执行。个人觉得自定义插件功能还是比较简单的。