一个轻量级服务编排框架的实现思路

服务编排的从知到行,我做了一个化繁为简的工程师支援小工具 服务编排提供了图形化、拖拽式开发的能力,来看华为云官网的完整介绍:服务编排是一种通过简单的拖拉拽式流程编排以及参数配置的方式,来进行服务开发的能力,并支持对已开发的服务重新进行组合编排。用户能够在服务编排编辑器内,以图形化编排的形式快速地进行服务的开发并扩展出更丰富的业务功能,同时能够与API接口进行绑定,以API的形式对外提供服务。将代码编程的方式转化为图形化、拖拽式编程的方式,很适合零基础的用户实现自己需要的功能。结合官网介绍,我们来大致总结一下服务编排的优点。 阅读详情
AI摘要

本文讨论了一种有别于传统的Java服务编排技术方案。介绍了服务编排相关名词术语,阐述了流程编排的串行与并行模式、分支条件支持等,还说明了动作执行、泛化调用、表达式求值等内容,给出了实现思路及部分关键代码,同时提及设计中的遗憾与可改进之处。

前言:服务编排(Orchestration)是在微服务架构中常用于聚合调用的低代码实现方案,通过对下游各个领域系统原子接口的排列组合,最终对客户端或前端提供可灵活适配业务的聚合接口,用以降低业务变化带来的技术成本。本文试图讨论一种有别于传统服务编排的技术方案,通过特别设计的运行规则来实现概念简单、配置灵活的服务编排框架,为后来者在设计上打开一定的思路。

前言

微服务发展到现在,几乎成了有一定技术能力公司内新业务技术架构的首选。通过对业务的水平切割,让各个领域的服务能力独立发展,以增强灵活性和易扩展性。但对业务本身而言,切割并不应该是常态,在诸如营销活动领域,往往会根据运营人员天马行空的思路,贯穿多个领域来共同支撑活动举行。为了快速适配业务,服务编排(Orchestration)往往会被技术者们挂在嘴边,并设想出一个“只需要配置就能将已有能力组合成新能力”的技术愿景。本文不讨论这种愿景是否是一个合适的发展方向,只就服务编排本身而言,确实在快速拉起业务上具备一定的优势。

目前常见的服务编排框架,例如Netflix Conductor、Kstry,甚至拓展到其他低代码编排或编舞引擎,往往会设计极其复杂的概念模型。一旦编排的策略变成重要的业务能力,复杂的配置往往会带来工程化的难题。比如模拟了类似流程图的编辑能力,再稍复杂的配置场景中,配置者就需要理解“开始节点”、“条件网关”、“执行节点”、“结束节点”等诸多概念;如果要模拟并行执行,还会要求做出“入网关节点”(FORK)、“出网关节点”(JOIN)配置策略,在数学上明明只是左右括号的概念而已;有些编排引擎会提供用于扩展的高级功能,例如子流程嵌入,那么父子流程会各自包含独立而又可以相互影响的配置。
常见的服务编排形式
本文在笔者以在厂设计营销活动服务编排经历的基础上,受到了devops工作流(Workflow)的启发,希望能够在服务编排上“去繁就简”,以最小的概念设计去设计服务编排的功能,文章描述了一个较为完整的服务编排框架实现思路。

本文来自笔者在厂时内部说明资料的整理,由于篇幅和精力有限,不可能贴出全部代码并加以解释,并且删去了没有特点的部分,整理上可能有不太流畅的地方,望读者们理解。

名词术语

工作流的编排形式

  • Pipeline - 表示一个经过编排的、可独立请求执行的流程,一个Pipeline中会通过节点连线,将多个节点以串联或并联的形式编排在一起
  • 节点(Node) - Pipeline中某个较为独立的步骤,通常由两个部分组成:1)进入节点的条件(默认true,表示一定会执行节点),2)该节点需要执行的动作
  • 连线(Line) - 表示两个节点先后执行的关系,多个节点通过连线有顺序的编排起来,先执行的节点叫做上行节点(lastNode),后执行的节点叫做下行节点(nextNode)。整个pipeline中,只作为上行节点的节点称为起始节点(startNode),只作为下行节点的节点成为结束节点(endNode)
  • 动作(Action) - 表示某个节点中需要执行的内容,通常包括三个部分组成:1)请求的服务和参数配置,2)执行判断,3)结果渲染
  • 表达式(Exp) - 在编排节点、执行动作时需要进行条件判断或参数求值,在本次实现中使用的是QLExpress表达式
  • 上下文(Context)- 在进行表达式求值时,表达式中的变量始终来源于上下文,而上下文数据的来源则包括用户请求、配置数据、服务请求结果等

流程编排

串行与并行编排

在笔者设计编排逻辑里只有节点和连线的概念,上行节点全部执行成功后,才会启动执行下行节点。没有上行节点的节点都是开始节点,没有下行节点的节点都是结束节点。在编排形式上,只设计了串行(Serial)和并行(Parallel)两种编排模式。
如果数据结构的角度来看,整个pipeline就是有向图结构模式,从实现后的运行效果来看,一个pipeline对于图连接要求不太严格,任意连线方式都可支持,但推荐还是按下图按步骤依次串行或并行配置,执行链路更加清晰,对编排者更加友好。

