联童科技是一家智能化母婴童产业平台,从事母婴童行业以及互联网技术多年,拥有丰富的母婴门店运营和系统开发经验,在会员经营和商品经营方面,能够围绕会员需求,深入场景,更贴近合作伙伴和消费者,提供最优服务产品,公司致力于以技术来驱动母婴童产业的发展,公司也希望借助于大数据为客户提供更多智能数据分析和决策分析,大数据是公司重点发展的一部分,公司从成立初期起就搭建了大数据团队,有了大数据团队后,大数据调度平台的构建自然是最基础也是最重要的环节。

一、为什么选择incubator-dolphinscheduler

1、incubator-dolphinscheduler是一个由国内公司发起的开源项目,中国本土社区成员非常活跃,更加容易去进行社区沟通,同时联童也希望能加入到这个社区中,一起把这个由本土成员为主成立的社区做的更好。

2、incubator-dolphinscheduler 能够支撑非常多的应用场景

  • 以DAG图的方式将Task按照任务的依赖关系关联起来,可实时可视化监控任务的运行状态
  • 支持丰富的任务类型:Shell、MR、Spark、SQL(mysql、postgresql、hive、sparksql),Python,Sub_Process、Procedure,flink,datax,sqoop,http等
  • 支持工作流定时调度、依赖调度、手动调度、手动暂停/停止/恢复,同时支持失败重试/告警、从指定节点恢复失败、Kill任务等操作
  • 支持工作流优先级、任务优先级及任务的故障转移及任务超时告警/失败
  • 支持工作流全局参数及节点自定义参数设置
  • 支持资源文件的在线上传/下载,管理等,支持在线文件创建、编辑
  • 支持任务日志在线查看及滚动、在线下载日志等
  • 实现集群HA,通过Zookeeper实现Master集群和Worker集群去中心化
  • 支持对Master/Worker cpu load,memory,cpu在线查看
  • 支持工作流运行历史树形/甘特图展示、支持任务状态统计、流程状态统计
  • 支持补数
  • 支持多租户
  • 支持国际化

其中DAG图 借鉴自spark ,在dolphinscheduler 一个工作流可以对应多个工作任务,每一个工作任务对应一个DAG中的节点。

3、incubator-dolphinscheduler在保证了高并发和高可用的设计时,架构思路也相对简单,技术架构中没有引入非常多的复杂技术组件,降低了学习和维护的成本。

incubator-dolphinscheduler在设计时,除了zookeeper外,没有引入太多复杂的技术组件。整个架构以zookeeper 作为集群管理,采用去中心化思想进行设计。

二、incubator-dolphinscheduler功能的不足

1、无法支持串行调度策略

incubator-dolphinscheduler 在一开始设计时,只支持并行调度,不支持串行调度,而在联童中,大部分场景都是需要串行运行的,也就是每一个工作流任务都只能有一个实例在运行,同一个工作流任务中必须要等前一个实例执行结束,下一个实例才能开始执行,这种场景大多出现在准实时任务中。

2、任务依赖不够强大,只能支持被动等待依赖执行成功,无法主动触发下游工作流实例运行

如下图所示,只能支持在创建任务时,被动去等待依赖执行成功,无法在当前任务执行成功后,主动去触发别的工作流任务执行。

 3、部分模块中用户体验不足,并且在数据量大时,部分模块数据查询性能较慢

 4、缺少比较完备的监控体系

在 incubator-dolphinscheduler 只提供了一些简单的监控,当有多大几千个任务在运行时,很难做到完备监控,更是缺少对每一个任务运行的性能分析。

三、我们对于incubator-dolphinscheduler的功能升级开发

1、增加串行调度的支持

如下图所示,我们在原有并行执行的基础上,增加了串行执行方式。

在串行执行时,我们还增加了串行执行的队列功能,每一任务都可以指定队列的长度大小。

2、增加主动触发下游工作流实例运行

如下图所示,我们在原有并行执行的基础上,增加主动触发下游一个或者多个工作流实例运行。

运行后效果如下:

