DolphinScheduler 1.2.0 源码解析之 MasterServer
这一篇主要讲解的是dolphinscheduler 1.2.0 的master部分的源码,从主类MasterServer开始,从启动到运行,master主要做了以下三件事情
Zookeeper 节点初始化
构建并提交工作流实例,跟踪运行状态
监控其他MasterServer和WorkerServer的健康状态并容错
维系心跳
@PostConstruct
public void run(){
//详情见1.zookeeper初始化
zkMasterClient.init();
//详情见2.MasterSchedulerThread线程
masterSchedulerService = ThreadUtils.newDaemonSingleThreadExecutor("Master-Scheduler-Thread");
//详情见3.heartBeatThread线程
heartbeatMasterService = ThreadUtils.newDaemonThreadScheduledExecutor("Master-Main-Thread",Constants.DEFAULT_MASTER_HEARTBEAT_THREAD_NUM);
}
1. zookeeper初始化
创建DS在zookeeper的相关节点,并判断是否对系统做failover,恢复异常的工作流实例和任务实例。
用于master的failover /dolphinscheduler/lock/failover/master
系统节点,保存master和worker的心跳信息 /dolphinscheduler/masters; /dolphinscheduler/workers;/dolphinscheduler/dead-servers
public void init(){
logger.info("initialize master client...");
this.initDao();
InterProcessMutex mutex = null;
try {
// 创建分布式锁节点,用于master节点的failover
String znodeLock = getMasterStartUpLockPath();
mutex = new InterProcessMutex(zkClient, znodeLock);
mutex.acquire();
// 在ZK中初始化系统节点,
this.initSystemZNode();
// 向ZK的/masters节点注册当前的master信息
this.registerMaster();
// 通过监听Zookeeper临时节点变化来进行容错处理(如果活跃的master只有自身一个,则进行failover)
if (getActiveMasterNum() == 1) {
failoverWorker(null, true); // 恢复任务实例 详情见1.1.
failoverMaster(null); // 恢复工作流实例 详情见1.2.
}
}catch (Exception e){
logger.error("master start up exception",e);
}finally {
releaseMutex(mutex);
}
}
1.1. failoverWorker 恢复任务实例
private void failoverWorker(String workerHost, boolean needCheckWorkerAlive) throws Exception {
logger.info("start worker[{}] failover ...", workerHost);
List<TaskInstance> needFailoverTaskInstanceList = processService.queryNeedFailoverTaskInstances(workerHost);
for(TaskInstance taskInstance : needFailoverTaskInstanceList){
if(needCheckWorkerAlive){
if(!checkTaskInstanceNeedFailover(taskInstance)){
// 不需要failover的两种情况
// 1.任务详情中不存在host信息
// 2.任务在ZK中存在,则判断启动时间是否小于worker启动时间,小于则不用failover
continue;
}
}
ProcessInstance instance = processService.findProcessInstanceDetailById(taskInstance.getProcessInstanceId());
if(instance!=null){
taskInstance.setProcessInstance(instance);
}
// 如果任务中有yarn的任务则杀掉,kill的方式,日志中用正则匹配containId的格式,获取containID,用yarn命令kill。
ProcessUtils.killYarnJob(taskInstance);
// 把任务的状态从“running”改为“need failover”
taskInstance.setState(ExecutionStatus.NEED_FAULT_TOLERANCE);
processService.saveTaskInstance(taskInstance);
}
logger.info("end worker[{}] failover ...", workerHost);
}
1.2. failoverMaster 恢复工作流实例
private void failoverMaster(String masterHost) {
logger.info("start master failover ...");
// 获取需要failover的工作流实例
List<ProcessInstance> needFailoverProcessInstanceList = processService.queryNeedFailoverProcessInstances(masterHost);
for(ProcessInstance processInstance : needFailoverProcessInstanceList){
// 1.更新工作流实例的host为null
// 2.写入 t_ds_commond 表一条恢复工作流实例的命令
processService.processNeedFailoverProcessInstances(processInstance);
}
logger.info("master failover end");
}
2. MasterSchedulerThread 线程
该线程主要对command进行解析生成工作流实例
public void run() {
logger.info("master scheduler start successfully...");
while (Stopper.isRunning()){
// process instance
ProcessInstance processInstance = null;
InterProcessMutex mutex = null;
try {
boolean runCheckFlag = OSUtils.checkResource(masterConfig.getMasterMaxCpuloadAvg(), masterConfig.getMasterReservedMemory());
if(!runCheckFlag) {
Thread.sleep(Constants.SLEEP_TIME_MILLIS);
continue;
}
if (zkMasterClient.getZkClient().getState() == CuratorFrameworkState.STARTED) {
// 创建分布式锁 /dolphinscheduler/lock/masters
String znodeLock = zkMasterClient.getMasterLockPath();
mutex = new InterProcessMutex(zkMasterClient.getZkClient(), znodeLock);
mutex.acquire();
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) masterExecService;
int activeCount = poolExecutor.getActiveCount();
// 需要确保实例构建存储过程和command数据从表中删除的过程在一个事务中
Command command = processService.findOneCommand();
if (command != null) {
logger.info("find one command: id: {}, type: {}", command.getId(),command.getCommandType());
try{
// handleCommand将commond解析成processInstance 详情见2.1
processInstance = processService.handleCommand(logger, OSUtils.getHost(), this.masterExecThreadNum - activeCount, command);
if (processInstance != null) {
logger.info("start master exec thread , split DAG ...");
// masterExecService,master执行线程 详情见 2.2
masterExecService.execute(new MasterExecThread(processInstance, processService));
}
}catch (Exception e){
logger.error("scan command error ", e);
processService.moveToErrorCommand(command, e.toString());
}
} else{
//indicate that no command ,sleep for 1s
Thread.sleep(Constants.SLEEP_TIME_MILLIS);
}
}
}catch (Exception e){
logger.error("master scheduler thread exception",e);
}finally{
AbstractZKClient.releaseMutex(mutex);
}
}
logger.info("master server stopped...");
}
2.1. handleCommand
根据command对象构建工作流实例,构建后把该条command从t_ds_command表中删除,需要确保的是实例构建存储过程和command数据从表中删除的过程在一个事务中。
command所有类型如下
0 start a new process
1 start a new process from current nodes
2 recover tolerance fault process
3 recover suspended process
4 start process from failure task nodes
5 complement data
6 start a new process from scheduler
7 repeat running a process
8 pause a process
9 stop a process
10 recover waiting thread
@Transactional(rollbackFor = Exception.class)
public ProcessInstance handleCommand(Logger logger, String host, int validThreadNum, Command command) {
//根据command命令生成新的工作流程实例
ProcessInstance processInstance = constructProcessInstance(command, host);
//cannot construct process instance, return null;
if(processInstance == null){
logger.error("scan command, command parameter is error: %s", command.toString());
moveToErrorCommand(command, "process instance is null");
return null;
}
if(!checkThreadNum(command, validThreadNum)){
logger.info("there is not enough thread for this command: {}",command.toString() );
return setWaitingThreadProcess(command, processInstance);
}
processInstance.setCommandType(command.getCommandType());
processInstance.addHistoryCmd(command.getCommandType());
saveProcessInstance(processInstance);
this.setSubProcessParam(processInstance);
//保存了任务流实例后将该命令删除
delCommandByid(command.getId());
return processInstance;
}
2.2. MasterExecThread 执行线程
public void run() {
......
try {
//检查此过程是否是补数 且 流程实例是否为子流程
if (processInstance.isComplementData() && Flag.NO == processInstance.getIsSubProcess()){
// 详情见2.2.2. 执行补数
executeComplementProcess();
}else{
//详情见2.2.1. 执行流程实例
executeProcess();
}
......
}
2.2.1. executeProcess() 执行流程实例
private void executeProcess() throws Exception {
//1.根据流程实例id查找有效的任务列表 initTaskQueue()
//2.构建DAG处理流程 buildFlowDag() 返回DAG对象,主要包括两个信息:vertex 点,即任务执行节点;edge 边,即任务之间的依赖关系
prepareProcess();
//提交并监控任务,直到工作流停止 详情见2.2.1.1
runProcess();
//当线程池不足以供流程实例使用时,创建恢复等待线程命令。
//子工作流程实例无需创建恢复命令。
//创建恢复等待线程命令并同时删除origin命令。
//如果存在recovery命令,则仅更新字段update_time
endProcess();
}
2.2.1.1. runProcess()提交并监控任务
submitPostNode方法传入父任务节点的名字,通过节点名,DAG,获取任务节点列表,并生成任务实例列表readyToSubmitTaskList
private void runProcess(){
submitPostNode(null);
submitStandByTask()方法里面会遍历任务实例列表readyToSubmitTaskList,判断任务实例的依赖关系,依赖项运行成功则会提交任务执行线程,失败则把当前节点状态改为失败。
if(canSubmitTaskToQueue()){
submitStandByTask();
}
try {
Thread.sleep(Constants.SLEEP_TIME_MILLIS);
} catch (InterruptedException e) {
logger.error(e.getMessage(),e);
}
updateProcessInstanceState();
}
logger.info("process:{} end, state :{}", processInstance.getId(), processInstance.getState());
}
submitStandByTask()最终会调用submitTaskExec,这里有个MasterBaseTaskExecThread线程 MasterBaseTaskExecThread线程有两个主要作用
用于把任务实例信息提交到数据库中submitTask()
把任务信息写进zookeeper队列 submitTaskToQueue(),后续worker会来认领任务。(节点命名方式:${processInstancePriority}_${processInstanceId}_${taskInstancePriority}_${taskInstanceId}_${task executed by ip1},${ip2}...)
另外MasterBaseTaskExecThread有两个子类,除了上面的两个作用外:
MasterTaskExecThread 任务执行完成后会把需要kill的任务信息写入zk队列中等待worker来kill任务。
SubProcessTaskExecThread 在当前工作流运行结束后会继续运行子工作流并做相关状态更新,子工作流完全完成才同步状态为子工作流的状态。
MasterBaseTaskExecThread线程异步提交,会把结果写入activeTaskNode。
private TaskInstance submitTaskExec(TaskInstance taskInstance) {
MasterBaseTaskExecThread abstractExecThread = null;
if(taskInstance.isSubProcess()){
abstractExecThread = new SubProcessTaskExecThread(taskInstance, processInstance);
}else {
abstractExecThread = new MasterTaskExecThread(taskInstance, processInstance);
}
Future<Boolean> future = taskExecService.submit(abstractExecThread);
activeTaskNode.putIfAbsent(abstractExecThread, future);
return abstractExecThread.getTaskInstance();
}
然后会遍历activeTaskNode,判断线程是否执行完成,若完成则移除该线程信息,再判断节点是否执行成功
for(Map.Entry<MasterBaseTaskExecThread,Future<Boolean>> entry: activeTaskNode.entrySet()) {
Future<Boolean> future = entry.getValue();
TaskInstance task = entry.getKey().getTaskInstance();
if(!future.isDone()){
continue;
}
// node monitor thread complete
activeTaskNode.remove(entry.getKey());
if(task == null){
this.taskFailedSubmit = true;
continue;
}
logger.info("task :{}, id:{} complete, state is {} ",
task.getName(), task.getId(), task.getState().toString());
// 如果节点成功,则继续提交任务节点
if(task.getState() == ExecutionStatus.SUCCESS){
completeTaskList.put(task.getName(), task);
submitPostNode(task.getName());
continue;
}
// 如果节点失败,先重试,然后再继续执行失败流程
if(task.getState().typeIsFailure()){
if(task.getState() == ExecutionStatus.NEED_FAULT_TOLERANCE){
this.recoverToleranceFaultTaskList.add(task);
}
if(task.taskCanRetry()){
addTaskToStandByList(task);
}else{
completeTaskList.put(task.getName(), task);
if( task.getTaskType().equals(TaskType.CONDITIONS.toString()) ||
haveConditionsAfterNode(task.getName())) {
submitPostNode(task.getName());
}else{
errorTaskList.put(task.getName(), task);
if(processInstance.getFailureStrategy() == FailureStrategy.END){
killTheOtherTasks();
}
}
}
continue;
}
// other status stop/pause
completeTaskList.put(task.getName(), task);
}
// send alert
2.2.2. executeComplementProcess() 执行补数流程实例
private void executeComplementProcess() throws Exception {
....
//根据调度的时间规则和补数的时间范围计算出需要补数的日期列表
int processDefinitionId = processInstance.getProcessDefinitionId();
List<Schedule> schedules = processService.queryReleaseSchedulerListByProcessDefinitionId(processDefinitionId);
List<Date> listDate = Lists.newLinkedList();
if(!CollectionUtils.isEmpty(schedules)){
for (Schedule schedule : schedules) {
listDate.addAll(CronUtils.getSelfFireDateList(startDate, endDate, schedule.getCrontab()));
}
}
//接下来是一个循环,用日期列表的每个日期执行一次
//以下三个方法同 2.2.1
....
prepareProcess();
....
runProcess();
....
endProcess();
3. heartBeatThread线程
每30秒上报一次心跳信息, 同时判断host是否在dead-servers节点下,即判断进程是否已经挂了。 进程正常则更新zookeeper的/dolphinscheduler/masters/${host}/ 下的节点名称,包括以下信息 ip, port ,cpUsage, memoryUsage, loadAverage, registerTIme, currentTime
private Runnable heartBeatThread(){
logger.info("start master heart beat thread...");
Runnable heartBeatThread = new Runnable() {
@Override
public void run() {
if(Stopper.isRunning()) {
// send heartbeat to zk
if (StringUtils.isBlank(zkMasterClient.getMasterZNode())) {
logger.error("master send heartbeat to zk failed: can't find zookeeper path of master server");
return;
}
zkMasterClient.heartBeatForZk(zkMasterClient.getMasterZNode(), Constants.MASTER_PREFIX);
}
}
};
return heartBeatThread;
}
DolphinScheduler 1.2.0 源码解析之 MasterServer的更多相关文章
- solr&lucene3.6.0源码解析(四)
本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...
- solr&lucene3.6.0源码解析(三)
solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...
- Heritrix 3.1.0 源码解析(三十七)
今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...
- Android事件总线(二)EventBus3.0源码解析
1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus ...
- solr&lucene3.6.0源码解析(二)
上文描述了solr3.6.0怎么采用maven管理的方式在eclipse中搭建开发环境,在solr中,为了提高搜索性能,采用了缓存机制,这里描述的是LRU缓存,这里用到了 LinkedHashMap类 ...
- solr&lucene3.6.0源码解析(一)
本文作为系列的第一篇,主要描述的是solr3.6.0开发环境的搭建 首先我们需要从官方网站下载solr的相关文件,下载地址为http://archive.apache.org/dist/luc ...
- apache mina2.0源码解析(一)
apache mina是一个基于java nio的网络通信框架,为TCP UDP ARP等协议提供了一致的编程模型:其源码结构展示了优秀的设计案例,可以为我们的编程事业提供参考. 依照惯例,首先搭建a ...
- EventBus3.0源码解析
本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...
- Retrofit2.0源码解析
欢迎访问我的个人博客 ,原文链接:http://wensibo.net/2017/09/05/retrofit/ ,未经允许不得转载! 今天是九月的第四天了,学校也正式开学,趁着大学最后一年的这大好时 ...
随机推荐
- TKE qGPU 通过 CRD 管理集群 GPU 卡资源
作者 刘旭,腾讯云高级工程师,专注容器云原生领域,有多年大规模 Kubernetes 集群管理经验,现负责腾讯云 GPU 容器的研发工作. 背景 目前 TKE 已提供基于 qGPU 的算力/显存强隔离 ...
- vsftp 详解
1.默认配置: 1>允许匿名用户和本地用户登陆. anonymous_enable=YES local_enable=YES2>匿名用户使用的登陆名为ftp或anonymo ...
- 【多线程与高并发原理篇:4_深入理解synchronized】
1. 前言 越是简单的东西,在深入了解后发现越复杂.想起了曾在初中阶段,语文老师给我们解说<论语>的道理,顺便给我们提了一句,说老子的无为思想比较消极,学生时代不要太关注.现在有了一定的生 ...
- asp.net core系列 77 webapi响应压缩
一.介绍 背景:目前在开发一个爬虫框架,使用了.net core webapi接口作为爬虫调用入口,在调用 webapi时发现爬虫耗时很短(1秒左右),但客户端获取响应时间却在3~4秒.对于这个问题考 ...
- 前端3JS2
内容概要 运算符 流程控制 三元运算符 函数 自定义对象 内置对象 JSON对象 正则对象 内容详情 运算符
- VSCode 安装以及初步使用教程
老样子先介绍一下VSCode(是什么?干什么?有什么用?好处是什么?等) VisualStudioCode(简称VSCode)是Microsoft开发的代码编辑器,它支持Windows,Linux和m ...
- python爬虫之企某科技JS逆向
python爬虫简单js逆向案例在学习时需要用到数据,学习了python爬虫知识,但是在用爬虫程序的时候就遇到了问题.具体如下,在查看请求数据时发现返回的数据是加密的信息,现将处理过程记录如下,以便大 ...
- 5-5配置Mysql复制 基于日志点的复制
配置MySQL复制 基于日志点的复制配置步骤 设置简单密码(可以选择不需要) set GLOBAL validate_password_length=6; set global validate_pa ...
- synchronized下的 i+=2 和 i++ i++执行结果居然不一样
起因 逛[博客园-博问]时发现了一段有意思的问题: 问题链接:https://q.cnblogs.com/q/140032/ 这段代码是这样的: import java.util.concurrent ...
- LVGL库入门教程01-移植到STM32(触摸屏)
LVGL库移植STM32 LVGL库简介 LVGL(Light and Versatile Graphics Library)是一个免费.开源的嵌入式图形库,可以创建丰富.美观的界面,具有许多可以自定 ...