在这里插入图片描述
如上图,要表达先同时执行步骤11、步骤12,全部成功后再同时执行步骤21、步骤22、步骤23,最后执行步骤31,可按上述进行编排。如果某个节点执行出现问题,那么整个pipeline会提前以报错的形式结束执行,该种模式默认隐含了“如果执行出错或错误断言为真就立即退出”的逻辑。

分支条件的支持

上述描述中没有分支选择逻辑,那么这种情况下,如何支持需要条件执行的场景呢?我们可以把条件执行可以看作并行执行的一般场景,只是默认并行分支执行条件都是true。而条件执行下,只有满足某分支的条件时,并行分支才可以执行,而未满足条件的分支需要跳过执行,也因此节点数据里会包含条件字段。
在这里插入图片描述
如上图,步骤31和步骤32分别实现了互斥的条件,那么执行时Pipeline只会选择步骤31进行执行,而步骤32则会跳过。这里需要注意的是,上行节点的执行条件会影响下行节点的执行。上图中,虽然步骤42也满足条件,但由于其依赖的上行节点步骤32没有执行,也没有其他上行节点可以触发步骤42,因此步骤42无法执行。

这里也可以看出来,条件的执行是具备 与 / 或 逻辑的。在节点上配置的条件并不表示错误断言,只是表示连线上是否可以继续运行。而这个条件不仅可以并行配置,也可以串行配置。简单来说,串行条件表示“与”,并行条件表示“或”。
在这里插入图片描述
如上图所示,对于步骤11链路,只要步骤21和步骤22有一条条件得到满足,那么步骤31就可以执行;而步骤12链路,如果步骤23和步骤24有一个条件没有满足,那么步骤32无法执行。换言之,下行节点会沿用所有上行节点的条件。

空节点的作用

笔者设计的Pipeline允许出现空节点,即节点没有配置关联动作,在推进时会直接进入空节点的下行节点。这种空节点有时会有额外的作用:

  1. 只带条件的空节点,那么该节点就会变成流程图中的条件节点;
    在这里插入图片描述

  2. 在某些并行步骤的后面通过加入一个空节点,可以汇聚执行链路,待整体执行完成后再继续进行推进;
    在这里插入图片描述3. 一个无作用的占位符,例如在不允许节点跳线,但又需要跳线的场景。在这里插入图片描述

注:空节点可以起到特别的作用可能是笔者设计失败的产物,见后续小结 “遗憾:动作应该在节点上还是在连线上?”

节点的推进策略

设计好Pipeline的运行规则后,那么应当如何推进整个pipline的运行呢?
这里设计的推进策略的核心规则是:

当前节点尽力尝试推进执行下行节点,收集所有下行节点的推进结果,向上行节点反馈自身包括下行节点的推进结果。

在这里插入图片描述

如上图所示,步骤11执行后会推进执行步骤21和步骤22,但步骤21和步骤22在执行后由于彼此并行隔离,因此两个节点都会设法执行步骤31和步骤32,但实际上只希望它们执行一次,因此最终只会有一个节点(例如步骤22)拿到下行节点的执行权,从而推进流程继续执行。

分析来看,由于多节点存在并行执行的情况,因此每个节点执行后,都应当尽可能向后推进执行,但一个节点最多只能执行一次,因此看右图修剪后的pipeline在实际执行的情况应当是一棵由图进行“修剪”而得到的树。下行节点都可能是上行节点的子节点,但每个下行节点只能作为一个上行节点的子节点,因此当一个上行节点尝试推进执行下行节点时,需要判断该支路是否是否被“修剪”。如此来看,每一个节点的下行节点都将独立的形成一棵子树,同时该节点也会作为其上行节点的子树。每个节点执行后都会尝试推进子树的执行,每棵子树都从顶层节点开始执行,由顶层节点决定支路是否执行或被修剪。由此我们得出一个重要的结论:

当前节点会收集每棵子树的执行结果,结合自身执行结果,得到当前节点所在树的执行结果,并由其上行节点收集。那么起始节点所在树的结果就是整个Pipeline执行的结果。

实现方案

本文只粘贴部分关键代码,实现上并不复杂,只看文字描述即可。

具体实现上,由于pipeline的推进具备树型的计算结构,毫无疑问,类似Java的 Fork/Join 的框架是比较适合的实现方式,可以方便地进行并发线程的管理。

public class PipeRecursiveTask extends RecursiveTask<PipelineRunningState> {
    private PipeTask pipeTask;

    public PipeRecursiveTask(PipeTask pipeTask) {
        this.pipeTask = pipeTask;
    }

    @Override
    protected PipelineRunningState compute() {
        return pipeTask.compute();
    }
}

