共计 9431 个字符,预计需要花费 24 分钟才能阅读完成。
本文作者: 葱油饼,观远前端工程师,落地团队开发标准,开发品质与速度并存,致力于打造更易用的 ABI 产品。
背景
先举个简略的例子,因为工作须要,你可能每天要从数据库抽取数据,而后做成报表,最初以邮件的模式发送给相干的领导。然而每个领导可能须要看的货色不一样,你须要在做成报表前对数据做下筛选和解决,那么每天这个反复的流程,是不是能够形象成为一个具体的工作流程,把每个步骤具象成一个性能结点,而后以工作的模式串联起来,通过 DAG 的可视化模式展示进去,每天定时跑一下就能够了呢?为此,咱们会须要一个工作流来标准化和自动化这个流程。
那工作流是什么?DAG 又是什么?上面让咱们进入明天的内容。
前言
这篇文章会解说咱们 Universe(观远三大产品线之一,即观远数据的智能数据开发平台)里的工作流和 DAG 这两个概念,而后开展介绍一些其余内容。整体分为四个局部:
- 开发平台里的工作流;
- 如何形象实现 DAG;
- 其余工作流介绍;
- 基于工作流和 DAG 的总结与思考。
接下来让咱们开始吧~
一、工作流
首先简略介绍下 Universe 里的工作流:
实现各类工作的依赖关系、调度程序设计,对流程进行可视化、低代码的设计及治理,对工作节点进行疾速且高可用的配置,来解决一系列的数据工作;并且能够在约定工夫 / 满足事件依赖后运行,有序调起各个工作节点,主动实现数据处理过程,具备简略易用、高可靠性及高扩展性等劣势。
依据这段形容,咱们能够简略总结出工作流的两个外围能力:
- 调度;
- 配置化(节点)。
上面具体介绍下这两个外围能力。
1.1 调度
开发平台反对基于 Cron 表达式的定时调度和基于输出源数据依赖的事件调度,其中定时调度采纳 quartz 散布式调度器。
具备以下几点个性:
- 高可用性
-
- 通过 DAG 实现工作节点的可视化编排,毋庸简单的平台语言学习老本,任务调度开箱即用;
- 反对配置程序调度、胜利调度、失败调度等多种调度关系,灵便调整调度策略;
- 反对按时、按天、按周、按月等定时运行工作流,运行后果可疾速推送至钉钉、企业微信等平台,一次配置,继续可用。
- 高可靠性:去中心化的多 Master 和多 Worker 分布式架构, 防止单点故障,加强系统可靠性。
- 高扩展性:能够基于 SDK 开发自定义工作类型和流程无缝连接。
1.1.1 定时调度
反对以每天 / 每周 / 每月 / 每年并准确到分钟的模式和距离时长(时 / 分)的模式去设置定时。
举个例子:我冀望工作流每天早上 7 时和早晨 21 时去运行,那我就能够抉择 每天 – 7 时 /21 时 – 00 分的模式,也能够设置分钟 / 时的间隔时间去运行。
1.1.2 事件调度
个别工作流都会有数据源依赖,比方数据集 / 数据库,当开启依赖的数据源全副更新的时候,工作流能够主动去运行一次。
1.2 配置化
基于一个约定式的配置形容,产出一个可交互的 UI,用于构建指标对象。
调度的目标是运行工作流,工作流的运行依赖于不同工作节点的配置,不同的配置必然会存在不一样的 UI 组件,那如何能用已知的数据结构去组装一个可视化的 UI 呢?答案就是配置化。
咱们基于一个配置形容(对象)去进行读取,而后依据配置渲染对应的组件,同时把组件的值集中设置到一个总的配置对象里,从而实现了从形容到 UI 再到指标对象构建的一个过程。上面我会简略的举三个例子来阐明配置化的弱小与魅力。
1.2.1 根底能力
如果咱们须要构建如下的一个指标对象:
{
name: '',
description: '',
}
而后咱们就会有以下一段配置形容:
[
{
fieldName: 'name',
label: '名称',
type: 'STRING',
defaultValue: '',
},
{
fieldName: 'description',
label: '形容',
type: 'TEXT',
defaultValue: '',
},
]
生成的 UI 如下:
1.2.2 动静能力
很多时候咱们会须要动静实现一个指标对象,什么意思呢?就是抉择一个属性的不同值,动静应用一个属性组合成一个新的指标对象,那对应到 UI 上就是抉择不同属性值对应展现不同的组件,那光靠咱们的根底能力去实现,显然无奈做到。
比方我想计算一个图形的面积,如正方形须要的是边长属性,而圆须要的是半径属性,那指标对象和 UI 就会变成:
- 抉择正方形时
{
shape: 'square',
side: 8,
}
- 抉择圆形时
{
shape: 'circle',
radius: 4,
}
能够看到 side
和 radius
是随着 shape
而动静呈现的,那咱们能够简略革新下配置形容:
{
fieldName: 'shape',
label: '图形',
type: 'MODEL',
model: {
modelType: 'SELECT',
labels: ['圆', '正方形'],
values: ['circle', 'square'],
},
},{
fieldName: 'radius',
label: '半径',
type: 'NUMBER',
dependsOnMap: {shape: [ 'circle'],
},
defaultValue: 4,
},
{
fieldName: 'side',
label: '边长',
type: 'NUMBER',
dependsOnMap: {shape: [ 'square'],
},
defaultValue: 8,
},
能够看到,咱们仅仅增加了 dependsOnMap 属性,而后外部渲染和构建对象的时候略微适配下,就能够实现抉择不同属性展现不同组件的需要了。
这里简略阐明下 dependsOnMap 属性,它的 key 值应该是某一个 fieldName,value 是一个数组,不便扩大容许多个值的状况,这样就能够依据 fieldName 去获取 value,与配置里的值去比拟,如果一样那就展现该组件,外围逻辑如下:
function isDependsOnMap (dependsOnMap, config) {const fieldNames = Object.keys(dependsOnMap || {})
if (fieldNames.length === 0) return true
return fieldNames.every(fieldName => {const values = dependsOnMap[fieldName] || []
return values.indexOf(_get(config, fieldName)) > -1
})
}
1.2.3 简单能力
咱们在日常编写中可能还会存在组件之间的数据传递。因为因为配置形容的对象束缚,咱们在渲染每个组件的时候其实都是独立的,组件之间并不存在分割,为此咱们只须要在最上层实现一个数据共享层即可,组件 3 把须要传递的数据放在数据共享层,须要该数据的组件 1 间接去获取即可。
配置如下:
{
fieldName: 'fieldName1',
label: '组件 1',
type: 'MODEL',
model: {
modelType: 'SELECT',
labels: ['圆', '正方形'],
values: ['circle', 'square'],
from: {fieldName: 'disabledFieldName'}, // 依赖于组件 3 里的设置,判读以后组件是否须要 disabled
},
},
{
fieldName: 'fieldName2',
label: '组件 2',
type: 'NUMBER',
},
{
fieldName: 'fieldName3',
label: '组件 3',
type: 'MODEL',
model: {
modelType: 'BOOLEAN',
targetSharedFieldName: 'disabledFieldName', // 往数据共享层设置数据的字段
},
},
要害的配置属性就是组件 3 里的 model.targetSharedFieldName
和组件 1 里的 model.from
,两者互相对应即可,大体实现如下:
const SharedContext = React.createContext({updateFieldValue: () => {}, // 更新字段 value
getFieldValue: () => {}, // 获取字段 value
})
function Comp1 ({definition}) {const { targetSharedFieldName} = definition.model
const {updateFieldValue} = useContext(SharedContext)
useEffect(() => {updateFieldValue(targetSharedFieldName, value)
}, [deps])
}
function Comp2 ({definition}) {const { from} = definition.model
const {getFieldValue} = useContext(SharedContext)
const value = getFieldValue(from)
}
最初简略上个开发平台中一个简单的配置化 UI 动图,感触下配置化的弱小和魅力:
1.2.4 服务能力
当咱们须要构建一些数组类指标对象时,第一工夫想到的必定是以列表的模式去展现 UI,因而咱们设计了一些服务类型的组件,只负责对列表的渲染,然而每个列表的组件依据数组元素的类型去决定。
比方咱们须要这样一个数组类指标对象:
{
list: [{ name: 'a', age: 12},
{name: 'b', age: 18},
],
}
那对应的配置形容能够写成这样:
[
{
fieldName: 'list',
label: '列表',
type: 'MODEL',
model: {
modelType: 'LIST',
definitions: [
{
fieldName: 'name',
label: '名称',
type: 'STRING',
},
{
fieldName: 'age',
label: '年龄',
type: 'NUMBER',
},
],
},
},
]
而对应的 UI 如下:
这个 LIST
组件就是一个服务类型的组件,把数组对象通过列表模式展示进去。
1.2.5 注册能力
内置组件可能并不能齐全满足配置化的需要,因为配置化只是一种约定,然而通过构建对象绘制 UI 属于自由化,展示模式千差万别,为此咱们提供了注册机制。用户能够自定义注册组件类型,去绘制对应的指标对象。
1.3 总结
基于这么优良的配置化能力应该被形象进去,所以也被使用在了 BI 的自定义图表上。基于此,咱们写了一个库叫 Lego,正如名字的含意,咱们冀望在搭建一些专门用于配置的 UI 时如同搭积木一样简略,约定好形容(接口),你去拼拼凑凑就能够了。
介绍完工作流,咱们还须要一个可视化的界面来形容这个流程,那么 DAG 无疑是一个很好的展现模式了。
二、DAG
DAG 全称 Directed Acyclic Graph,中文为 有向无环图。它由无限个顶点和“有向边”组成,从任意顶点登程,通过若干条有向边,都无奈回到该顶点。举例如下图:
简略了解了 DAG 的概念,如何来针对开发平台的工作流场景来形象出一个简略好用的 DAG 呢?首先整顿下绘制一个 DAG 须要哪些信息及状态:
- 节点信息(nodes)
- 节点地位(location)
- 连线信息(edges)
- 编辑和只读状态 \
前三点很好了解,应该是绘制 DAG 必不可少的三要素,对于第四点解释下,因为开发平台的工作流有高低线的概念,开发实现后上线运行,不容许批改,来作为数仓开发中的一个标准,那么咱们的工作流就存在了下线可编辑,上线只读的辨别。
首先从编辑和只读下手,咱们能够把 DAG 分为 Playground 和 Renderer 两局部,并且可独立应用。Playground 对应编辑态,Renderer 对应只读态。Playground 应该去实时生成编辑状态中的绘制信息,而 Renderer 则负责依据绘制信息去实时渲染。而后咱们来梳理下编辑和只读状态下应该具备什么能力:
-
Playground
- 节点拖动
- 连线增删
- 新增 / 复制节点
- 框选节点进行批量复制 / 删除
- 主动布局 / 撤销操作
-
Renderer
- 放大放大
- 画布拖动
- 节点点击
那再往上考虑一下,咱们的 DAG 还应该具备什么能力?这里我联合开发平台的应用简略的列了以下几点:
- 提供款式配置(如节点大小 / 连线宽度等)
- 反对宽高自适应
- 自定义绘制节点和连线
- 其余绘制能力加强(如正文性能,自身并不属于 DAG 的性能,而是思考成扩大性能实现)
至此,咱们的 DAG 大略有了一个残缺的构造和实现方向:
|- ConfigContext --- 配置层
|- Playground --- 编辑层
|- ResponsiveProvider --- 自适应宽高层(可选)|- Renderer --- 只读层,只做展现
|- Nodes --- 节点
|- Edges --- 连线
应用上大略是这样的:
2.1 只读应用
<ConfigContext.Provider value={{node: { width: 56, height: 56} }}>
<ResponsiveProvider>
<Renderer nodes={nodes} location={location} edges={edges} />
</ResponsiveProvider>
</ConfigContext.Provider>
2.2 编辑应用
<ConfigContext.Provider value={{node: { width: 60, height: 60} }}>
<Playground nodes={nodes} location={location} edges={edges} />
</ConfigContext.Provider>
2.3 自定义节点和连线应用
<ConfigContext.Provider value={{node: { width: 56, height: 56} }}>
<Renderer nodes={nodes} location={location} edges={edges}>
<Nodes>
{(props) => <CustomNode />}
</Nodes>
<Edges>
{(props) => <CustomEdge />}
</Edges>
</Renderer>
</ConfigContext.Provider>
2.4 底层绘制
这里咱们抉择了 svg,是因为 svg 在绘制上足够弱小,反对 css 去自定义款式,同时也不便事件的绑定。有了这个方向,咱们能够确定下元素顺次对应哪些标签:
- 画布:svg
- 节点:foreignObject
- 连线:path
画进去大抵是上面这样的构造:
其中画布的放大放大及挪动是通过 viewBox 属性设置
依据 html 构造,连线是咱们须要关怀如何生成的,这里次要是通过两个节点的地位来计算一条二次贝塞尔曲线(Quadratic Curves)来失去一条反向对称的完满曲线,如下:
这里说下二次贝塞尔曲线在 path 标签中如何实现。首先绘制须要三个点的信息,如下动图:
其次因为咱们的曲线是反向对称的,那么其实只须要绘制一半就行,这一半就是一条二次贝塞尔曲线,那么三个点的地位就很好确认了,如下:
其中 P0 为终点,P4 为起点,为不便计算,P1 对应 1/4 程度间距,高度同终点,P2 则是 1/2 的程度间距和垂直间距,而后通过计算 path 门路的 d 属性,别离把 3 个点代入即可:d = M P0x P0y Q P1x P1y P2x P2y T P4x P4y
这样咱们就失去了一条残缺的曲线,由两条二次贝塞尔曲线形成。
2.5 布局
有了节点和连线,布局方面也是很重要的一环,人工拖拽显然有时候会显得不够参差,如果有一个主动布局的算法,那么就会轻松许多,这里咱们抉择了 dagre 来作为主动布局的计算工具。次要有以下三种算法:
function rank(g) {switch(g.graph().ranker) {case "network-simplex": networkSimplexRanker(g); break;
case "tight-tree": tightTreeRanker(g); break;
case "longest-path": longestPathRanker(g); break;
default: networkSimplexRanker(g);
}
}
network-simplex
和 tight-tree
在布局上相似,都是以紧凑的形式去实现布局,longest-path
的区别在于如果有多个末端结点,则保障这些结点从上而下对齐,而不是就近去布局,如下图:
network-simplex
和tight-tree
longest-path
三、其余工作流
这里不会具体介绍这些工作流如何应用,只是会借鉴它们在对工作流绘制及利用上的一些想法。
3.1 **n8n
The workflow automation platform that doesn’t box you in, that you never outgrow.
n8n 反对以事件驱动(个别通过第三方利用的 hooks/ 本地文件批改监听等)和 cron 表达式的定时调度工作流,同时以数据传递的程序确定结点之间的依赖关系。和咱们的工作流很像,只是咱们的工作流是结点任务调度上的依赖,而不是数据上的依赖。
3.1.1 利用
那它适宜来干什么呢?如下图:
如果你是一个开源爱好者,心愿晓得本人的 Github Repo 被 star 或者移除 star 的时候第一工夫晓得音讯,那么就能够应用 github 凋谢的 star hook,而后通过 slack 给本人发送音讯。通过对第三方平台的集成,能很好把各种没有关系的利用串联起来,开发出便捷的工作流。\
3.1.2 总结
n8n 目前曾经集成了 200+ 的利用,笼罩了大部分支流的利用。然而国内的一些利用还是缺失的,比方钉钉 / 企业微信等,所以它也就顺利成章的反对了自定义开发结点,有趣味的能够点击 这里。整体来说,n8n 更像是一个集成利用的工作流,当然也反对一部分本地性能,如读写文件 / 应用 git 操作等。它能够把咱们日常工作或者开发中须要点点写写的一些罕用操作,整合成一个工作流,便捷日常生活。
3.1.3 借鉴
从它的工作流设计上,或者有些点能够借鉴过去:
- 结点配置时能够看到上一个结点的输入数据是什么,不便以后步骤进行配置
- 结点配置完能够立刻执行,看到对应的输入数据
- 连线上有一些数据可视化加强,如输入数据有多少行
- 结点能够间接点击增加抉择后置结点,省去一部分连线操作
3.1.4 其余
前面我试了下结点是否成环,后果是能够,陷入有限循环的运行中,利用卡死了,如下:
数据有限增长,运行有限循环。
3.2 Orange
Open source machine learning and data visualization. Build data analysis workflows visually, with a large, diverse toolbox.
3.2.1 利用
Orange 比拟适宜做 ML 相干的工作,有点像咱们的 AI Flow,然而又把数据流 / 数据探查 / 图表剖析等性能集成在外面,不必去其余页面独自配置解决查看,以工作流的模式对数据进行查看、解决和剖析。简略上个图:
有个很有意思的小点,它的连线反对全量数据或者选中数据进行传递,如下图:
而后会把数据传递的形式体现在连线上。在连贯上,它是以圆弧的模式来展示端点(我猜用圆弧是为了减少结点的连贯面积,同时也适配圆形结点),有连贯则为实线,无连贯则为虚线,对于状态的展现上很敌对。
3.2.2 总结
Orange 性能集成非常弱小,除了根本的数据转换,还有图表 / 模型 / 评估等性能,很适宜做 AI 方向的数据分析工作。
四、总结与思考
工作流这个概念曾经提出很久了,它是对流程及其各操作步骤之间业务规定的形象、概括和形容。工作流的呈现让咱们的流程失去标准,步骤变得清晰。而数据开发下面的工作流更是防止了一系列的反复操作,同时以 DAG 的模式去展示,让流程变得更为直观。当然 DAG 也不肯定用在调度这类有着先后顺序限度的零碎中,也能够用在其余模式中,比方数据血统这类有着因果关系的展现上,也能够用于家族图谱的展现上,再晋升一层,甚至能够用在数据处理网络中,数据从一个点流到另一个点,而并不一定须要以可视化的模式展示进去,仅仅须要这个概念就能够了。
4.1 可能性
其实咱们的工作流的调度能力和配置化性能很弱小,然而受限于无限的性能结点,如果咱们能够反对自定义配置结点,能让用户在数据开发层面有更大的设想空间,而不是只受限于这些已有的结点去做工作流的开发。
参考资料
[1] https://dolphinscheduler.apache.org/zh-cn/docs/latest/user_doc/about/introduction.html
[2] https://en.wikipedia.org/wiki/Workflow
[3] https://en.wikipedia.org/wiki/Directed_acyclic_graph
[4] https://github.com/biolab/orange3
[5] https://github.com/n8n-io/n8n
扫码或点击应用 demo,体验观远产品