3、一些较大的Bug修复

联童在使用 incubator-dolphinscheduler时,同样也踩过不少的坑,这里我们举其中一个例子,比如在内部使用时,同事反馈最多的问题就是调度任务的日志刷新不及时,有时候很久才能刷新出日志。后来经过源码分析,发现是源码中存在了一些不太健壮的处理导致了这个问题。

 incubator-dolphinscheduler 中AbstractCommandExecutor.java 部分源码

  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17.  
  18. package org.apache.dolphinscheduler.server.worker.task;
  19.  
  20. import static org.apache.dolphinscheduler.common.Constants.EXIT_CODE_FAILURE;
  21. import static org.apache.dolphinscheduler.common.Constants.EXIT_CODE_KILL;
  22. import static org.apache.dolphinscheduler.common.Constants.EXIT_CODE_SUCCESS;
  23.  
  24. import org.apache.dolphinscheduler.common.Constants;
  25. import org.apache.dolphinscheduler.common.enums.ExecutionStatus;
  26. import org.apache.dolphinscheduler.common.thread.Stopper;
  27. import org.apache.dolphinscheduler.common.thread.ThreadUtils;
  28. import org.apache.dolphinscheduler.common.utils.HadoopUtils;
  29. import org.apache.dolphinscheduler.common.utils.LoggerUtils;
  30. import org.apache.dolphinscheduler.common.utils.OSUtils;
  31. import org.apache.dolphinscheduler.common.utils.StringUtils;
  32. import org.apache.dolphinscheduler.server.entity.TaskExecutionContext;
  33. import org.apache.dolphinscheduler.server.utils.ProcessUtils;
  34. import org.apache.dolphinscheduler.server.worker.cache.TaskExecutionContextCacheManager;
  35. import org.apache.dolphinscheduler.server.worker.cache.impl.TaskExecutionContextCacheManagerImpl;
  36. import org.apache.dolphinscheduler.service.bean.SpringApplicationContext;
  37.  
  38. import java.io.BufferedReader;
  39. import java.io.File;
  40. import java.io.FileInputStream;
  41. import java.io.IOException;
  42. import java.io.InputStreamReader;
  43. import java.lang.reflect.Field;
  44. import java.nio.charset.StandardCharsets;
  45. import java.util.ArrayList;
  46. import java.util.Collections;
  47. import java.util.LinkedList;
  48. import java.util.List;
  49. import java.util.concurrent.ExecutorService;
  50. import java.util.concurrent.TimeUnit;
  51. import java.util.function.Consumer;
  52. import java.util.regex.Matcher;
  53. import java.util.regex.Pattern;
  54.  
  55. import org.slf4j.Logger;
  56.  
  57. /**
  58. * abstract command executor
  59. */
  60. public abstract class AbstractCommandExecutor {
  61. /**
  62. * rules for extracting application ID
  63. */
  64. protected static final Pattern APPLICATION_REGEX = Pattern.compile(Constants.APPLICATION_REGEX);
  65.  
  66. protected StringBuilder varPool = new StringBuilder();
  67. /**
  68. * process
  69. */
  70. private Process process;
  71.  
  72. /**
  73. * log handler
  74. */
  75. protected Consumer<List<String>> logHandler;
  76.  
  77. /**
  78. * logger
  79. */
  80. protected Logger logger;
  81.  
  82. /**
  83. * log list
  84. */
  85. protected final List<String> logBuffer;
  86.  
  87. /**
  88. * taskExecutionContext
  89. */
  90. protected TaskExecutionContext taskExecutionContext;
  91.  
  92. /**
  93. * taskExecutionContextCacheManager
  94. */
  95. private TaskExecutionContextCacheManager taskExecutionContextCacheManager;
  96.  
  97. public AbstractCommandExecutor(Consumer<List<String>> logHandler,
  98. TaskExecutionContext taskExecutionContext,
  99. Logger logger) {
  100. this.logHandler = logHandler;
  101. this.taskExecutionContext = taskExecutionContext;
  102. this.logger = logger;
  103. this.logBuffer = Collections.synchronizedList(new ArrayList<>());
  104. this.taskExecutionContextCacheManager = SpringApplicationContext.getBean(TaskExecutionContextCacheManagerImpl.class);
  105. }
  106.  
  107. /**
  108. * build process
  109. *
  110. * @param commandFile command file
  111. * @throws IOException IO Exception
  112. */
  113. private void buildProcess(String commandFile) throws IOException {
  114. // setting up user to run commands
  115. List<String> command = new LinkedList<>();
  116.  
  117. //init process builder
  118. ProcessBuilder processBuilder = new ProcessBuilder();
  119. // setting up a working directory
  120. processBuilder.directory(new File(taskExecutionContext.getExecutePath()));
  121. // merge error information to standard output stream
  122. processBuilder.redirectErrorStream(true);
  123.  
  124. // setting up user to run commands
  125. command.add("sudo");
  126. command.add("-u");
  127. command.add(taskExecutionContext.getTenantCode());
  128. command.add(commandInterpreter());
  129. command.addAll(commandOptions());
  130. command.add(commandFile);
  131.  
  132. // setting commands
  133. processBuilder.command(command);
  134. process = processBuilder.start();
  135.  
  136. // print command
  137. printCommand(command);
  138. }
  139.  
  140. ..........
  141.  
  142. /**
  143. * get the standard output of the process
  144. *
  145. * @param process process
  146. */
  147. private void parseProcessOutput(Process process) {
  148. String threadLoggerInfoName = String.format(LoggerUtils.TASK_LOGGER_THREAD_NAME + "-%s", taskExecutionContext.getTaskAppId());
  149. ExecutorService parseProcessOutputExecutorService = ThreadUtils.newDaemonSingleThreadExecutor(threadLoggerInfoName);
  150. parseProcessOutputExecutorService.submit(new Runnable() {
  151. @Override
  152. public void run() {
  153. BufferedReader inReader = null;
  154.  
  155. try {
  156. inReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
  157. String line;
  158.  
  159. long lastFlushTime = System.currentTimeMillis();
  160.  
  161. while ((line = inReader.readLine()) != null) {
  162. if (line.startsWith("${setValue(")) {
  163. varPool.append(line.substring("${setValue(".length(), line.length() - 2));
  164. varPool.append("$VarPool$");
  165. } else {
  166. logBuffer.add(line);
  167. lastFlushTime = flush(lastFlushTime);
  168. }
  169. }
  170. } catch (Exception e) {
  171. logger.error(e.getMessage(), e);
  172. } finally {
  173. clear();
  174. close(inReader);
  175. }
  176. }
  177. });
  178. parseProcessOutputExecutorService.shutdown();
  179. }
  180.  
  181. ................
  182.  
  183. /**
  184. * when log buffer siz or flush time reach condition , then flush
  185. *
  186. * @param lastFlushTime last flush time
  187. * @return last flush time
  188. */
  189. private long flush(long lastFlushTime) {
  190. long now = System.currentTimeMillis();
  191.  
  192. /**
  193. * when log buffer siz or flush time reach condition , then flush
  194. */
  195. if (logBuffer.size() >= Constants.DEFAULT_LOG_ROWS_NUM || now - lastFlushTime > Constants.DEFAULT_LOG_FLUSH_INTERVAL) {
  196. lastFlushTime = now;
  197. /** log handle */
  198. logHandler.accept(logBuffer);
  199.  
  200. logBuffer.clear();
  201. }
  202. return lastFlushTime;
  203. }
  204.  
  205. /**
  206. * close buffer reader
  207. *
  208. * @param inReader in reader
  209. */
  210. private void close(BufferedReader inReader) {
  211. if (inReader != null) {
  212. try {
  213. inReader.close();
  214. } catch (IOException e) {
  215. logger.error(e.getMessage(), e);
  216. }
  217. }
  218. }
  219.  
  220. protected List<String> commandOptions() {
  221. return Collections.emptyList();
  222. }
  223.  
  224. protected abstract String buildCommandFilePath();
  225.  
  226. protected abstract String commandInterpreter();
  227.  
  228. protected abstract void createCommandFileIfNotExists(String execCommand, String commandFile) throws IOException;
  229. }