public class PipeTask {
    // ... ...
    protected PipelineRunningState compute() {
        // 开始节点
        PipelineRunningState runningState;
        List<String> nextUniqueIds;

        /* 执行节点,得到执行状态 */
        if (StringUtils.isEmpty(nodeUniqueId)) {
            runningState = new PipelineRunningState(pipelineContext.getPipelineUniqueId(),"");
            runningState.setExecuteState(PipeStateEnum.SUCCESS);
            runningState.setExecuted(false);

            nextUniqueIds = pipelineContext.getPipeline().getStartNodeUniqueIdList();
        } else {
            runningState = nodeExecutor.apply(this);
            nextUniqueIds = pipelineContext.getPipeline().getNextUniqueIdsMap().get(runningState.getNodeUniqueId());
        }

        runningState.setPipeState(runningState.getExecuteState());
        if (runningState.getExecuteState().equals(PipeStateEnum.ERROR)) {
            runningState.setErrorNodeUniqueId(nodeUniqueId);
        }

        /* 获取后向节点,如果没有则直接返回当前状态 */
        if (CollectionUtils.isEmpty(nextUniqueIds)) {
            runningState.setPipe("arriveEnd");
            return runningState;
        }
        /* 节点不需要推进,则直接返回当前状态 */
        if (! runningState.getExecuteState().isNeedContinue()) {
            runningState.setPipe("discontinue");
            return runningState;
        }
        /* 并行执行所有后向节点,透出推进状态 */
        long nextStartTime = System.currentTimeMillis();
        List<PipelineRunningState> nextRunningStateList = Lists.newArrayList();
        if (nextUniqueIds.size() == 1) {
            PipelineRunningState nextRunningState = new PipeTask(nextUniqueIds.get(0), nextStartTime, pipelineContext, nodeExecutor).compute();
            nextRunningStateList.add(nextRunningState);
        } else {
            Map<String, PipeRecursiveTask> recursiveTaskMap = Maps.newHashMap();
            for (String nodeUniqueId : nextUniqueIds) {
                PipeTask nodeTask = new PipeTask(nodeUniqueId, nextStartTime, pipelineContext, nodeExecutor);
                PipeRecursiveTask recursiveTask = new PipeRecursiveTask(nodeTask);
                recursiveTaskMap.put(nodeUniqueId, recursiveTask);
                recursiveTask.fork();
            }
            recursiveTaskMap.forEach((currentNodeUniqueId, recursiveTask) -> nextRunningStateList.add(recursiveTask.join()));
        }
        PipelineRunningState pipeState = calNextRunningState(runningState, nextRunningStateList);
        runningState.setPipe("nextForward");
        runningState.setPipeState(pipeState.getPipeState());
        if (StringUtils.isNotEmpty(pipeState.getErrorNodeUniqueId())) {
            runningState.setErrorNodeUniqueId(pipeState.getErrorNodeUniqueId());
        }
        runningState.setNextRunningStateList(nextRunningStateList);
        return runningState;
    }

注:树形计算结构都可以使用Fork/Join。

这里我们设计节点有如下运行状态:

public enum PipeStateEnum {
    /* 不具备执行条件 */
    NONE("NONE", "无", false, false),
    /* 运行 */
    RUNNING("RUNNING", "运行",false, false),
    /* 错误 */
    ERROR("ERROR", "错误", true, false),
    /* 成功 */
    SUCCESS("SUCCESS", "成功", true, true),
    /* 修剪 */
    PRUNING("PRUNING", "修剪", true, true),
    ;

    private String code;
    private String msg;
    private boolean finished;
    private boolean needContinue;
    // ... ...

在这里插入图片描述
这里有两个核心处理逻辑:

  1. 当前节点如何判断所推进的支路是否需要执行:当前节点会被所有的上行节点尝试推进执行,因此当前节点每次被触发推进的时候,会收集所有其上行节点的执行结果,只有在所有上行节点执行成功后,并且自身成功抢占了执行权,才可以执行。

关键代码:

public class NodeExecutorImpl implements NodeExecutor {
    // ... ...
    /** 抢占状态 */
    public PipeStateEnum calAndOccupiedNodeState(PipelineNode node, List<String> lastNodeUniqueIds, PipelineContext pc) {
        synchronized (node.getUniqueId()) {
            if (pc.getNodeState(node.getUniqueId()) != null) {
                return PipeStateEnum.NONE;
            }
            PipeStateEnum nodeState = calCurrentNodeState(lastNodeUniqueIds, pc);
            if (nodeState.equals(PipeStateEnum.NONE)) {
                return PipeStateEnum.NONE;
            }
            pc.putNodeState(node.getUniqueId(), nodeState);
            return nodeState;
        }
    }
    /* 检查节点 */
    /* 有一个为NONE、RUNNING --> NONE : 上游有未执行完的节点,本节点不执行 */
    /* 有一个ERROR --> NONE : 上游有执行失败的节点,本节点不再执行 */
    /* 全部为PRUNING --> PRUNING : 上游有不满足执行调节的节点,本节点也需要跳过 */
    /* 其余 --> RUNNING : 上游有执行成功的节点,本节点需要继续执行 */
    private PipeStateEnum calCurrentNodeState(List<String> lastNodeUniqueIds, PipelineContext pc) {
        if (CollectionUtils.isEmpty(lastNodeUniqueIds)) {
            return PipeStateEnum.RUNNING;
        }

        boolean none = false;
        boolean error = false;
        boolean pruning = true;

        for (String nodeUniqueId : lastNodeUniqueIds) {
            PipeStateEnum nodeState = pc.getNodeState(nodeUniqueId);
            if (nodeState == null || nodeState.equals(PipeStateEnum.NONE) || nodeState.equals(PipeStateEnum.RUNNING)) {
                none = true;
                break;
            }
            if (nodeState.equals(PipeStateEnum.ERROR)) {
                error = true;
                break;
            }
            if (! nodeState.equals(PipeStateEnum.PRUNING)) {
                pruning = false;
            }
        }

        return none    ? PipeStateEnum.NONE :
               error   ? PipeStateEnum.NONE :
               pruning ? PipeStateEnum.PRUNING :
                         PipeStateEnum.RUNNING;
    }
    // ... ...
  1. 当前节点如何根据自身和下行的执行结果,向上行返回执行结果:在当前节点执行成功的前提下,如果有下行节点执行失败,则以该下行节点作为本节点执行结果向上行节点反馈;如果下游没有执行失败的节点,则以当前节点结果向上行节点反馈。
    关键代码:
public class PipeTask {
    // ... ...
    /* 检查节点 */
    /* 有一个ERROR --> ERROR 节点 : 后向有节点报错 */
    /* 有一个SUCCESS --> SUCCESS 节点 : 后向有节点成功 */
    /* 全部PRUNING --> 当前节点 : 后续节点都未执行*/
    /* 其余 --> 当前节点 */
    public static PipelineRunningState calNextRunningState(PipelineRunningState currentState, List<PipelineRunningState> nextRunningStateList) {
        PipelineRunningState errorState = null;
        PipelineRunningState successState = null;

        for (PipelineRunningState nextRunningState : nextRunningStateList) {
            if (nextRunningState.getPipeState().equals(PipeStateEnum.ERROR)) {
                errorState = nextRunningState;
            }
            if (nextRunningState.getPipeState().equals(PipeStateEnum.SUCCESS)) {
                successState = nextRunningState;
            }
        }

        return errorState != null   ? errorState   :
               //successState != null ? successState :
                                      currentState ;
    }

整个pipeline的核心逻辑只有三处,一个Fork/Join的实现,两处推进策略

扩展能力:编排组合

Pipeline内部的联系仅依赖线进行组织,而运行时不依赖节点的实际归属Pipeline,因此通过增加线、增加起始点,可以实现串接、并联、替换等组合,可具备类似父子流程嵌套的能力,而不需要额外增加概念设计。下图展示两个Pipeline串接、并联、替换某节点后的有向图形式。
在这里插入图片描述
从理论上而言,可以通过设计子Pipeline,然后通过一定规则拼接的形式,从而实现功能扩展。但读者们可以思考以下几个问题:

