关于工作流:技术实践干货-从工作流到工作流

47次阅读

共计 9431 个字符,预计需要花费 24 分钟才能阅读完成。

本文作者: 葱油饼,观远前端工程师,落地团队开发标准,开发品质与速度并存,致力于打造更易用的 ABI 产品。

背景

先举个简略的例子,因为工作须要,你可能每天要从数据库抽取数据,而后做成报表,最初以邮件的模式发送给相干的领导。然而每个领导可能须要看的货色不一样,你须要在做成报表前对数据做下筛选和解决,那么每天这个反复的流程,是不是能够形象成为一个具体的工作流程,把每个步骤具象成一个性能结点,而后以工作的模式串联起来,通过 DAG 的可视化模式展示进去,每天定时跑一下就能够了呢?为此,咱们会须要一个工作流来标准化和自动化这个流程。

那工作流是什么?DAG 又是什么?上面让咱们进入明天的内容。

前言

这篇文章会解说咱们 Universe(观远三大产品线之一,即观远数据的智能数据开发平台)里的工作流和 DAG 这两个概念,而后开展介绍一些其余内容。整体分为四个局部:

  1. 开发平台里的工作流;
  2. 如何形象实现 DAG;
  3. 其余工作流介绍;
  4. 基于工作流和 DAG 的总结与思考。
    接下来让咱们开始吧~

一、工作流

首先简略介绍下 Universe 里的工作流:

实现各类工作的依赖关系、调度程序设计,对流程进行可视化、低代码的设计及治理,对工作节点进行疾速且高可用的配置,来解决一系列的数据工作;并且能够在约定工夫 / 满足事件依赖后运行,有序调起各个工作节点,主动实现数据处理过程,具备简略易用、高可靠性及高扩展性等劣势。

依据这段形容,咱们能够简略总结出工作流的两个外围能力:

  1. 调度;
  2. 配置化(节点)。

上面具体介绍下这两个外围能力。

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,体验观远产品

正文完
 0