在这段源码中,parseProcessOutput(Process process) 方法是负责任务日志的获取以及Flush。 但是由于采用了BufferedReader 中的readLine() 方法来读取任务进程的process.getInputStream()日志,由于readLine() 是一个阻塞方法,

flush(long lastFlushTime) 方法在处理时有一个判断条件if (logBuffer.size() >= Constants.DEFAULT_LOG_ROWS_NUM || now - lastFlushTime > Constants.DEFAULT_LOG_FLUSH_INTERVAL),只有当日志条数达到64条或者间隔1s时才会

flush。按理说,代码其实是要实现至少每隔1s会flash 一次日志,但是由于readLine() 是一个阻塞方法,所以并不会一直在执行,而是readLine()必须是读取到新数据后,才会执行flush方法。 那么在出现1s内产生的任务日志不满足64条,而任务又很久没有新日志出现时,就会触发这个bug。例如执行如下一个shell 脚本任务,由于每个执行步骤产生的日志少,而且每个步骤执行的时间又很久,时间间隔很大,就会出现很久都不会刷新上一次产生的日志。

  1. #!/bin/bash
  2. echo "hello world"
  3. exec 10m
  4. sleep 100000s
  5. echo "hello world2"
  6. exec 10m
  7. sleep 100000s
  8. echo "hello world3"
  9. exec 10m
  10. sleep 100000s 