  • 组合场景应用于业务,实用性未必很强;
  • 由于节点可以任意组合,因此每个节点的执行动作不能存在冲突,这一点对配置数据和规范的要求比较高;
  • 多个Pipeline组合后依然是一个Pipeline,最后只能生成一份结果,对于会生成结果的节点需要额外关注

遗憾:动作应该在节点上还是在连线上?

本次设计上,节点负责所有的能力,而连线仅负责连接。这未必是一个好的设计形式。系统首次上线后,笔者也反复思考,可能实现目标上不会有差别,但是在连线上配置动作执行应该更合适。只是已然上线,只能一条路走到底。望读到此处的朋友们有类似设计需求时可以注意一下。
请添加图片描述

动作执行

该部分整体并不复杂,调用形式也很简单。

基本流程

每个节点会关联一个执行动作,该执行动作是节点执行的本体。每个动作包含三个部分:1)服务调用,2)执行判断,3)结果渲染。并且这三个部分是可以独立配置在动作里的,也就意味着可以有专门的节点负责服务调用、专门的节点负责结果渲染,也可以合并配置。
在这里插入图片描述

执行判断

动作执行时可以针对当前上下文进行判断,判断时会依次进行对规则进行求值,一旦匹配规则便立即退出,并按配置的失败方式返回,如果匹配不到则默认判断为真。因此对于规则配置的要求是需要错误条件要覆盖完整。

在这里插入图片描述
在执行判断上,也可以支持全真/全假/单真/单假的多条件配置,在前期使用上可能并不比单表达式直接判断更方便,这里不做描述。

结果合并

如果一个Pipeline中有多个Action产生执行结果,那么最终Pipeline将尝试对结果进行合并。如果是对结果参数进行渲染,那么渲染后的结果会和当前pipeline已有的结果尝试进行合并。对于无法合并的情况,新渲染的值会覆盖已有的值,因此如果没有特别要求,建议结果渲染最外层配置使用结构体配置,防止覆盖丢失。这种合并只支持一级合并,无法对子项合并。在合并结果中,只分为三种类型:

  • 基本类型,包括布尔型、整形、字符串等无结构类型;
  • 结构体类型,主要带字段的对象或Map类型;
  • 列表类型,主要指数组、列表等集合类型。
旧值类型 ↓ | 新值类型 →基本类型结构体类型列表类型
列表类型新值覆盖新值覆盖两个列表按第一层进行合并
基本类型新值覆盖新值覆盖新值覆盖
结构体类型新值覆盖两结构体按字段合并,字段同名时,新值覆盖旧值新值覆盖

实现代码

public class ActionBizServiceImpl implements ActionBizService {
    // ... ...
    @Override
    public ActionResult doAction(Action action, PipelineContext pc) {
        ActionResult actionResult = new ActionResult(action);
        actionResult.setStartTime(System.currentTimeMillis());
        try {
            doAction(action, actionResult, pc);
        } catch (BffBizException be) {
            actionResult.setEndTime(System.currentTimeMillis());
            actionResult.setCode(be.getCode());
            actionResult.setMsg(be.getMessage());
        } catch (Exception e) {
            actionResult.setEndTime(System.currentTimeMillis());
            actionResult.setCode(BizErrorEnum.UNKNOWN_EXCEPTION.getCode());
            actionResult.setMsg(e.getMessage());
        }
        return actionResult;
    }

