实战项目:设计实现一个流程编排框架(分析)

最近几篇文章,我会带大家一起设计一个流程编排框架,从项目的分析、设计、实现、重构、测试方面去了解整个编排框架,也会用到一些设计开发原则及设计模式,话不多说,我们先来看下编排框架的一个背景。

背景

对于交易这样复杂的业务场景,随着时间增加、功能逐渐增多,代码越来越多,所以系统就会考虑使用微服务框架;但是使用微服务框架之后,原有的业务并没有发生变化,与传统架构相比,微服务架构下会更依赖通过各微服务之间的协调来实现业务流程,这种协作就是流程编排。编排设计到方法节点、服务调用、条件选择、串行、并行、子流程等;当然在这个过程中也需要考虑通讯层、分布式事务;需要有一个完善的编排框架来支撑。

一般常见2种编排方式

第一种:面向可执行的流程,通过一个可执行的流程来协同内部及外部的服务交互,通过流程来控制总体的目标、涉及的操作、服务调用顺序。首先要有一个流程控制服务,该服务接收请求,依照业务逻辑规则,依次调用各个微服务,并最终完成处理逻辑。流程控制服务时时刻刻都知道每一笔业务究竟进行到了什么地步,监控业务成了相对简单的事情。

第二种:API网关,API网关可以看作一种简单的接口聚合/拆分的方式:每笔业务到来后先到达网关,网关调用各微服务,并最终聚合/拆分需反馈的结果。如果有开放接口文档还可以用编排定制产品,直接达到商用的效果。

项目背景

我们常规的项目是怎么做这种编排的呢?我们列举一个例子,A->B->C:这种场景是比较简单,对于开发人员来说就是新建一个类先执行A逻辑同步等待执行,在执行B逻辑同步等待结果在执行C;然后对所有结果进行处理返回;但是这个逻辑不可能是不变的,可能第二天又说在B->C加一个D逻辑,有需要在之前的业务代码里面做业务流程新增逻辑,而且需要负责的测试;当然后面可能还有更多的需求提供来,比如其它逻辑要应用这个编排,或者在执行B时需要加条件执行C和D,对于开发人员来说是迟早奔溃的事情。

在这个例子中我们可以看到几个问题:

  • 流程扩展性不强,需要对流程做变更的时候需要在原有逻辑修改代码,对于流程的风险也是比较大的;

  • 流程可读性不行,简单流程逻辑我们还可以理解,但是节点多了之后,就是产生各种逻辑、各种设计模式,不管对于老员工或者新员工来说,都是一笔不小的负担;

  • 代码冗余,在项目里面有过多的我是看到的只是一堆代码类似,但是功能实现不一样的逻辑;某一段逻辑出现问题,需要改涉及到的所有流程,还有可能改漏的风险性;

  • 缺少开发规范,一万个人就有一万个哈姆雷特,只要能实现功能,只要能完成任务就行,根本考虑不到谁来接手的问题;

  • 缺少监控手段,不知道这个流程定义了几个节点,每个节点耗时,是那个节点出现的问题,对于运维来说也是大问题;

针对上面的这些问题,我们有什么建议呢?我先来说说的我的;

我们可以把被调用的服务或者是简单封装成一个个的基础组件,规范输入输出,把调用条件封装成一个个要素;通过文件或者是外部接口做服务编排;比如我定义一个test.flow.yml文件对我的基础组件就行编排,完全不用再代码里面去做业务逻辑实现。

我们希望我们开发出来的东西有一定的影响能力,即使做不到在行业里面有影响力,起码也要做到在公司范围内有影响力。所以,从一开始,我们就不想把这个编排框架,做成只有我们项目可以,而是一个通用框架,可以集成到各个系统,甚至集成到微服务治理平台中。实际上,这也体现了业务开发中要具备的抽象意识、框架意识。我们要善于识别出通用的功能模块,将它抽象成通用的框架、组件、类库等。

需求分析

刚刚我们花了一些篇幅项目背景和背景需求,接下来,我们再对需求进行更加详细的分析和整理。

功能性需求

首先我们需要定义编排规则,但是没有可视化操作的界面的前提下,我们会把编排定义在yml、properties、xml中,我们先看一个demo示例:

name: openAccount
id: test
desc: 条件执行
input: com.service.flow.sample.common.model.TestInput
output: com.service.flow.sample.common.model.TestOutput
temp: com.service.flow.sample.common.model.TestTemp
startNode: node1
nodes:
  - node:
      id: node1
      name: methodInvoke
      component: com.service.flow.sample.common.component.TestComponent:test1
      desc: 单节点执行
      input: com.service.flow.sample.common.model.TestInput
      type: method
      next: node2
  - node:
      id: node2
      name: beanInvoke
      component: testComponent:test2
      desc: 单节点执行
      input: com.service.flow.sample.common.model.TestInput
      type: bean
      next: node3
  - node:
      id: node3
      name: conditionByAge
      component: temp.getAge>20:node4,temp.getAge<20:node5
      desc: 单节点执行
      input: com.service.flow.sample.common.model.TestInput
      type: condition
  - node:
      id: node4
      name: beanInvoke
      component: testComponent:test2
      desc: 单节点执行
      input: com.service.flow.sample.common.model.TestInput
      type: bean
  - node:
      id: node5
      name: beanInvoke
      component: testComponent:test2
      desc: 单节点执行
      input: com.service.flow.sample.common.model.TestInput
      type: bean
  • 我们需要定义一些元素请求参数、输出参数、执行ID、流程描述、临时变量、节点类型,执行条件;

  • 我们需要提供多种编排注册方式yml、json、xml、properties、text、接口;

  • 我们采用链表的形式执行所有的节点,减少内存开销;

  • 需要支持多种类型节点,方法节点、服务节点、条件节点、Bean节点、子流程节点等;

  • 我们需要对流程上下文做扩展;

  • 我们框架需要基于Spring做基层框架,并提供对应的starter,做可集成框架;

非功能性需求

易用性方面,我们希望提供流程提示,有能力的情况下提供可视化编排,我们将以Spring框架作为基础框架,我们还希望编排框架能否非常方便地集成到使用 Spring 框架的项目中。

扩展性、灵活性方面,我们希望能够自己定义各种组件和元素模型,我们希望编排初始化的时候提供各种扩展接口,将不同类型的文本,转换成编排模型;

性能方面:我们希望模型在项目启动的时候就初始化好,只初始化一次;节点执行的时候采用递归链表的形式,减少不必要的代码循环;

容错性方面,不能因为编排框架而影响的本身的业务逻辑,即使不用这个框架也不至于框架大乱。

功能需求其实没有多少,但将非功能性需求考虑进去之后,明显就复杂了很多。还是那句老话,写出能用的代码很简单,写出好用的代码很难。对于编排框架来说,非功能性需求是设计与实现的难点。怎么做到易用、灵活、可扩展、低延迟、高容错,才是开发的重点。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章