之后我们对这段源码进行了重写,采用了两个线程进行处理,一个线程负责readline(),一个线程负责flush.做到在readline()方法的线程阻塞时,不影响flush线程的处理。

  1. public abstract class AbstractCommandExecutor {
  2. /**
  3. * rules for extracting application ID
  4. */
  5. protected static final Pattern APPLICATION_REGEX = Pattern.compile(Constants.APPLICATION_REGEX);
  6.  
  7. /**
  8. * process
  9. */
  10. private Process process;
  11.  
  12. /**
  13. * log handler
  14. */
  15. protected Consumer<List<String>> logHandler;
  16.  
  17. /**
  18. * logger
  19. */
  20. protected Logger logger;
  21.  
  22. /**
  23. * log list
  24. */
  25. protected final List<String> logBuffer;
  26.  
  27. protected boolean logOutputIsScuccess = false;
  28.  
  29. /**
  30. * taskExecutionContext
  31. */
  32. protected TaskExecutionContext taskExecutionContext;
  33.  
  34. /**
  35. * taskExecutionContextCacheManager
  36. */
  37. private TaskExecutionContextCacheManager taskExecutionContextCacheManager;
  38.  
  39. .........
  40. /**
  41. * get the standard output of the process
  42. *
  43. * @param process process
  44. */
  45. private void parseProcessOutput(Process process) {
  46. String threadLoggerInfoName = String.format(LoggerUtils.TASK_LOGGER_THREAD_NAME + "-%s", taskExecutionContext.getTaskAppId());
  47. ExecutorService getOutputLogService = ThreadUtils.newDaemonSingleThreadExecutor(threadLoggerInfoName + "-" + "getOutputLogService");
  48. getOutputLogService.submit(() -> {
  49. BufferedReader inReader = null;
  50.  
  51. try {
  52. inReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
  53. String line;while ((line = inReader.readLine()) != null) {
  54. logBuffer.add(line);
  55. }
  56. } catch (Exception e) {
  57. logger.error(e.getMessage(), e);
  58. } finally {
  59. logOutputIsScuccess = true;
  60. close(inReader);
  61. }
  62. });
  63. getOutputLogService.shutdown();
  64.  
  65. ExecutorService parseProcessOutputExecutorService = ThreadUtils.newDaemonSingleThreadExecutor(threadLoggerInfoName);
  66. parseProcessOutputExecutorService.submit(() -> {
  67. try {
  68. long lastFlushTime = System.currentTimeMillis();
  69. while (logBuffer.size() > 0 || !logOutputIsScuccess) {
  70. if (logBuffer.size() > 0) {
  71. lastFlushTime = flush(lastFlushTime);
  72. } else {
  73. Thread.sleep(Constants.DEFAULT_LOG_FLUSH_INTERVAL);
  74. }
  75. }
  76. } catch (Exception e) {
  77. logger.error(e.getMessage(), e);
  78. } finally {
  79. clear();
  80. }
  81. });
  82. parseProcessOutputExecutorService.shutdown();
  83. }
  84. .......
  85. /**
  86. * when log buffer siz or flush time reach condition , then flush
  87. *
  88. * @param lastFlushTime last flush time
  89. * @return last flush time
  90. */
  91. private long flush(long lastFlushTime) throws InterruptedException {
  92. long now = System.currentTimeMillis();
  93.  
  94. /**
  95. * when log buffer siz or flush time reach condition , then flush
  96. */
  97. if (logBuffer.size() >= Constants.DEFAULT_LOG_ROWS_NUM || now - lastFlushTime > Constants.DEFAULT_LOG_FLUSH_INTERVAL) {
  98. lastFlushTime = now;
  99. /** log handle */
  100. logHandler.accept(logBuffer);
  101.  
  102. logBuffer.clear();
  103. }
  104.  
  105. return lastFlushTime;
  106. }
  107. .......
  108. }

 4、将调度系统的监控接入到prometheus和grafana中