    @Override
    public void doAction(Action action, ActionResult actionResult, PipelineContext pc) {
        /* 请求服务 */
        ServiceResult serviceResult = invokeService(action, pc);
        actionResult.setServiceResult(serviceResult);
        pc.saveActionResult(actionResult);
        if ((action.getServiceErrorIgnore() == null || !action.getServiceErrorIgnore()) && serviceResult != null) {
            if (! serviceResult.isSuccess() || serviceResult.getCode() != 0) {
                actionResult.setCode(serviceResult.getCode());
                actionResult.setMsg(serviceResult.getMsg());
                actionResult.setEndTime(System.currentTimeMillis());
                return;
            }
        }

        /* 执行判断 */
        boolean judge = judgeStrategy(action, actionResult, pc);
        if (! judge) {
            actionResult.setEndTime(System.currentTimeMillis());
            return;
        }

        /* 组装结果 */
        constructResult(action, pc);

        actionResult.setSuccess(true);
        actionResult.setCode(BizErrorEnum.SUCCESS.getCode());
        actionResult.setMsg(BizErrorEnum.SUCCESS.getMsg());
        actionResult.setEndTime(System.currentTimeMillis());
    }

    private ServiceResult invokeService(Action action, PipelineContext pc) {
        if (StringUtils.isEmpty(action.getService())) {
            return null;
        }
        Object params = evalBizService.render(action.getServiceParamField(), pc);
        return serviceProviderBizService.invoke(action.getService(), action.getServiceProtocol(), params, pc, action.getServiceCacheObj());
    }

    private boolean judgeStrategy(Action action, ActionResult actionResult, PipelineContext pc) {
        if (CollectionUtils.isEmpty(action.getJudgeStrategyList())) {
            return true;
        }
        for (JudgeStrategy strategy : action.getJudgeStrategyList()) {
            boolean judge = evalBizService.judge(strategy.getRule(), pc);
            if (judge && strategy.isSuccessFlag()) {
                return true;
            } else if (judge) {
                actionResult.setCode(strategy.getErrorCode());
                actionResult.setMsg(strategy.getErrorMsg());
                return false;
            }
        }
        return true;
    }

    private void constructResult(Action action, PipelineContext pc) {
        if (action.getResultParamField() != null) {
            Object result = evalBizService.render(action.getResultParamField(), pc);
            pc.putResult(result);
        }
    }
}

Action编排形式讨论

Action的三个步骤:服务调用、执行判断、结果渲染,是可以独立生效的,一个Action甚至可以拆成三个Action分别出现在不同的节点上。这种设计除了考虑灵活性外,更多的是对用户行为模式的推测。很有可能用户在配置Pipeline时采用以下固定的配置模式

在这里插入图片描述

实际使用中,大家更喜欢在最后一步配置结果渲染。

泛化调用

泛化调用主要用于向上游屏蔽不同服务的调用差异,在网关类系统设计中是比较常见的功能。这里没有过多描述,参考其他网关实现即可。

上层动作执行时,必须要服务调用器传入三种参数:调用的服务接口、调用的服务协议(HTTP/RPC)、调用服务的参数,服务调用器会根据协议,会生成一对对象,一个是请求对象,一个是实际调用器。通过调用器调用请求对象,则拿到调用结果。服务调用器catch所有的调用异常,统一以success和code的形式向动作执行器报告执行结果。

在这里插入图片描述

表达式求值

在整个Pipeline运行过程中,需要组织各处参数,计算数据结果,如果全部采用配置模式,功能受限、设计复杂、配置繁琐。这里引用了求值器,根据配置的表达式进行求值。目前使用求值器的地方有两处:

  • 谓词判断:用在Pipeline节点的分支条件判断和动作执行判断时的结果断言上。该表达式必须是一个布尔表达式,通过计算上下文参数的真假,来判断条件是否成立;
  • 参数渲染:用在动作执行请求服务参数渲染以及结果渲染上,该表达式最终需要生成一个对象,作为运行时的参数。

笔者在实现过程中,没有找到特别满意的求值器,即便是自己设计的也不是很满意。最终笔者采用了工厂模式,提供多个求值器,允许用户自己选择合适的求值器运行脚本,也允许开发者扩展新的求置器:

