Apache DolphinScheduler介绍
Apache DolphinScheduler 是一个分布式易扩展的可视化DAG工作流任务调度开源系统。适用于企业级场景,提供了一个可视化操作任务、工作流和全生命周期数据处理过程的解决方案。
要适应于企业级场景,两个特色必不可少
High Reliability
高可靠性: 去中心化设计,确保稳定性。 原生 HA 任务队列支持,提供过载容错能力。 DolphinScheduler 能提供高度稳健的环境。
High Scalability
高扩展性: 支持多租户和在线资源管理。支持每天10万个数据任务的稳定运行。
中心化和去中心化
中心化思想
中心化的设计理念比较简单,分布式集群中的节点按照角色分工,大体上分为两种角色
Master
Master的角色主要负责任务分发并监督Worker的健康状态,可以动态的将任务均衡到Worker上,以致Worker节点不至于“忙死”或”闲死”的状态。
Worker
Worker的角色主要负责任务的执行工作并维护和Master的心跳,以便Master可以分配任务给Worker。
中心化思想设计存在的问题:
一旦Master出现了问题,则群龙无首,整个集群就会崩溃。为了解决这个问题,大多数Master/Slave架构模式都采用了主备Master的设计方案,可以是热备或者冷备,也可以是自动切换或手动切换,而且越来越多的新系统都开始具备自动选举切换Master的能力,以提升系统的可用性。
另外一个问题是如果Scheduler在Master上,虽然可以支持一个DAG中不同的任务运行在不同的机器上,但是会产生Master的过负载。如果Scheduler在Slave上,则一个DAG中所有的任务都只能在某一台机器上进行作业提交,则并行任务比较多的时候,Slave的压力可能会比较大。
去中心化
- 在去中心化设计里,通常没有Master/Slave的概念,所有的角色都是一样的,地位是平等的,全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备down机,都只会影响很小范围的功能。
- 去中心化设计的核心设计在于整个分布式系统中不存在一个区别于其他节点的”管理者”,因此不存在单点故障问题。但由于不存在” 管理者”节点所以每个节点都需要跟其他节点通信才得到必须要的机器信息,而分布式系统通信的不可靠性,则大大增加了上述功能的实现难度。
- 实际上,真正去中心化的分布式系统并不多见。反而动态中心化分布式系统正在不断涌出。在这种架构下,集群中的管理者是被动态选择出来的,而不是预置的,并且集群在发生故障的时候,集群的节点会自发的举行"会议"来选举新的"管理者"去主持工作。最典型的案例就是ZooKeeper及Go语言实现的Etcd。
- DolphinScheduler的去中心化是Master/Worker注册心跳到Zookeeper中,Master基于slot处理各自的Command,通过selector分发任务给worker,实现Master集群和Worker集群无中心。
Master Server去中心化
去中心化的设计理念是Token Ring,一组节点提供服务,每个节点被分配一个slot,所有的节点组成一个收尾相顾的圆环,对于一个待分配的任务,对任务id进行hash取模,模因子为slot num,模值就是要分配的任务节点索引
在dolphin scheduler 提供了3中动态发现的Registry机制,分别是
- EtcdRegistry
- JdbcRegistry
- ZookeeperRegistry
具体使用哪个,可以根据不同的配置激活
zookeeper
@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "zookeeper")
etcd
@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "etcd")
jdbc
@ConditionalOnProperty(prefix = "registry", name = "type", havingValue = "jdbc")
master节点的slot获取,需要三个核心的service
MasterRegistryClient
提供了心跳机制,心跳信息中包含了机器的状态
- busy
- normal
当心跳发生变更时,会过滤掉busy状态的节点,这样对过载的机器起到了保护的作用
判断机器是否过载的标准的阈值为0.7
private double maxCpuUsagePercentageThresholds = 0.7;
private double maxJVMMemoryUsagePercentageThresholds = 0.7;
private double maxSystemMemoryUsagePercentageThresholds = 0.7;
private double maxDiskUsagePercentageThresholds = 0.7;
同时监听master和worker的状态,当master或者worker server down掉后,执行failover后续处理
ServerNodeManager
ServerNodeManager维护了两个重要的listener list, workerInfoChangeListeners和masterInfoChangeListeners,所有对master和worker节点状态的信息都可以使用这个接口。
ServerNodeManager是一个工具性service,不参与具体的业务逻辑。
ServerNodeManager同时也会对master和worker的状态变化进行重要监控,可以修改
全局报警组
default admin warning group
global alert group
这样后续的master或者worker节点down掉,就能够及时发现报警
看最新版只对机器down掉进行通知,可以改一下源代码,增加对master和worker的恢复进行通知
MasterSlotManager
MasterSlotManager是维护slot的核心service
这包含两个核心变量,这两个变量是维护去中心化设计实现的核心抓手
private volatile int currentSlot = 0;
private volatile int totalSlot = 0;
这两个变量的获取也是非常简单的,拿到所有的master节点,过滤掉busy状态的节点
设置totalSlot为状态为normal的 master节点数量
设置currentSlot为当前机器所在的索引
源代码
private void syncMasterNodes(List<Server> masterNodes) {
slotLock.lock();
try {
this.masterPriorityQueue.clear();
this.masterPriorityQueue.putAll(masterNodes);
int tempCurrentSlot = masterPriorityQueue.getIndex(masterConfig.getMasterAddress());
int tempTotalSlot = masterNodes.size();
if (tempCurrentSlot < 0) {
totalSlot = 0;
currentSlot = 0;
log.warn("Current master is not in active master list");
} else if (tempCurrentSlot != currentSlot || tempTotalSlot != totalSlot) {
totalSlot = tempTotalSlot;
currentSlot = tempCurrentSlot;
log.info("Update master nodes, total master size: {}, current slot: {}", totalSlot, currentSlot);
}
} finally {
slotLock.unlock();
}
}
slot管理流程如下
这个流程有一个小小的bug
// remove before persist
registryClient.remove(masterRegistryPath);
registryClient.persistEphemeral(masterRegistryPath, JSONUtils.toJsonString(masterHeartBeatTask.getHeartBeat()));
while (!registryClient.checkNodeExists(NetUtils.getHost(), RegistryNodeType.MASTER)) {
log.warn("The current master server node:{} cannot find in registry", NetUtils.getHost());
ThreadUtils.sleep(SLEEP_TIME_MILLIS);
}
如果persistEphemeral失败,会一直sleep在这里,不过这种情况貌似是不是很少出现,要不官方也会解决这个问题。
master轮训执行任务
DolphinScheduler使用mysql表t_ds_command作为分布式任务队列,每个master节点轮训请求t_ds_command获取待执行的任务
poll from the queue 语句为
<select id="queryCommandPageBySlot" resultType="org.apache.dolphinscheduler.dao.entity.Command">
select *
from t_ds_command
where id % #{masterCount} = #{thisMasterSlot}
order by process_instance_priority, id asc
limit #{limit}
</select>
看起来,是优先执行优先级高的任务,sql语句中thisMasterSlot是本机在list<master>中的索引
masterCount为正常的master数量
t_ds_command建表语句为
CREATE TABLE `t_ds_command` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'key',
`command_type` tinyint(4) DEFAULT NULL COMMENT 'Command type: 0 start workflow, 1 start execution from current node, 2 resume fault-tolerant workflow, 3 resume pause process, 4 start execution from failed node, 5 complement, 6 schedule, 7 rerun, 8 pause, 9 stop, 10 resume waiting thread',
`process_definition_code` bigint(20) NOT NULL COMMENT 'process definition code',
`process_definition_version` int(11) DEFAULT '0' COMMENT 'process definition version',
`process_instance_id` int(11) DEFAULT '0' COMMENT 'process instance id',
`command_param` text COLLATE utf8_bin COMMENT 'json command parameters',
`task_depend_type` tinyint(4) DEFAULT NULL COMMENT 'Node dependency type: 0 current node, 1 forward, 2 backward',
`failure_strategy` tinyint(4) DEFAULT '0' COMMENT 'Failed policy: 0 end, 1 continue',
`warning_type` tinyint(4) DEFAULT '0' COMMENT 'Alarm type: 0 is not sent, 1 process is sent successfully, 2 process is sent failed, 3 process is sent successfully and all failures are sent',
`warning_group_id` int(11) DEFAULT NULL COMMENT 'warning group',
`schedule_time` datetime DEFAULT NULL COMMENT 'schedule time',
`start_time` datetime DEFAULT NULL COMMENT 'start time',
`executor_id` int(11) DEFAULT NULL COMMENT 'executor id',
`update_time` datetime DEFAULT NULL COMMENT 'update time',
`process_instance_priority` int(11) DEFAULT '2' COMMENT 'process instance priority: 0 Highest,1 High,2 Medium,3 Low,4 Lowest',
`worker_group` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT 'worker group',
`tenant_code` varchar(64) COLLATE utf8_bin DEFAULT 'default' COMMENT 'tenant code',
`environment_code` bigint(20) DEFAULT '-1' COMMENT 'environment code',
`dry_run` tinyint(4) DEFAULT '0' COMMENT 'dry run flag?0 normal, 1 dry run',
`test_flag` tinyint(4) DEFAULT NULL COMMENT 'test flag?0 normal, 1 test run',
PRIMARY KEY (`id`),
KEY `priority_id_index` (`process_instance_priority`,`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COLLATE=utf8_bin
command里面也有一个资源控制字段worker_group,
任务组主要用于控制任务实例并发,旨在控制其他资源的压力(也可以控制 Hadoop 集群压力,不过集群会有队列管控)。您可在新建任务定义时,可配置对应的任务组,并配置任务在任务组内运行的优先级。用户仅能查看有权限的项目对应的任务组,且仅能创建或修改具有写权限的项目对应的任务组。
注意:任务组的对资源的限制是在项目级别的,和租户没有关系
关于任务组的配置,您需要做的只需要配置红色框内的部分,其中:
【任务组名称】:任务组配置页面显示的任务组名称,这里只能看到该项目有权限的任务组(新建任务组时选择了该项目),或作用在全局的任务组(新建任务组时没有选择项目)
【组内优先级】:在出现等待资源时,优先级高的任务会最先被 master 分发给 worker 执行,该部分数值越大,优先级越高。
任务组的实现逻辑
获取任务组资源:
Master 在分发任务时判断该任务是否配置了任务组,如果任务没有配置,则正常抛给 worker 运行;如果配置了任务组,在抛给 worker 执行之前检查任务组资源池剩余大小是否满足当前任务运行,如果满足资源池 -1,继续运行;如果不满足则退出任务分发,等待其他任务结束唤醒。
释放与唤醒:
当获取到任务组资源的任务结束运行后,会释放任务组资源,释放后会检查当前任务组是否有任务等待,如果有则标记优先级最好的任务可以运行,并新建一个可以执行的event。该event中存储着被标记可以获取资源的任务id,随后在获取任务组资源然后运行。
任务组执行流程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。