写在前面
每次跟技术同行聊到"我们的流程引擎是自研的"这个话题,对方的第一反应几乎都一样:“为什么不用开源的?Activiti/Camunda不香吗?”
说实话,我们内部当年做这个决策的时候,也纠结了很久。自研意味着更大的投入、更长的周期、更多的坑要自己填。但五年多走下来,回过头看,这是数环通iPaaS平台做得最正确的技术决策之一。
这篇文章想把这个决策过程拆开来讲:当初看了哪些方案,为什么不合适,自研的引擎做成了什么样,踩了哪些坑,最终的收益到底体现在哪里。
市面上的开源流程引擎,各有各的"但是"
Activiti / Camunda — BPMN阵营
这两个可能是Java生态里知名度最高的流程引擎了。基于BPMN 2.0规范,有成熟的流程设计器,社区生态丰富。
优点:
- BPMN标准规范,流程定义可移植
- 有完整的人工审批、会签、加签等能力
- 丰富的事件(信号事件、消息事件、定时事件)
- 商业版Camunda有不错的监控后台
但是:
这两个引擎的设计初衷是企业内部审批流程——人工任务在流程中占主导,节点之间可能间隔数小时甚至数天。所以它们的执行模型是"事件驱动 + 数据库持久化每一步状态"。
对iPaaS场景来说,问题在于:
- 性能模型不匹配。iPaaS流程是"API调用链",一个流程可能调用5-20个外部API,全程几秒到几十秒搞定。每一步都做数据库持久化?在日均百万执行量下,这个IO开销是致命的。
- 流程定义过重。BPMN XML冗长复杂,一个简单的"触发器→数据转换→调API"就要写几十行XML。而iPaaS的流程本质上就是一条数据管道,不需要泳道、不需要人工节点。
- 多租户能力弱。Activiti的多租户只是在数据层面隔离,并发控制、资源限制、流量管控全部需要自己做。
- 扩展成本高。想加一个"连接器调用"的节点类型?得深入理解它的ServiceTask和JavaDelegate机制,跟自有的连接器体系做适配。改造成本不亚于自研。
Temporal / Cadence — 工作流编排阵营
Temporal(原Uber的Cadence项目衍生)是这几年的新秀,在微服务编排领域口碑很好。
优点:
- 天然分布式,支持集群部署
- Activity执行自动重试和超时处理
- 支持长时运行工作流(几天甚至几个月)
- 工作流的持久化和恢复机制很优秀
- 有信号和查询能力
但是:
- 技术栈依赖重。需要部署独立的Temporal Server集群(依赖Cassandra或MySQL + ElasticSearch),运维成本不低。对于一个本身就是SaaS产品的平台来说,多一套基础设施就多一分出问题的概率。
- 编程模型限制。Temporal要求每个Activity是一个独立函数,工作流通过代码编排。而iPaaS的流程是用户在前端可视化拖拽出来的,是JSON配置驱动的。两种范式之间的转换层会非常厚。
- 国内生态弱。文档、社区支持主要面向英文用户,国内SaaS特有的签名算法、认证方式等适配全靠自己。
- 计费困难。SaaS产品需要按执行次数、步骤数计费。Temporal本身不提供这种粒度的度量,需要额外开发。
Apache Airflow — 数据管道阵营
Airflow在数据工程领域是标配,DAG调度能力很强。
优点:
- DAG定义直观,Python代码定义流程
- 调度能力强(Cron、依赖调度)
- 有成熟的任务重试和告警机制
- 社区庞大,Operator丰富
但是:
- Python生态。数环通的技术栈是Java,引入Airflow意味着混合语言部署,增加复杂度。
- 批处理导向。Airflow是为ETL批处理设计的——T+1跑、按天调度。iPaaS需要的是实时事件驱动,一个钉钉审批通过,2秒内触发后续流程。
- 不适合高并发短任务。Airflow一个Worker进程能处理的并发DAG数很有限,远达不到iPaaS单节点数千并发的需求。
- 没有可视化编排能力。面向开发者,不面向业务用户。
Node-RED / n8n — 轻量集成阵营
这类工具跟iPaaS最像,都是可视化流程编排 + 连接器生态。
优点:
- 低代码可视化编排
- 连接器生态丰富(n8n 400+节点)
- 部署简单
- 前端交互设计优秀
但是:
- 单机模型。Node-RED和n8n都是单进程设计,没有集群调度能力。当执行量上去后,只能堆机器,没有统一的队列和调度。
- 没有租户隔离。作为自部署工具很合适,作为多租户SaaS平台的底座完全不行。
- 执行模型太简单。没有暂停恢复、没有并发控制、没有优雅停机。一个流程卡住可能影响整个进程。
- Node.js性能天花板。在大数据量同步场景下(比如一次拉取10万条订单做ETL),受限于V8内存和单线程模型。
我们需要的引擎,长什么样?
把上面的分析综合下来,数环通iPaaS的流程引擎需要满足这些核心诉求:
| 需求维度 | 具体要求 |
|---|---|
| 执行模型 | 内存态运行,只在必要时持久化(而非每步持久化) |
| 并发能力 | 单节点支撑数千流程并发执行 |
| 多租户 | 租户级并发隔离、资源限制、公平调度 |
| 驱动方式 | JSON配置驱动,支持前端可视化编排 |
| 扩展能力 | Handler插件化,新增节点类型不改引擎核心 |
| 运维能力 | 暂停恢复、优雅停机、超时管控 |
| 计费支持 | 步骤级计数、连接器维度统计 |
| 重试机制 | 节点级自动重试,支持固定间隔和动态间隔 |
| 并行分支 | Fork/Join并行执行 |
没有一个开源方案能同时满足上面这些要求。与其在别人的框架上缝缝补补,不如从零设计一个专为iPaaS场景优化的轻量执行引擎。
自研引擎的核心架构
执行模型:Handler Chain + Step推进
引擎的核心执行模型非常简洁。一个流程在运行时由三个核心对象组成:
- FlowInstance:流程定义,包含一组Handler和它们的连接关系
- Execution:一次流程执行的上下文,承载所有运行时数据
- ExecutionHandler:每个节点的执行逻辑,通过
nextHandler串联成链
执行过程就是沿着Handler Chain逐个推进:
TriggerHandler → ConnectorHandler → DataTransformHandler → ConnectorHandler → EndEvent每一步(Step)执行完成后,Runner检查是否有中断信号,没有就推进到下一个Handler:
// 核心执行循环handler.execute(ec,callback);// 步骤完成后检查中断if(interruptControl.executeInterrupt(ec)){return;// 响应中断信号}// 推进下一步ec.newStep(currentStep.getNextHandler(),...);为什么这么设计而不是状态机?
状态机的问题是每次状态变迁都要做持久化(否则无法恢复),这在高频执行场景下是瓶颈。我们的做法是:执行过程全部在内存中完成,只在中断时才做快照持久化。99%的流程一气呵成跑完,不需要任何IO开销。那1%被中断的流程,通过Snapshot恢复到其他节点继续执行。
并发控制:Semaphore + 多级水位
单机数千并发流程执行,最怕的是雪崩。我们设计了一套多级水位的并发控制机制:
// 6级执行决策1.子流程/恢复执行 → 直接放行(skipQueue)2.流程维度并发数超限 → 快速失败或排队3.低于安全水位 → 直接执行4.队列中有排队 → 直接排队(保证公平性)5.达到警告水位 → 快速失败或排队6.组织维度并发超限 → 快速失败或排队底层用Java Semaphore控制许可数:
publicclassConcurrencyControl{privatefinalSemaphoresemaphore;publicvoidacquire(Executionec){semaphore.acquire();// 获取许可,满了就等}publicvoidrelease(Executionec){semaphore.release();// 执行完释放}publicdoublepermitsUsageRate(IntegerpermitsLimits){// (总量-余量)/总量 = 使用率return(permitsLimits-availablePermits())/permitsLimits;}}更巧妙的是,许可数支持通过Nacos动态调整。线上出现性能问题时,可以热调整并发上限而不用重启:
publicvoidchangePermits(intoriginal,inttarget){if(original<target){increase(target-original);// 增加许可}else{decrease(original-target);// 逐个回收许可}}排队机制:不能排的快速失败,能排的入队等待
并非所有流程都适合排队。同步API调用(用户在等响应的)不能排——排10秒用户早超时了。异步的事件触发流程可以排——反正多等几秒用户感知不到。
privatebooleanunableEnqueue(Executionec){// 试运行不排队if(ec.getFlow().isTest())returntrue;// 同步API触发不排队if(TriggerTypeEnum.API_MANAGE_TRIGGER.equals(triggerType)&&!isAsync(ec))returntrue;returnfalse;}排队的流程会做一次快照持久化,等资源空闲后由调度器取出恢复执行。这让引擎在高峰期不会崩溃,只是响应变慢了一点——降级而不是崩溃。
Fork/Join并行:不只是多线程那么简单
iPaaS流程中经常需要并行执行多个分支——比如"同时向钉钉和飞书发通知"、“把一批数据拆成多个批次并行推送”。
我们实现了Fork/Join模型:
protectedList<Execution>runForkContext(Executionec,ExecutionForkContextforkContext){for(ExecutionForkRecordrecord:forkContext.getRecords()){ExecutionforkedEc=ec.fork();// 派生子执行上下文forkedEc.forkStep(record.getCurrentStep().getCurrentHandler(),...);if(isSync){runner.runSync(forkedEc);// 单分支直接同步执行}else{runner.run(forkedEc);// 多分支并行执行}}join(ec.getCurrentStep().getStepId());// 等待所有分支完成}注意这里的优化:如果只有一个分支(实际不需要并行),直接同步执行,避免线程切换开销。这是我们在线上发现的——大量"并行网关"实际只有一条分支(另一条被条件过滤掉了),如果还做线程派发,纯浪费。
Fork等待用Semaphore实现:Fork时acquire,所有分支完成后在JoinGateway中release。简洁且线程安全。
步骤级重试:失败了不用从头来
外部API调用是不稳定的——网络抖动、限流、超时都可能发生。引擎内置了步骤级重试机制:
// 支持两种重试策略// 1. 固定间隔:每隔N秒重试一次// 2. 动态间隔:[1s, 3s, 10s] 逐次递增for(intcurExecCount=start;curExecCount<=stepRetryCount;curExecCount++){// 恢复本步骤的上下文ec.putParameter(currentHandler.getName(),parameter);ec.getCurrentStep().setOutput(output);ec.setNextHandler(nextHandler);ec.setError(error);// 重新执行doExecuteHandler(currentHandler,ec,callback);if(ec.getError()==null)break;// 成功了就跳出Thread.sleep(interval);// 等待后重试}关键设计点:只对外部连接器调用做重试,内部逻辑节点不重试。因为内部节点的失败通常是逻辑错误(比如字段映射配错),重试也不会好。而外部调用的失败大多是瞬时的,重试有意义。
中断信号机制:无侵入的流程控制
引擎支持多种中断类型——手动暂停、部署暂停、手动停止、执行失败、排队中断:
publicenumInterruptTypeEnum{MANUAL_PAUSE,// 用户手动暂停MANUAL_STOP,// 用户手动停止DEPLOY_PAUSE,// 优雅停机暂停EXECUTE_FAILED,// 执行失败QUEUE// 排队等待}中断信号存在本地 Cache中(性能考虑),执行器在每个步骤间隙检测一次。检测到信号后,流程在当前步骤完成后停下来,做快照持久化。
这套机制的精巧在于——对Handler实现完全透明。写一个新的连接器Handler,不需要关心中断逻辑,引擎在外层统一处理。这大大降低了开发新节点类型的心智负担。
超时监控:不让僵尸流程占着资源
每300毫秒巡检一次所有运行中的流程,超过配置时间(默认6小时)的直接终结:
privateJobResultmonitorTimeoutExecutions(){for(Map.Entry<String,Execution>e:executions.entrySet()){if(ec.getRuntime().getDuration()>executionTimeout){timeouts.add(ec);}}for(Executionec:timeouts){ec.setError(newIpaasException(ResultCode.EXECUTION_TIMEOUT));complete(ec);// 释放资源}}看似简单,但如果用Activiti做这个事情,你需要额外部署一个定时任务去查数据库里的运行中流程,然后发信号去中断——链路长、延迟高。我们的实现是纯内存操作,毫秒级响应。
多租户感知:每一层都有隔离
从并发控制到队列排队,引擎的每一层都是租户感知的:
- 并发许可:全局信号量控制总并发,避免单租户打爆整台机器
- 组织级并发限制:
groupConcurrencyLimit限制单个组织的最大并发数 - 排队公平性:队列按组织隔离,一个组织排队不影响其他组织
- 计费统计:按组织+流程维度统计步骤数、连接器调用数
这些能力在开源引擎上几乎不可能开箱获得,都是自研才能做到这种粒度的嵌入。
自研的真实难度
说了这么多好处,也得诚实地讲讲自研的代价。
第一年最痛苦。从执行模型设计、Handler抽象、到线程模型调优,前半年基本在反复推翻重来。特别是并发模型——一开始用的是线程池直接submit,结果高并发下线程数爆炸。后来改成Semaphore许可模型才稳定下来。
边界场景多。Fork/Join看似简单,但嵌套Fork(Fork里面再Fork)、Fork某个分支失败另一个还在跑、Fork和中断信号的交互……这些边界场景的组合爆炸,每一个都是生产事故的潜在来源。
没有参考。用开源方案至少StackOverflow上能搜到别人遇到的问题。自研引擎的坑,全世界只有你自己踩过,只能靠线上案例一个一个打磨。
测试难度大。流程引擎的测试不是简单的单元测试能覆盖的。并发竞态、超时边界、中断时机……这些需要大量的集成测试和混沌工程手段。
我们团队前后投入了3人×12个月才把核心引擎打磨到生产级稳定。这不是一个"随便试试"的投入量。
但回报也是实实在在的
性能方面:单节点稳定支撑2000+并发流程执行。如果用Activiti + MySQL这套方案,同等并发量可能需要3-5倍的机器资源。
灵活性方面:新增一种节点类型(比如CDC触发器、OPC UA协议节点),只需要实现ExecutionHandler接口,不需要动引擎核心代码。过去三年我们加了十几种新节点类型,引擎主循环的代码几乎没改过。
运维方面:优雅停机、暂停恢复、动态调参、超时管控——这些能力是生产环境的刚需,自研可以做到丝滑集成。如果是基于开源方案改造,每一个都是大动作。
产品差异化:排队机制、多级水位降级、租户并发隔离——这些精细化的资源管控能力,直接转化为了SaaS产品的卖点(“不限量执行” vs “保证SLA”)。
发版速度:遇到线上问题,我们能在小时内定位到引擎层面的原因并修复。如果是开源方案,得先判断是框架Bug还是自己的使用问题,光排查就可能花几天。
总结:什么时候该自研,什么时候不该
说到底,是否自研流程引擎,取决于你的业务场景和引擎的贴合度。
建议用开源方案的场景:
- 企业内部审批流程(Activiti/Camunda很合适)
- 数据ETL调度(Airflow是最佳选择)
- 微服务编排且团队熟悉Temporal(上Temporal)
- 个人或小团队做集成工具(n8n开箱即用)
建议自研的场景:
- 做SaaS平台,需要多租户资源隔离
- 高并发短任务(秒级执行、万级QPS)
- 需要深度定制执行模型(配置驱动、非代码驱动)
- 流程执行是核心竞争力,不是辅助功能
- 团队有足够的工程投入能力
对数环通来说,流程引擎是整个产品的心脏。它的性能、稳定性、灵活性直接决定了客户体验。这种核心中的核心,交给自己掌控是值得的投入。
五年打磨下来,这个引擎每天稳定执行着千百万条自动化流程,支撑着上万家企业的集成需求。它不完美——代码里还有不少待优化的地方——但它是真正为iPaaS场景量身打造的。
这,就是自研的意义。
数环通iPaaS——自研流程引擎驱动的企业级集成平台,1000+应用连接器,亿万级流程执行。了解更多:https://www.solinkup.com
标签:#流程引擎 #iPaaS #Activiti #Camunda #Temporal #自研引擎 #并发控制 #微服务编排 #数环通 #工作流引擎 #企业集成 #低代码平台