  • QLEXPRESS - 基于QlExpress规则引擎而扩展的求值器,功能范围支持较广,也是默认的求值器;
  • VELOCITY - 基于Velocity模板渲染的求值器,可以根据参数变量渲染常规字符串或JSON字符串。该求值器可以实现和QlExpress一样的功能范围,优点在于简单场景配置简单,但对于复杂场景配置过于复杂;
  • JSON - 基于Json反序列化的求值器,可以配置对象或JSON字符串。该求值器只能做常量配置;
  • FAST - 笔者设计用于实现快速参数赋值的求值器。

后记

本次服务编排在设计上有很多笔者觉得可以改进或提升的地方,后续如果有机会进行调整,会把最新的思路分享给大家。

Kstry流程编排框架 Kstry是什么? 1、所见( 图示模型 )即所得( 代码执行 )的可视化流程编排框架 2、可轻易将流程从串行升级到并行,支持任务拆分、任务重试、任务降级、子任务遍历、指定超时时间的并发框架 3、共享能力平台侧的微服务业务整合框架 4、类似阿里TMF2.0,可以做到平台与业务分离,业务与业务隔离的服务化框架 阅读详情

相关推荐

阿里开源Skill编排引擎:轻量级Agent调度框架

Skill(技能)是AI工程化中可复用、可编排、可治理的最小执行单元,其核心原理在于将业务函数自动转化为LLM可理解与调度的语义化接口,绕过传统Function Calling的Schema映射困境。技术价值体现在零代码改造接入现有服务、跨语言跨环境部署能力,以及执行可观测性与熔断治理机制。典型应用场景包括SaaS系统间胶水层编排、边缘设备轻量Agent落地、遗留系统AI增强等。本文聚焦qianwen-ai生态下的生产级Skill编排实践,深入解析Skill注册、Plan生成、上下文传递与多环境Runtim

weixin_29039773的博客 235

谈谈服务编排

最近,同事Spring微服务技术架构网上应用出现了服务堵塞,监控不到服务运行(业务进展情况),以及需求变更困难、维护成本高等情况,再回顾以前数据不一致等情况,通过讨论分析发现系统架构中没有使用流程方法的服务编排。

肖永威的专栏 2万+

轻量级编排引擎colibri:微服务链路编排的设计与实战

微服务架构中,业务链路的编排往往面临同步调用耦合深、链路长、性能瓶颈明显等挑战。编排引擎通过节点状态机与异步事件驱动,将复杂流程拆解为可管理的执行单元,实现并行分支、超时控制、失败降级与重试容错。相比引入重型工作流框架,轻量级自研方案在部署成本和灵活性上具有显著优势,尤其适合中小团队和特定业务场景。以秒杀库存扣减为例,通过并行化风控与资格校验,可将链路耗时缩短近半并有效提升系统QPS。本文从编排引擎的概念和原理出发,结合工程实践,系统阐述了轻量级编排的设计思路、核心实现与落地经验,为微服务链路治理提供了一套

weixin_31315567的博客 269

Netty学习五:Netty框架之服务编排

Netty学习系列

weixin_41402069的博客 1475

轻量级特征编排框架higgsfield:实现特征版本管理与训练可复现

特征工程是机器学习流程中最易失控的环节,特征定义分散、数据质量波动、版本追溯困难,常导致模型实验不可复现。特征编排的理念由此而生——将特征视为具备规范、校验与版本化能力的资源,通过统一框架管理从原始数据到模型产物的全链路。轻量级编排框架higgsfield借鉴希格斯场赋予粒子质量的思想,为原始数据注入工程意义上的“质量”,让特征在版本化、可校验、可依赖的条件下被模型训练稳定消费。此类方案无需部署重型调度或特性存储平台,适用于中小型算法团队在现有Python脚本基础上快速落地。通过YAML配置与Python装

weixin_32678995的博客 277

MCP Gateway:AI服务联邦编排的轻量级协议桥接中枢

AI服务编排是指在异构模型与工具之间建立标准化调用契约,其核心原理在于协议治理而非模型优化。通过协议桥接、语义注入与服务联邦,实现REST API向MCP Tool的无损升维,显著提升AI能力复用效率与工程交付确定性。该技术价值体现在降低多源AI服务集成成本、强化跨团队协作治理能力,并支撑RAG、智能体(Agent)及金融/医疗等高合规场景的稳定落地。MCP Gateway正是这一范式的典型实践,以Go语言实现轻量嵌入、声明式YAML配置与零信任安全默认,让‘服务即配置’成为现实。

weixin_42683392的博客 278

Camunda微服务编排思路

本文重点讲解下怎么使用Camunda框架进行微服务的编排。Camunda工作流引擎支持轻量级微服务编排,包括业务流程的end-to-end(端点到端点)监控。 如何处理微服务混乱编排? 近年来,微服务架构越来越受欢迎,并且有充分的理由去使用微服务的一些优秀架构思想:团队可以在使用他们选择的技术栈时快速独立地提供一些很好的价值,而不会受制于单服务所带来的一组共同技术约束问...

分享牛 3万+

OpenClaw:面向AI Agent的轻量级技能编排框架

AI Agent开发中,‘技能(Skill)’作为可复用、可编排的功能单元,正成为大模型应用落地的关键抽象。其核心原理在于将业务能力解耦为标准化接口,通过契约化定义(输入/输出模型、超时、重试等)实现统一调度与可观测性。这种设计显著提升工程效率与系统稳定性,支撑客服自动化、多源数据处理、企业API集成等典型场景。OpenClaw正是聚焦于此的技术方案——它不替代LLM编排框架,而是提供专注的Skill运行时,强调原子性、可测试性与上下文链式调用,契合当前AI工程化对模块化、可维护性的迫切需求。

weixin_34101914的博客 308

Spring AI 流程编排:AI时代,你需要一个更轻量的 Spring 流程编排框架

本文论述AI时代轻量级Spring流程编排框架的必要性。随着AI应用从简单模型调用发展为复杂流程系统,传统重量级工作流平台过于笨重,而Salt Function Flow等轻量框架能以Java原生DSL嵌入Spring Boot,通过next、concurrent、loop等API统一编排Prompt、LLM、Agent、RAG等节点,实现AI与业务逻辑的同构执行,让流程结构显式可见、执行控制与业务分离,填补"需要编排但无需BPMN平台"的中间场景。

花千树的专栏 825

基于DAG的轻量级任务编排器设计与实现

任务编排是解决复杂工作流依赖与并发执行的核心技术。其基本原理是通过有向无环图(DAG)模型抽象任务节点与依赖关系,利用调度算法动态解析执行路径,实现任务间的顺序与并行控制。该技术能显著提升数据处理、自动化流水线等场景的执行效率与可靠性。本文聚焦于轻量级实现,深入剖析了以DAG和状态机为核心的设计思想,并详细解读了调度器与执行器解耦、任务生命周期管理、失败传播等关键工程实践。通过一个图片处理流水线的实战案例,演示了如何快速构建可维护、高并发的任务流程,为开发者提供了一套清晰、可控的轻量级解决方案。

weixin_30851867的博客 343

hermes-agent:轻量级消息代理与任务编排框架详解

在分布式系统与微服务架构中,服务间的通信与任务调度始终是工程实践的关键挑战。消息代理(Message Broker)作为一种异步解耦机制,通过引入统一的消息模型,使生产者和消费者无需感知彼此位置,从而降低系统耦合度。在此基础上,任务编排(Orchestration)进一步将多个处理步骤组合成可配置的流水线,支持条件分支、并行执行和结果聚合,为复杂业务自动化提供了基础。这类技术广泛应用于AI服务集成、定时任务聚合、多系统数据同步等场景,能够显著减少胶水代码并提升运维效率。然而,传统消息中间件往往过于重量,而轻

weixin_31412781的博客 308

轻量级资源调度与执行编排框架rea的设计与实现

资源调度与任务编排是分布式系统的核心基础能力,其基本原理是通过资源感知、调度决策与执行反馈实现计算任务在节点间的合理分配。在边缘计算与端侧推理场景下,传统重型调度系统因启动慢、依赖多而难以适用,轻量级调度框架因此成为工程实践中的重要方向。这类框架通常采用单进程事件循环模型,结合cgroup实现资源隔离,通过心跳机制维护节点状态,以最佳适配策略提升资源利用率。其技术价值在于以极低开销完成任务的可靠分发与执行,适用于边缘网关、嵌入式设备及小型集群等资源受限环境。rea正是面向这一需求设计的轻量级调度与执行编排方

weixin_33507732的博客 185

微服务架构设计原理与实战:如何进行微服务的编排

作者:禅与计算机程序设计艺术 1.背景介绍 软件开发中存在着一个痛点就是业务的快速增长带来的技术难题。而服务化架构也提供了一种解决方案。由于其分离的模块化、松耦合、可独立部署等特点,微服务架构能够帮助企业减少单体应用的复杂性,从而实现业务需求的快速响应和迭代更新。然而微服务的采用往往需要引入一定的组

AI天才研究院 742

轻量级Node.js流程编排框架ruflo设计与实现

在现代Web应用中,随着业务逻辑复杂度的提升,异步任务之间的依赖关系与执行顺序往往成为工程维护的痛点。传统的回调函数与Promise链虽然能解决基础异步问题,却难以应对条件分支、并行执行、超时重试等复合场景,导致代码中充斥着大量胶水逻辑。流程编排作为一种将业务逻辑与执行顺序解耦的架构思想,通过声明式的方式定义任务之间的拓扑关系,能显著提升系统的可维护性与可扩展性。在Node.js生态中,轻量级的流程引擎可以在不引入重型依赖的前提下,为开发者提供类似工作流引擎的核心能力,使复杂的异步任务调度变得清晰可控。本文

weixin_33924312的博客 339

告别两个电源模块!用UCD3138+四开关Buck-Boost,一个电路搞定电池充放电(附原理图分析)

本文详细介绍了采用UCD3138数字控制器和四开关Buck-Boost拓扑的双向DC/DC变换器设计,实现单一电路完成电池智能充放电管理。该方案显著降低BOM成本和PCB面积,提升系统效率至96.2%,适用于UPS、通信基站和储能系统,具有高效、紧凑和成本优势。

weixin_30256901的博客 397

关系代数表达式优化步骤

关系代数表达式优化步骤 本篇主要讲解怎么画查询语法树并对其优化,因为我在学关系代数的语法树的时候,在网上找不到比较详细的教法或者技巧,最后通过答案反推原理,所以想写一篇技巧来描述一下这类题的解题方法。 先上书内讲解 1、构造查询树 第一步:把用高级语言定义的查询转换为关系代数表达式 ★ 以 SELECT子向对应投影操作,以FROM字向对应笛卡尔积以 WHERE子句对应选择操作,生成原始查询树 ★ SQL语句转化为原始查询树 第二步:把关系代数表达式转换为查询树。 注: 查询树是一种表示关系代数表达式的树形结

m0_51718449的博客 7015

轻量级流处理框架Loong:声明式数据编排与实时计算实践

流式数据处理是现代数据架构的核心组件,它通过连续处理无界数据流,实现了低延迟的数据分析与响应。其基本原理是将数据视为持续流动的事件序列,通过管道化的处理单元进行实时转换、过滤与聚合。这项技术的核心价值在于能够支撑实时监控、事件驱动架构和即时决策等场景,有效弥补了传统批处理的延迟短板。在工程实践中,Apache Flink和Spark Streaming等框架虽功能强大,但其复杂性和资源消耗常成为轻量级场景的负担。针对这一痛点,轻量级流处理框架应运而生,它们通过声明式编排和嵌入式设计,大幅降低了开发与运维门槛

weixin_30596343的博客 665

MT6816磁编码器在无人机电调中的实战应用:从选型到调试全记录

本文详细介绍了MT6816磁编码器在无人机电调系统中的实战应用,涵盖选型、硬件集成、软件配置到飞行测试的全流程。通过对比霍尔传感器和光电编码器,突出MT6816在角度分辨率、安装公差和抗振动能力等方面的优势,为无人机动力系统提供高精度控制解决方案。

pandas7gardener的博客 232

ruflo 流程编排指南:用 Rust 轻量级框架简化复杂业务

在微服务架构与异步任务日益复杂的今天,流程编排成为控制业务逻辑、管理失败重试与超时补偿的关键技术。传统重量级工作流引擎部署成本高,自研状态机又容易陷入重复造轮子的困境。ruflo 作为基于 Rust 的轻量级编排库,以节点、依赖、上下文三大原语为核心,通过声明式 DSL 串联业务步骤,天然支持异步并行、失败重试与补偿机制,为服务编排、数据管道、任务调度等场景提供低侵入、高性能的解决方案。本文从流程引擎的选型对比出发,结合真实订单示例详细拆解 ruflo 的节点模型、DSL 写法、上下文传递及容错配置,帮你快

weixin_33600816的博客 89

RAID 0/1/5/10 性能实测:4块 SATA SSD 组阵列,顺序读写速度对比

本文通过实测对比了RAID 0/1/5/10在4块SATA SSD上的顺序读写性能,详细展示了不同RAID级别的配置方法、理论性能与实际测试数据。测试结果显示,RAID 0在顺序读写速度上表现最佳,而RAID 10则在性能与数据安全之间取得了平衡,适合数据库服务器等关键应用场景。

weixin_33462167的博客 423

揭秘Outlook/QQ邮箱伪造:如何利用Unicode编码绕过SPF检测

本文深入剖析了利用Unicode编码进行邮件显示名伪造的攻击手法,揭示了攻击者如何通过插入零宽字符等特殊编码绕过SPF检测,使邮件在Outlook、QQ邮箱等客户端中看似来自可信发件人。文章通过Python代码示例演示了伪造过程,并提供了从用户自查到管理员部署DMARC策略的多层次防御建议。

weixin_29063035的博客 332

多Agent协作不混乱:轻量级编排框架的消息总线设计

在构建大模型应用时,多Agent系统常因相互直连导致上下文混乱、流程失控。如何让多个智能体有序协作,是工程落地中的关键挑战。以消息总线为核心,通过解耦Agent间依赖、统一路由与调度,可显著提升系统的可扩展性与可观测性。这类机制不仅适用于多角色协同写作、代码评审等场景,也为复杂任务自动化提供了轻量级实现思路。文章以此为主线,剖析了基于消息的路由、状态机调度及pydantic与asyncio的组合实践,并结合真实踩坑记录,帮助开发者理解如何设计一个简洁、可控的Agent协作框架,让消息准确传递、流程清晰可追踪

weixin_33393017的博客 166

ServiceNow Discovery & CMDB Interview Questions and Answers (2025)

【代码】ServiceNow Discovery & CMDB Interview Questions and Answers (2025)

weixin_47584092的博客 910

轻量级Agent框架设计实战:从工具调用到任务编排

在AI应用开发中,智能体(Agent)已成为连接大模型与真实业务系统的关键桥梁。其核心原理并非让模型直接输出答案,而是通过工具调用、记忆管理与任务编排,将自然语言目标拆解为可执行的步骤序列。一个成熟的Agent系统,技术价值在于既保持模型决策的灵活性,又通过工具边界定义和流程可观测性确保生产环境的可靠性。从自动化工单处理、代码分析到运营报表生成,Agent正在批量替代繁琐的人工操作流程。然而,实际落地常面临工具选择不准、上下文溢出、死循环等工程挑战。本文从一个内部项目实践出发,剖析了工具注册的细节规范、动态

weixin_30983353的博客 215

【Claude Code】Extra inputs are not permitted 报错:代理网关剥离 Beta 标头的修复

通过代理或 LLM 网关连接 Anthropic API 时,Claude Code 报 "Extra inputs are not permitted"?根因是中间件删除了 anthropic-beta 请求标头,导致 Beta 字段被 API 拒绝。本文详解标头转发配置和 CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS 回退方案。

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 123

YOLOv8改进策略【YOLO和Mamba】| 2024 VM-UNet,高效的特征提取模块VSS block 二次创新提高精度

VM-UNet模型设计的出发点是解决现有CNN-based和模型在医学图像分割中的局限性,利用(SSMs)的优势,提出一种更有效的医学图像分割模型。具体来说,CNN-based模型在捕捉长距离信息方面存在不足,而模型由于自注意力机制的二次复杂度导致计算负担较重。SSMs模型如Mamba不仅在建模长距离依赖方面表现出色,还具有线性计算复杂度,这为VM-UNet的设计提供了理论基础。

Limiiiing的博客 616
上一篇: 用高阶函数轻松实现Java对象的深度遍历
下一篇: 企业网网络架构“拓扑还原”技术探索
coderlius
博客等级 码龄9年 141粉丝 10原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值