Kstry 流程编排框架
🟢 项目主页
🟢 功能测试
Kstry 是什么?
Kstry 可以将原本存在于代码中错综复杂的方法调用关系以可视化流程图的形式更直观的展示出来,并提供了将所见的方法节点加以控制的配置手段。框架不能脱离程序执行之外存在,只能在方法与方法的调用中生效和使用,比如某个接口的一次调用。不会像 Activiti、Camunda 等任务流框架一样,脱离程序执行之外将任务实例存储和管理。
不同使用场景中,因其发挥作用的不同,可以理解成不同的框架,Kstry 是:
🟢 【流程编排框架】提供所见( 流程图示 )即所得( 代码执行 )的可视化能力,可自定义流程协议,支持流程配置的热部署
🟢 【并发框架】可通过极简操作将流程从串行升级到并行,支持任务拆分、任务重试、任务降级、子任务遍历、指定流程或任务的超时时间
🟢 【微服务业务整合框架】支持自定义指令和任务脚本,可负责各种基础能力组件的拼装
🟢 【轻量的TMF2.0框架】可以通过以下三个步骤来满足同一接口下各类业务对实现功能的不同诉求:抽象能力资源、定义并将抽象出的能力资源授权给业务角色、同一流程的不同场景可分别匹配不同角色再将其下的能力资源任务加以执行
Kstry 有哪些特点?
🟢【业务可视】编排好的图示模型即为代码真实的执行链路,通过所见( 图示模型 )即所得( 代码执行 )的方式在技术和业务之间架起一道通用语言的桥梁,使彼此之间沟通更加顺畅
🟢【配置灵活】提供开始事件、结束事件、服务节点、脚本节点、排他网关、包含网关、并行网关、条件表达式、自定义指令、子流程、拦截器等配置组件,可以支持变态复杂的业务流程
🟢【动态配置】主流程、子流程、角色、变量等组件支持动态化配置,不启动应用的前提下可以动态变更,动态化配置支持包括开源和公司自研在内的全部存储介质
🟢【适配度高】包含 BPMN 可视化配置文件和代码两套流程定义 API,在保证可视化配置的前提下,又支持通过代码方式解析任意格式的流程配置文件,从而结合合适的前端产品搭建个性化的流程配置平台
🟢【轻松运维】服务节点支持定义超时时间、重试次数、失败降级、严格模式、资源迭代等,可满足生产环境下对应用稳定性的严苛要求
🟢【性能优异】最底层采用 Spring 工具集进行服务节点调用,任务执行消耗与 Spring 切面相当
🟢【秒变异步】无缝衔接 SpringFlux。无需改动代码,仅仅在并行网关或包含网关上配置 open-async=true
,即可在保证线程安全的前提下将其后的子链路全部并行化
🟢【交互顺畅】引入 StoryBus 和其中四个数据域的概念。节点之间数据存取交互可以做到安全、灵活、方便
🟢【业务抽象】引入资源、权限、角色等概念,构建定制化业务身份,为抽象化业务能力提供技术支持和解决方案
🟢【流程回溯】可以零成本记录节点执行顺序、节点耗时、入参、出参、异常信息等重要数据,并支持自定义执行监控日志
Kstry 可以解决哪些问题?
🟢 【业务模糊】代码复杂、模型文档更新不及时,致使新同学和非技术同学不能短时间内了解业务现状。技术和非技术间对同一业务理解存在分歧而不自知。甚至业务 Owner 也不能很流畅的描述出自己所负责的业务
🟢 【代码杂乱】项目中涉及到许多领域对象,对象间不仅存在复杂的前后依赖关系还相互掺杂没有明显边界,代码多次迭代后更是混乱不堪难以维护
🟢 【性能低下】某业务链路由一系列子任务组成,其中需要并行处理一些耗时长且数据间没有依赖的子任务,但苦于没有精简且无代码侵入的并发框架
🟢 【平台之殇】维护平台型产品,为众多上游业务线提供着基础服务,但在短时间内应对各个业务方的定制化需求捉襟见肘,更不知如何做好平台与业务、业务与业务之间的隔离
🟢 【回溯困难】业务流转数据状态追踪困难,只存在于线上环境的偶现问题更是难以排查。需要一种可以通过简单操作就能将重要节点数据都保存下来的能力,此能力堪比对链路精细化梳理后的系统性日志打印
🟢 【测试复杂】业务场景多样,不乏一些复杂的链路难以被测试覆盖。或者三方数据 Mock 困难,测试成本居高不下
Kstry 组件概念介绍
1、配置域与运行域
简单来讲配置域就是通过可视化、代码亦或者脚本的方式配置流程,执行域就是根据配置好的规则来执行。
概念上理解,通常情况下的程序执行是方法与方法之间的调用,在 Kstry 中却是任务节点之间的相互关系(之所以是概念上来看,是因为无论是否用 Kstry,程序实际执行载体都是类中的方法,而且任务节点与方法也是一一对应的)。任务执行前的流程配置决定了节点之间在实际执行时会产生何种关系。程序未执行前的流程配置就是配置域,框架解析流程配置并在程序执行时指导任务节点先后调用就是执行域。
2、流程定义
流程定义是程序实际执行前,在配置域需要完成的工作,由开始事件、结束事件、服务节点、网关、及这些组件相连接的有向线段共同组成。编排好的图示模型即为代码真实的执行链路,通过所见( 图示模型 )即所得( 代码执行 )的方式在技术和业务之间架起一道通用语言的桥梁,使彼此之间沟通更加顺畅。
只有bpmnPath
一种流程配置途径时,下面这种可视化流程图就是被定义出来的流程,也是凭此来指导程序实际线路的执行。ProcessLink
出现之后,框架提供了自定义流程的能力给使用者,在 1.x 之后的版本中,BPMN 可视化流程图也是被解析成ProcessLink
对象来生效的,此时的 BPMN 可视化流程就作为了流程定义的一个子集而存在。更多情况下使用者可以通过编码、解析各类配置文件等方式来创建ProcessLink
实例,最终交付给框架的ProcessLink
实例将会作为流程定义来指导程序的执行。
Kstry 提供了三种方式来定义流程配置,分别是:
🟢 【基础流程配置】指定@EnableKstry
注解的bpmnPath
属性来扫描指定目录下的 bpmn 配置文件
🟢 【基础流程配置】创建ProcessLink
实例放入 Spring 容器中
🟢 【动态流程配置】实现DynamicProcess
接口的getProcessLink
方法
前面两种方式定义的流程是应用启动前就必须要定义出来的属于基础流程配置,基础流程除非应用重启否则是不允许修改的。根据startId
在基础流程配置库里获取不到流程配置时最后一种方式才会生效,其获取流程配置的方式是动态的,传入startId
返回ProcessLink
实例,其中所需的流程配置信息可以通过 http 或 rpc 调用、关系或者非关系型数据库查询、订阅消息队列、订阅注册中心、公司自定义存储介质查询等任意方式获取。
之所以没有使用动态改变基础流程配置库的方式来实现流程的动态配置能力,有以下原因:
🟢 【安全性】流程配置可能会分重要级别,核心的流程配置是被严格管控不允许随意变更的,所以这类配置应该放在代码中不允许随意变更。如果在流程配置基础库中找到与startId
相匹配的流程,动态获取部分将失效,以此来保障流程的绝对安全。即便核心流程中有部分需要动态获取的扩展逻辑,也可以用动态子流程的形式来实现。
🟢 【兼容性】动态化配置可能比较适合使用注册中心来存储维护,这样当发生变更时能快速及时通知相关应用做流程变更。但并没有一个注册中心是所有公司都使用的,除了开源产品还有公司自研,如果想要一一支持是不现实的。选用其他中间件产品也是同样的道理。所以解决这个问题的方式就是提供一个动态获取流程配置的入口,至于流程配置在什么地方获取将是因人而异的。
3、子流程定义
顾名思义,子流程概念很好理解。在流程中出现可复用或者复杂的链路片段时,就可以将这些片段抽离出来定义子流程。父流程可以通过子流程 Id 来引用子流程。同样也有三种方式来定义子流程,分别是:
🟢 【基础子流程配置】指定@EnableKstry
注解的bpmnPath
属性来扫描指定目录下的 bpmn 配置文件
🟢 【基础子流程配置】创建SubProcessLink
实例放入 Spring 容器中
🟢 【动态子流程配置】实现DynamicSubProcess
接口的getSubProcessLinks
方法
主流程有基础和动态之分,相对应也有基础和动态子流程的存在。下图为两者结合起来时,相互之间是否允许被引用的说明:
🟢 基础流程仅能引用基础子流程配置,动态流程可以引用基础子流程和动态子流程配置
🟢 两种子流程在各自的区域内可以相互引用,动态子流程中可以引用基础子流程配置,反之则不被允许
注意:子流程相互间引用时,一定不能出现环状依赖,否则会出现异常
4、服务节点
框架通过定义服务节点来划分领域边界并实现业务功能。服务节点是流程编排和任务执行的最小单元。配置域内一个个被有向线段连接起来的服务节点组成了主流程或者子流程。运行域中节点对应类组件中的某个方法,流程执行实际上是与节点相对应的方法被一一调用的过程。
框架中如果要定义服务节点,首先需要定义服务节点(也就是方法)所在的组件类。组件类都是托管在 Spring 容器中的,有两种定义方式:
🟢 类放在 Spring 容器可以扫描到的位置,并添加@TaskComponent
注解
🟢 以 Spring 组件的方式被加载,并实现TaskComponentRegister
接口
组件类被定义之后,其内的公有方法加上@TaskService
注解就是服务节点。@TaskService
注解中的@Invoke
属性可以指定服务节点重试次数、超时时间、降级方法、异常后 Story 是否被允许继续执行等
注意:@TaskService 修饰的方法必须是public
的,否则无法识别
正如前面所说的,Kstry 即是流程编排框架,也是轻量的 TMF2.0 框架。服务节点的理解方式相关联的也有两种思路:
🟢 仅仅只是流程编排的话,可以将整个流程看作一个完整的任务,服务节点就是其中的子任务
🟢 另一种思路更适用于平台型产品。可以将一个完整的流程定义看作是平台针对某个业务活动提供的解决方案。服务节点就是解决方案中的服务(下图中,加载商品信息、店铺信息、营销信息、计算价格都是服务),服务下可以定义多个能力点,在代码维度来看就是标注@TaskService
注解的方法中,name
属性相同的服务节点为同一个服务,不同的ability
属性区分出了不同的能力点。针对不同业务可以匹配各服务下的不同能力点来完成个性化业务诉求。其中涉及到的更具体概念可以查看下面能力 &角色的介绍
无论哪种思路,服务节点的划分原则都是尽可能的做到业务隔离。一次业务请求经过各种业务活动最终拿到结果的过程是动态的,使用边界将动态的业务流程进行合理切分,其产物就是服务节点也是领域驱动中的限界上下文。理解以下限界上下文的特点,可以指导如何更好的定义服务节点:
🟢 最小完备:具备完成自身使命目标的最小依赖条件,无需求助其他的服务节点,避免不必要的依赖
🟢 自我履行:根据服务节点自己现掌握的资源信息,判断预期目标与自身的使命目标是否相符,决策是否应该履行该职责
🟢 稳定空间:减少外界变化对服务节点内部的影响
🟢 独立进化:减少服务节点的变化对外界的影响
5、网关
网关有三种:排他网关、包含网关、并行网关
网关有控制流程执行的作用,不同网关有着不同的特点,包含网关和并行网关还可以配置其后的流程并发执行。各类网关与服务节点结合起来使用可以实现非常复杂的业务场景
5.1 排他网关
🟢 排他网关只能接收一个入度。也就是说有且只能有一个箭头可以指向排他网关。这点上服务节点也是如此
🟢 排他网关入度只能有一个,出度可以多个。出度上面的条件表达式会被解析执行,如果没有条件表达式会默认是 true
🟢 排他网关有多个出度相关的表达式被解析成 true 时,会选择第一个为 true 的分支继续向下执行,其他的将会被忽略不再执行。出度的前后并不代表程序解析时出度的先后顺序,所以排他网关后面如果多个出度都为 true 时运行结果是不确定的,应尽量避免这种事情发生
🟢 当全部出度上的表达式都解析为 false 时会抛出异常并结束流程
🟢 由于排他网关后面最终执行的只有一条链路,所以排他网关是不支持开启异步的,因为没啥意义
5.2 并行网关
🟢 并行网关可以接收归并多个入度
🟢 并行网关要求所有入度全部执行完才能继续,否则将一直等待。所以使用前需要确认网关的全部入度一定都是可达的
🟢 使用并行网关时,一般前后两个并行网关会一起出现。前面将一个分支拆解成多个,后面将多个分支进行聚合
🟢 并行网关支持开启异步流程。未开启异步时,并行网关拆分出的多个分支还是被当前线程执行,开启异步流程后,网关后面所有分支都会创建异步任务并提交到线程池中执行
🟢 并行网关后面的出度如果有条件表达式,表达式会被忽略,无论设置与否都不会解析,都会默认为 true
5.3 包含网关
🟢 包含网关与并行网关一样,支持开启异步流程,支持接收多个入度
🟢 包含网关没有全部入度必须到达的限制,等待全部入度执行完成或者得知其中可能有部分入度不满足条件不再执行后,会继续向下
🟢 包含网关后面出度可以设置条件表达式,表达式解析规则与排他网关出度解析规则相同
6、流程 Story
流程定义描述当下的这个流程如何执行属于配置域内容,而流程 Story 则是根据定义好的流程进行的一次次执行,属于运行域内容。两者可以理解成代码中类定义与对象实例的关系,流程定义就是类文件来描述对象的形态内容,对象则是根据类文件创建出的一个个实例。
框架提供StoryEngine
实例来解析流程执行 Story,该实例托管在 Spring 容器中,可以使用任意获取 Spring Bean 的方式来获取StoryEngine
对象。对象提供同步和异步两种方式来执行 Story,值得一提的是,无论同步和异步执行前是一定会被设置超时时间的,超时时间默认是 3s。耗时高场景或 debug 代码时可以根据需要调大到合适的超时时间。
7、StoryBus
流程定义中的服务节点是一个个的独立个体,相互之间不会存在任何的依赖关系,数据也就更无法直接进行传递。但是一次业务诉求又是各节点间通力合作相互配合来完成的,此时就需要了解 StoryBus 这个概念了。StoryBus 是流程 Story 的数据总线,生命周期也会和流程 Story 保持一致。
🟢 每一个服务节点都可以从 StoryBus 中获取需要的参数,执行完之后,再将需要的结果通知到 StoryBus
🟢 StoryBus 中有四个数据域,分别是:
🔷 req:保存请求传入的 request 对象,request 对象本身不会发生变化。但对象里面的值允许通过后期的变量通知发生更新
🔷 sta:保存节点执行完成后产生的变量,一经设置,将不再发生变化,如果出现重复设置会有警告日志(当只有对象的引用在 sta 域时,对象里面的字段还是可以发生更新的,这点与 req 相同)
🔷 var:保存节点执行完成后产生的变量,可以被重复替换,对象里面的字段性质同上
🔷 res:保存最终结果,作为 Story 执行完成后最终的结果返回给调用者(1.0.9 版本开始,result 更名为 res)
🟢 服务节点通过注解(@NoticeXxx
、@XxxTaskParam
、@XxxTaskField
Xxx 泛指上面提到的四个域)和ScopeDataOperator
两种方式来从 StoryBus 中存取变量
🟢 节点出度中,可以定义条件表达式直接引用这四个域做条件判断,如流程编排中出现的:res.img != null
、req.source!='app'
🟢 四个作用域被读写锁保护着,get 获取读锁,notice 获取写锁,防止出现并发问题
🟢 开启异步模式后,同时创建的子任务都可以读写 StoryBus 中的变量,所以数据方面有前后依赖关系的节点,不能被创建到同一时间段执行的不同子任务中,有相关诉求时可以通过聚合节点来保证节点执行时数据的先后依赖顺序
8、能力 &角色
针对平台型服务,首先可以定义编排出通用的链路模型,也就是上面说的流程定义
模型中的某个服务节点,应对不同业务场景或需求方的诉求时,可以扩展不同的服务能力( 比如 A、B 两个业务方都需要抽佣的服务,那么就可以定义一个抽佣的服务节点,然后 A 业务需要比例抽佣,而 B 业务需要阶梯式抽佣,这时就可以在抽佣的服务节点上再扩展两个不同的抽佣能力 )
扩展出来的能力可视作资源,所有的资源都有着独一无二的资源名称,携带着包含某个资源名称的权限对象即可访问与之对应的资源( 资源也可称为:扩展出来的服务能力 )
一批独立的权限对象有着较高的维护成本,所以可依次将某一业务场景所需的全部权限聚合起来组成角色对象
提供平台能力时,根据参数标识判断出具体的业务场景或需求方,并找到与之对应的角色,携带该角色执行预设的链路模型,即可完成定制化的业务诉求
详见:RBAC模式
9、流程回溯
流程回溯可以在链路执行完之后,拿到结果或者异常之前,打印节点执行日志或执行自定义回调方法,可以应对如下问题:
🟢 查看运行过节点的信息如:执行顺序、节点耗时、入参、出参、异常信息等重要数据
🟢 自定义流程回溯日志,或者可以在出现异常时才打印详情日志
🟢 检查节点执行、参数设置等是否符合预期。因为有时结果确实没有报错,但并不代表过程一定没有问题
🟢 如果链路中有自定义角色的操作,检查最终角色是否符合预期
评论