2021-11-15
2021SC@SDUSC
2021-11-15-DolphinScheduler(8)
1.run
run之中大多是线程初始化、提交的逻辑。
// start QuartzExecutors
// what system should do if exception
try {
ProcessScheduleJob.init(processDao);
QuartzExecutors.getInstance().start();
} catch (Exception e) {
try {
QuartzExecutors.getInstance().shutdown();
} catch (SchedulerException e1) {
logger.error("QuartzExecutors shutdown failed : " + e1.getMessage(), e1);
}
logger.error("start Quartz failed : " + e.getMessage(), e);
}
/**
* register hooks, which are called before the process exits
*/
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
if (zkMasterClient.getActiveMasterNum() <= 1) {
for (int i = 0; i < Constants.DOLPHINSCHEDULER_WARN_TIMES_FAILOVER; i++) {
zkMasterClient.getAlertDao().sendServerStopedAlert(
1, OSUtils.getHost(), "Master-Server");
}
}
stop("shutdownhook");
}
}));
ProcessScheduleJob.init就是给ProcessScheduleJob一个static字段赋值,也就是给所有的ProcessScheduleJob一个全局的processDao
public static void init(ProcessDao processDao) {
ProcessScheduleJob.processDao = processDao;
}
源码中processDao的处理有些是传参,有些是getBean,有些又是全局变量。
addShutdownHook的逻辑就比较清晰了,就是添加了进程退出的hook。
先发送预警信息,然后调用stop进行退出了。
FetchTaskThread
worker涉及的线程主要有两个FetchTaskThread、TaskScheduleThread。
FetchTaskThread从名称上来看,是从zk队列拉取任务信息的。
它也是Runnable的一个实现类,就从run入手。
@Override
public void run() {
while (Stopper.isRunning()){
InterProcessMutex mutex = null;
try {
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) workerExecService;
//check memory and cpu usage and threads
boolean runCheckFlag = OSUtils.checkResource(this.conf, false) && checkThreadCount(poolExecutor);
Thread.sleep(Constants.SLEEP_TIME_MILLIS);
if(!runCheckFlag) {
continue;
}
//whether have tasks, if no tasks , no need lock //get all tasks
List<String> tasksQueueList = taskQueue.getAllTasks(Constants.DOLPHINSCHEDULER_TASKS_QUEUE);
if (CollectionUtils.isEmpty(tasksQueueList)){
continue;
}
// creating distributed locks, lock path /dolphinscheduler/lock/worker
mutex = zkWorkerClient.acquireZkLock(zkWorkerClient.getZkClient(),
zkWorkerClient.getWorkerLockPath());
// task instance id str
List<String> taskQueueStrArr = taskQueue.poll(Constants.DOLPHINSCHEDULER_TASKS_QUEUE, taskNum);
for(String taskQueueStr : taskQueueStrArr){
if (StringUtils.isEmpty(taskQueueStr)) {
continue;
}
if (!checkThreadCount(poolExecutor)) {
break;
}
// get task instance id
taskInstId = getTaskInstanceId(taskQueueStr);
// mainly to wait for the master insert task to succeed
waitForTaskInstance();
taskInstance = processDao.getTaskInstanceDetailByTaskId(taskInstId);
// verify task instance is null
if (verifyTaskInstanceIsNull(taskInstance)) {
logger.warn("remove task queue : {} due to taskInstance is null", taskQueueStr);
removeNodeFromTaskQueue(taskQueueStr);
continue;
}
Tenant tenant = processDao.getTenantForProcess(taskInstance.getProcessInstance().getTenantId(),
taskInstance.getProcessDefine().getUserId());
// verify tenant is null
if (verifyTenantIsNull(tenant)) {
logger.warn("remove task queue : {} due to tenant is null", taskQueueStr);
removeNodeFromTaskQueue(taskQueueStr);
continue;
}
// set queue for process instance, user-specified queue takes precedence over tenant queue
String userQueue = processDao.queryUserQueueByProcessInstanceId(taskInstance.getProcessInstanceId());
taskInstance.getProcessInstance().setQueue(StringUtils.isEmpty(userQueue) ? tenant.getQueue() : userQueue);
taskInstance.getProcessInstance().setTenantCode(tenant.getTenantCode());
logger.info("worker fetch taskId : {} from queue ", taskInstId);
if(!checkWorkerGroup(taskInstance, OSUtils.getHost())){
continue;
}
// local execute path
String execLocalPath = getExecLocalPath();
logger.info("task instance local execute path : {} ", execLocalPath);
// init task
taskInstance.init(OSUtils.getHost(),
new Date(),
execLocalPath);
// check and create Linux users
FileUtils.createWorkDirAndUserIfAbsent(execLocalPath,
tenant.getTenantCode(), logger);
logger.info("task : {} ready to submit to task scheduler thread",taskInstId);
// submit task
workerExecService.submit(new TaskScheduleThread(taskInstance, processDao));
// remove node from zk
removeNodeFromTaskQueue(taskQueueStr);
}
}catch (Exception e){
logger.error("fetch task thread failure" ,e);
}finally {
AbstractZKClient.releaseMutex(mutex);
}
}
run还是一个while“死循环”,首先检查了当前资源是否超阈值、线程数是否够用。
然后获取runCheckFlag标志,再之后休眠1秒,再判断前面的结果,
如果为false则进入下一个循环。
调用taskQueue.getAllTasks获取当前所有的任务列表,为空则进入下一次循环
申请InterProcessMutex锁,这样同时刻只有一个worker节点从队列中poll任务
这意味着,任务会随机的在worker节点执行。
抢占到锁之后,每次poll固定数量的任务。
之前TaskQueue中插入的数据
${processInstancePriority}_${processInstanceId}_${taskInstancePriority}_${taskId}_host1,host2...
获取到任务列表之后,就是一个for循环,依次处理任务。下面简单总结一下其逻辑。
判断taskQueueStr是否为空。感觉有点多此一举。
判断当前线程数是否够用
从taskQueueStr中取到任务ID。就是按照_分隔之后的第四个字段。
等待任务实例信息插入到数据库。循环30次,每次等待1秒。注释说数据库操作会被延迟,不知道哪里会延迟。
通过任务id,获取任务实例信息。
通过任务实例,获取租户信息。
通过任务实例,获取用户队列信息。为啥不在查询任务实例信息的时候,直接获取到呢?或者在getTaskInstanceDetailByTaskId一次性获取到?
判断任务实例是否可以在当前节点执行,不能则继续下一个任务处理。这为啥不提前判断呢?调了2次db查询才来判断?
任务实例初始化
检查目录、用户是否存在。不存在则创建用户、目录。为啥不是提前建好?每次还要检查一遍。
提交任务,交给TaskScheduleThread线程执行。
删除taskQueue中对应的任务节点。
FetchTaskThread功能就是抢占zk锁,从TaskQueue获取任务,然后创建TaskScheduleThread线程去执行。
TaskScheduleThread
TaskScheduleThread的功能应该是要执行具体的逻辑。
@Override
public void run() {
try {
// update task state is running according to task type
updateTaskState(taskInstance.getTaskType());
logger.info("script path : {}", taskInstance.getExecutePath());
// task node
TaskNode taskNode = JSONObject.parseObject(taskInstance.getTaskJson(), TaskNode.class);
// copy hdfs/minio file to local
copyHdfsToLocal(processDao,
taskInstance.getExecutePath(),
createProjectResFiles(taskNode),
logger);
// get process instance according to tak instance
ProcessInstance processInstance = taskInstance.getProcessInstance();
// set task props
TaskProps taskProps = new TaskProps(taskNode.getParams(),
taskInstance.getExecutePath(),
processInstance.getScheduleTime(),
taskInstance.getName(),
taskInstance.getTaskType(),
taskInstance.getId(),
CommonUtils.getSystemEnvPath(),
processInstance.getTenantCode(),
processInstance.getQueue(),
taskInstance.getStartTime(),
getGlobalParamsMap(),
taskInstance.getDependency(),
processInstance.getCmdTypeIfComplement());
// set task timeout
setTaskTimeout(taskProps, taskNode);
taskProps.setTaskAppId(String.format("%s_%s_%s",
taskInstance.getProcessDefine().getId(),
taskInstance.getProcessInstance().getId(),
taskInstance.getId()));
// custom logger
Logger taskLogger = LoggerFactory.getLogger(LoggerUtils.buildTaskId(LoggerUtils.TASK_LOGGER_INFO_PREFIX,
taskInstance.getProcessDefine().getId(),
taskInstance.getProcessInstance().getId(),
taskInstance.getId()));
task = TaskManager.newTask(taskInstance.getTaskType(),
taskProps,
taskLogger);
// task init
task.init();
// task handle
task.handle();
// task result process
task.after();
}catch (Exception e){
logger.error("task scheduler failure", e);
kill();
// update task instance state
processDao.changeTaskState(ExecutionStatus.FAILURE,
new Date(),
taskInstance.getId());
}
logger.info("task instance id : {},task final status : {}",
taskInstance.getId(),
task.getExitStatus());
// update task instance state
processDao.changeTaskState(task.getExitStatus(),
new Date(),
taskInstance.getId());
}
还是一步步分析run方法。
更新任务状态为ExecutionStatus.RUNNING_EXEUTION
从任务实例获取任务节点信息。
从HDFS复制文件到本地。包括一些用户上传的资源文件,jar包、SQL文件、配置文件等等。
构造TaskProps对象。
初始化任务日志对象。
构造AbstractTask实例
依次调用AbstractTask的init、handle、after。
更新任务实例的状态。异常失败或成功等。
TaskManager.newTask
TaskManager.newTask还是比较重要的,它创建了最终的、具体的、可执行的任务实例。
public static AbstractTask newTask(String taskType, TaskProps props, Logger logger)
throws IllegalArgumentException {
switch (EnumUtils.getEnum(TaskType.class,taskType)) {
case SHELL:
return new ShellTask(props, logger);
case PROCEDURE:
return new ProcedureTask(props, logger);
case SQL:
return new SqlTask(props, logger);
case MR:
return new MapReduceTask(props, logger);
case SPARK:
return new SparkTask(props, logger);
case FLINK:
return new FlinkTask(props, logger);
case PYTHON:
return new PythonTask(props, logger);
case DEPENDENT:
return new DependentTask(props, logger);
case HTTP:
return new HttpTask(props, logger);
default:
logger.error("unsupport task type: {}", taskType);
throw new IllegalArgumentException("not support task type");
}
至此,终于找到了前端配置任务的具体实现类。其实吧,这个异常抛的没有道理。这个taskType肯定是保存在数据库的,保存之前应该做校验了吧,毕竟是enum转过去的。