incubator-dolphinscheduler 只提供了一些如下的简单实时监控,尤其缺少对任务的监控。

联童在此基础上,引入了prometheus和grafana。

使用prometheus和grafana 不但可以监控到调度系统任务的总体运行,也可以监控到单个任务的运行耗时曲线等。

5、对incubator-dolphinscheduler 的性能优化

待稍后晚点补充

四、联童对于开源社区的拥抱和回馈

联童虽然是一家新兴起的母婴童公司,但是在成立的初始,就秉承着以技术来驱动母婴童产业的发展,公司拥有一个非常好的技术团队,也一直在拥抱开源社区,目前已经引入了incubator-dolphinscheduler、prometheus、grafana 、hadoop、spark、flink、hive、presto......等很多开源项目来支撑公司的技术驱动。在未来,联童也一定回不断的去回馈开源社区,去提供更多的Pull requests,贡献自己的一份力量。

联童科技基于incubator-dolphinscheduler从0到1构建大数据调度平台之路的更多相关文章

  1. 从 Airflow 到 Apache DolphinScheduler,有赞大数据开发平台的调度系统演进

    点击上方 蓝字关注我们 作者 | 宋哲琦 ✎ 编 者 按 在不久前的 Apache  DolphinScheduler Meetup 2021 上,有赞大数据开发平台负责人 宋哲琦 带来了平台调度系统 ...

  2. 基于MaxCompute的媒体大数据开放平台建设

    摘要:随着自媒体的发展,传统媒体面临着巨大的压力和挑战,新华智云运用大数据和人工智能技术,致力于为媒体行业赋能.通过媒体大数据开放平台,将媒体行业全网数据汇总起来,借助平台数据处理能力和算法能力,将有 ...

  3. 基于 HTML5 WebGL 与 GIS 的智慧机场大数据可视化分析

    前言:大数据,人工智能,工业物联网,5G 已经或者正在潜移默化地改变着我们的生活.在信息技术快速发展的时代,谁能抓住数据的核心,利用有效的方法对数据做数据挖掘和数据分析,从数据中发现趋势,谁就能做到精 ...

  4. 基于 HTML5 WebGL 与 GIS 的智慧机场大数据可视化分析【转载】

    前言:大数据,人工智能,工业物联网,5G 已经或者正在潜移默化地改变着我们的生活.在信息技术快速发展的时代,谁能抓住数据的核心,利用有效的方法对数据做数据挖掘和数据分析,从数据中发现趋势,谁就能做到精 ...

  5. 三:基于Storm的实时处理大数据的平台架构设计

    一:元数据管理器==>元数据管理器是系统平台的“大脑”,在任务调度中有着重要的作用[1]什么是元数据?--->中介数据,用于描述数据属性的数据.--->具体类型:描述数据结构,数据的 ...

  6. vue中,基于echarts 地图实现一个人才回流的大数据展示效果

    0.引入echarts组件,和中国地图js import eCharts from 'echarts' import 'echarts/map/js/china.js'// 引入中国地图 1. 设置地 ...

  7. 实践:由0到1-无线大数据UX团队的成长

    背景 大数据产品的在项目成立之初,采用的是模仿原有网优工具的方式做UI设计,由BA主导画草图.手绘线框图.excel制作,更有直接打开参考产品做原型的方式,没有统一的设计和规范可言.随着团队逐渐增多. ...

  8. 大数据平台迁移实践 | Apache DolphinScheduler 在当贝大数据环境中的应用

    大家下午好,我是来自当贝网络科技大数据平台的基础开发工程师 王昱翔,感谢社区的邀请来参与这次分享,关于 Apache DolphinScheduler 在当贝网络科技大数据环境中的应用. 本次演讲主要 ...

  9. 4 亿用户,7W+ 作业调度难题,Bigo 基于 Apache DolphinScheduler 巧化解

    点击上方 蓝字关注我们 ✎ 编 者 按 成立于 2014 年的 Bigo,成立以来就聚焦于在全球范围内提供音视频服务.面对 4 亿多用户,Bigo 大数据团队打造的计算平台基于 Apache Dolp ...

