一、 问题现象
在某次工作流业务迭代后,测试环境突然出现接口响应超时、应用服务器 CPU 使用率飙升的现象。查看应用日志,发现 Activiti 引擎在短时间内疯狂打印 DEBUG 级别的执行日志,且伴随大量重复的节点流转记录。
截取其中一段核心日志如下:
text
2026-09-16 15:38:33.174 [http-nio-5500-exec-27] DEBUG o.a.e.i.a.DefaultActivitiEngineAgenda - Operation class org.activiti.engine.impl.agenda.ContinueProcessOperation added to agenda
2026-09-16 15:38:33.175 [http-nio-5500-exec-27] DEBUG o.a.e.i.i.CommandInvoker - Executing operation class org.activiti.engine.impl.agenda.ContinueProcessOperation
2026-09-16 15:38:33.175 [http-nio-5500-exec-27] DEBUG o.a.e.i.a.ContinueProcessOperation - Sequence flow 'sid-A31E8830-CCC6-41E8-8503-B828995B0B94' encountered. Continuing process by following it using execution 3515010
...
2026-09-16 15:38:33.181 [http-nio-5500-exec-27] DEBUG o.a.e.i.a.ContinueProcessOperation - Executing activityBehavior class org.activiti.engine.impl.bpmn.behavior.ExclusiveGatewayActivityBehavior on activity 'sid-26F76F2A-1691-4440-A305-7040EBA091D0' with execution 3515010
2026-09-16 15:38:33.182 [http-nio-5500-exec-27] DEBUG o.a.e.i.b.b.ExclusiveGatewayActivityBehavior - Leaving exclusive gateway 'sid-26F76F2A-1691-4440-A305-7040EBA091D0'
2026-09-16 15:38:33.182 [http-nio-5500-exec-27] DEBUG o.a.e.i.b.b.ExclusiveGatewayActivityBehavior - Sequence flow 'sid-A31E8830-CCC6-41E8-8503-B828995B0B94' selected as outgoing sequence flow.
...
2026-09-16 15:38:33.200 [http-nio-5500-exec-27] DEBUG o.a.e.i.a.ContinueProcessOperation - Executing activityBehavior class org.activiti.engine.impl.bpmn.behavior.ExclusiveGatewayActivityBehavior on activity 'sid-26F76F2A-1691-4440-A305-7040EBA091D0' with execution 3515010
从日志中可以提取出三个关键特征:
- 时间戳极短 :从
15:38:33.174到15:38:33.200,在 26 毫秒内完成了多次节点流转,说明这不是正常的业务等待,而是同步的 CPU 密集型计算。 - Execution ID 固定 :所有的流转都发生在同一个执行实例
3515010上,没有产生子执行(Child Execution)。 - 节点与连线重复 :流程在排他网关
sid-26F76F2A-1691-4440-A305-7040EBA091D0和序列流sid-A31E8830-CCC6-41E8-8503-B828995B0B94之间反复横跳,形成闭环。
这是一个典型的 Activiti 引擎死循环(Infinite Loop)问题。
二、 引擎执行机制与根因剖析
要理解为什么会发生死循环,需要深入了解 Activiti 的核心执行机制。
1. Agenda 与 ContinueProcessOperation
Activiti 5/6 及 Flowable 引擎采用了基于 Agenda(议程)的命令模式来驱动流程流转。当流程向前推进时,引擎会将 ContinueProcessOperation 放入 Agenda 队列中。该操作负责判断当前节点类型:
- 如果是连线(Sequence Flow),则继续寻找目标节点。
- 如果是活动节点(Activity),则执行对应的
ActivityBehavior。
2. 排他网关的路由逻辑
当日志显示执行到 ExclusiveGatewayActivityBehavior 时,引擎会调用其 leave() 方法。排他网关的职责是评估所有出线(Outgoing Sequence Flows)上的条件表达式(Condition Expression),并选择第一条 评估结果为 true 的连线继续执行。
3. 死循环的形成机制(根因)
经过排查 BPMN XML 流程定义文件,发现问题出在流程设计器的操作失误上:在排他网关上多拉了一根序列流,但这根序列流没有连接任何目标节点(悬空),或者在 XML 层面其 targetRef 错误地指向了网关自身。
当引擎执行到这根"无效"的序列流时,引发了以下连锁反应:
- 条件评估失效 :悬空或自引用的序列流通常没有配置条件表达式。在某些引擎版本的解析逻辑中,没有条件表达式的连线可能会被默认视为
true,或者在缺乏 Default Flow 配置时,引擎的容错机制导致其错误地选中了这条连线。 - 自引用闭环 :日志显示选中的出线是
sid-A31E8830...,而该连线的目标又回到了sid-26F76F2A...(排他网关自身)。 - 缺乏 Wait State(等待状态) :排他网关和序列流都是非等待节点。与 UserTask(用户任务)或 ReceiveTask(接收任务)不同,它们在执行时不会将控制权交还给数据库和调用方,而是在同一个数据库事务和同一个线程内同步执行完毕。
因此,引擎在 网关 -> 悬空/自引用连线 -> 网关 的路径上不断将 ContinueProcessOperation 压入 Agenda 并立即执行,形成了一个没有出口的同步死循环,最终导致线程阻塞、CPU 飙升,甚至引发 StackOverflowError 或事务超时。
三、 解决方案
针对此类问题,处理过程分为紧急止血和根本修复两个阶段。
1. 紧急止血(运维侧)
死循环会持续占用线程池和 CPU 资源,必须立即中断。
-
终止流程实例 :通过 Activiti 的 API 或管理后台,强制删除陷入死循环的流程实例。
java// 删除流程实例,reason 可以写 "死循环强制终止" runtimeService.deleteProcessInstance("3515010", "Infinite loop detected");注意:如果引擎已经因为死循环导致该线程完全卡死,API 调用可能会超时。此时需要重启应用节点,并在重启后立刻清理该实例。
-
调整日志级别 :死循环会产生海量 DEBUG 日志,极易撑爆磁盘。临时将
org.activiti的日志级别调整为INFO或WARN。
2. 根本修复(开发侧)
修改 BPMN 流程定义文件,消除结构缺陷。
-
清理无效序列流 :在流程设计器中删除那根多余的、未连接目标节点的序列流。如果是直接修改 XML,需确保
<sequenceFlow>标签的sourceRef和targetRef指向合法且非自身的节点,或者直接删除该<sequenceFlow>标签。 -
配置默认流(Default Flow) :这是排他网关设计的核心规范。必须为排他网关指定一条
default属性,指向一条兜底的序列流。当所有业务条件都不满足时,引擎会走默认流,从而避免路由失败或走入错误分支。xml<exclusiveGateway id="sid-26F76F2A..." default="defaultFlowId" /> <sequenceFlow id="defaultFlowId" sourceRef="sid-26F76F2A..." targetRef="endEvent_1" /> -
重新部署:修正 XML 后,重新部署流程定义。对于已经运行的旧版流程实例,如果存在悬空线风险,建议通过脚本进行数据迁移或作废处理。
四、 最佳实践
依赖人工检查 BPMN XML 是不靠谱的,尤其是在流程复杂、分支众多的情况下。建议在工程体系中引入以下机制来杜绝此类低级错误:
1. 引入 BPMN 自动化校验
在 CI/CD 流水线或应用启动阶段,增加流程定义的静态校验。
可以使用 bpmnlint 等开源工具,或者在 Activiti/Flowable 中自定义 ProcessValidator。校验规则应至少包含:
- 所有 SequenceFlow 必须具有合法的
sourceRef和targetRef。 - SequenceFlow 的
sourceRef和targetRef不能相同(禁止直接自循环)。 - ExclusiveGateway 和 InclusiveGateway 必须配置
default属性(默认流)。 - 流程中不能存在孤立节点(无入线或无出线的非 Start/End 节点)。
在 Spring Boot 中,可以通过实现 DeploymentConfigurer 或在部署前调用 ProcessValidator 进行拦截:
java
@Autowired
private ProcessValidator processValidator;
public void validateAndDeploy(BpmnModel bpmnModel) {
List<ValidationError> validationErrors = processValidator.validate(bpmnModel);
if (!validationErrors.isEmpty()) {
// 记录错误并抛出异常,阻断部署
throw new IllegalStateException("BPMN 模型校验失败: " + validationErrors);
}
repositoryService.createDeployment().addBpmnModel("process.bpmn20.xml", bpmnModel).deploy();
}
2. 规范流程设计器使用
很多悬空线是由于前端流程设计器(如 bpmn.js)的拖拽误操作产生的,且在 UI 上不易察觉(例如线头没有吸附到节点的锚点上)。
- 培训业务人员和开发人员正确使用建模工具。
- 在前端设计器中增加保存拦截逻辑:在生成 XML 前,遍历所有连线,检查其
targetRef是否为空,若为空则禁止保存并高亮提示。
3. 完善监控与告警
- 线程监控 :配置 APM 工具(如 SkyWalking、Arthas),监控 Tomcat/Undertow 线程池的状态。如果发现某个线程长时间(如超过 5 秒)停留在
ContinueProcessOperation或ExclusiveGatewayActivityBehavior的堆栈上,立即触发告警。 - 日志限流:在 Logback/Log4j2 中配置限流策略,防止单个节点在极短时间内打印成千上万条重复日志,保护磁盘 I/O。