随机推荐

  1. day133:2RenMJ:TypeScript的变量&函数&类&接口

    目录 1.变量 2.函数 3.类 4.接口 1.变量 1.变量的声明 // 1.即指定数据类型 也指定值 var 变量名:类型 = 值; eg:var username:string = " ...

  2. vue3.0初尝试

  3. MarkDown学习笔记 Typora

    快捷方式篇 新建 ctrl + N 新建窗口 ctrl + shift + N 打开md文件 ctrl + O 快速打开 ctrl + P 保存 ctrl + S 另存为 ctrl + shift + ...

  4. Sublime text之中文乱码超简单解决方案

    很多玩程序的小伙伴,刚开始使用Sublime Text神器软件时,都会遇到打开一个程序文件,里面的中文编程乱码,不知道怎么办,网上也有很多不同解决方案,这里小编跟大家分享一个超简单的办法. 打开文档后 ...

  5. 用werkzeug实现一个简单的python web框架

    使用工具 Pycharm , Navicat , WebStorm等 使用库 Werkzeug用于实现框架的底层支撑,pymysql用于实现ORM,jinja2用于模板支持,json用于返回json数 ...

  6. Codeforces Round #652 (Div. 2) B. AccurateLee(字符串)

    题目链接:https://codeforces.com/contest/1369/problem/B 题意 给出一个长 $n$ 的 二进制串,每次可以选择字符串中的一个 $10$,然后删除其中的一个字 ...

  7. STL中去重函数unique

    一:unique(a.begin(),a.end());去重函数只是去掉连续的重复值,对于不连续的值没有影响,SO,在使用前一般需要进行排序处理: 二:  vector<int>::ite ...

  8. Codeforces Round #643 (Div. 2) 题解 (ABCDE)

    目录 A. Sequence with Digits B. Young Explorers C. Count Triangles D. Game With Array E. Restorer Dist ...

  9. 【noi 2.6_7113】Charm Bracelet(DP)

    题意:N个饰物,有重量和渴望程度.问在M的重量限制内能达到的最大的渴望度. 解法:经典的01问题,但有一个小技巧值得记住:用if比较大小比调用max函数快了不少,这题有100ms左右. 1 #incl ...

  10. Codeforces Round #648 (Div. 2) F. Swaps Again

    题目链接:F.Swaps Again 题意: 有两个长度为n的数组a和数组b,可以选择k(1<=k<=n/2)交换某一个数组的前缀k和后缀k,可以交换任意次数,看最后是否能使两个数组相等 ...