ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Camunda 8云原生工作流引擎实战:从核心概念到流程编排

Camunda 8云原生工作流引擎实战:从核心概念到流程编排

工作流引擎这个技术方向,说实话挺容易被低估的。新人觉得不就是画个流程图,跑到对应节点就触发一段代码;熟手又会抱怨框架太重、和业务绑定太深。但真正做过生产级流程编排的人都明白,把“流程”这件事单独抽出来做成引擎,远比自己维护一套状态机踏实。Camunda 8 就是当前云原生工作流引擎里非常值得花时间吃透的一个,它把流程定义、任务调度、人工审批、业务监控统一到一个平台上,这也是我这次想好好梳理一遍的东西。

如果你要处理的是订单流转、审批流、风控流水线、工单分派这类场景,或者你正在做一个需要把 SOP 落地成代码的中后台系统,Camunda 8 能帮你把“流程怎么走”和“每一步怎么做”彻底分离开来。这篇内容不是官方文档的逐一翻译,而是从选型、核心概念到跑通一个真实流程的完整拆解,适合刚接触工作流引擎、以及需要从 Camunda 7 往 8 迁移的读者。

1. 为什么是 Camunda 8:先搞懂它和 Camunda 7 的区别

1.1 Camunda 7 的老路子

很多人第一次接触 Camunda,其实是 Camunda 7。它的架构很直观:一个进程内的流程引擎,可以嵌入到你的 Spring Boot、Java 应用、或者作为独立容器跑。流程的状态、待办任务、历史数据存放在同一个关系型数据库里,依靠数据库事务保证 ACID。当流程步骤多、实例量大之后,乐观锁冲突、数据库连接池耗尽这类问题就会开始出现,因为所有引擎实例都对同一套数据表做读写。

Camunda 7 本身并不难理解,难的是水平扩展。你虽然可以部署多台节点,但这需要额外的路由策略和共享数据库配置,本质上还是“单体应用 + 数据库”的思路。对外提供的 REST API 和嵌入式 Java API 也确实好用,但在云原生环境下,人们希望引擎可以按服务粒度分散,不再被一个中心数据库卡住脖子。

1.2 Camunda 8 到底改了什么

Camunda 8 的设计思路是“把引擎拆成微服务”。核心引擎变成了一个叫 Zeebe 的分布式流程引擎,底层不用关系型数据库存状态,而是用事件流的方式记录变更,通过分区(Partition)把压力分散到多个节点上。每个流程实例被分配给某个分区,由那个分区负责推进,其他节点可以水平扩容,一起分担负载。

这意味着,它不再像 Camunda 7 那样强依赖数据库行锁和事务,而是采用事件溯源和最终一致性的思路。命令会先被验证并写入日志,再通过内部处理器更新内存状态,最后异步导出到 Elasticsearch,供监控和历史查询使用。初次接触会觉得多了一层、变复杂了,但换来的是更明确的扩展边界:引擎节点负责推进流程,IO 密集的查询和可视化监控交给另一批组件。

另外,Zeebe 对外提供的客户端 SDK 变成了“Job Worker”模式。外部系统不是直接调用引擎接口去跑下一步,而是引擎执行到服务任务时,把该做的活儿生成一个 Job,等着外部 Worker 来领取和处理。这种模式大幅降低了引擎和业务代码之间的强耦合,也是和 Camunda 7 最直观的差异之一。

1.3 什么时候该选 8,什么时候该选 7

选型是个很现实的问题。我自己的判断标准有三个:

第一,项目是否要上容器化和 Kubernetes。如果团队已经以 K8s 为部署基座,那毫无疑问选 Camunda 8,它的分布式设计、HPA 自动扩容、组件分拆部署都和容器化天然契合。第二,是否有明确的高吞吐场景。比如每天的流程实例数以万计、流程步骤频繁更新,这时 Camunda 8 的分区设计能让集群弹性扩展,8 的收益更明显。第三,团队是否愿意改变使用习惯。从嵌入式 API 切换到 job worker 模式需要一点适应成本,但反过来也让代码和流程定义之间有一条更清晰的边界。

如果项目非常小、只有几个流程,而且团队希望把引擎能力以库的形式直接嵌进业务服务里,那 Camunda 7 反而更轻量。它是成熟的、文档丰富的老牌引擎,对单机业务系统依然足够。不用因为在“追新”而去强行引入分布式架构,工具永远是服务于场景的。

2. 从宏观到微观:Camunda 8 的组件全景

2.1 Zeebe:核心流程引擎

Zeebe 是整个平台里最核心的组件,负责流程状态的计算、部署、执行和事件发布。它能跑 BPMN 2.0 流程图,也能跑一部分 DMN 决策需求。流程推进过程中产生的所有事件经过分区处理后会写入内部日志,再通过 Exporter 机制推送到外部存储,比如 Elasticsearch。这里要注意,Zeebe 自身并不负责长期保存历史数据,它更像个执行中枢,状态算出来,结果送出去,剩下的交给下游组件。

Zeebe 正常工作需要三个维度配合:网关节点、分区、副本。Gateway 负责客户端接入和命令分发,比如一个 Worker 提交任务完成指令时,请求会先到 Gateway,由它找到负责对应分区的 Leader 节点来处理。多副本设计保证了节点宕机时分区领导权能自动转移。对于生产环境部署,至少两个节点、每个分区两个副本是常规起步配置。

2.2 Operate:流程可视化监控

Operate 负责把流程执行的过程可视化出来。你可以在界面上看到一个流程实例当前跑到哪个节点、每个节点的状态如何、有没有卡住的实例,还能直接查看流程实例的变量内容和异常信息。实际排障时,我通常先开 Operate,按时间范围筛选出失败或取消的实例,再点进去看具体卡在某一步。

它是基于 Elasticsearch 做查询的,因为 Zeebe 已经将流程事件导入了 ES。所以你在部署 Camunda 8 时基本都会看到 Elasticsearch 一同出现,它不是可选项,而是 Operate 和后面提到的组件都依赖的数据底座。Operate 本身不发起流程,不改写数据,它以只读视角辅助排查和观察,这能避免监管操作对生产流程产生二次影响。

2.3 Tasklist:人工任务分派台

Tasklist 是给人用的。它负责管理流程走到用户任务(User Task)时产生的人工地步,比如审批、复核、信息补充。业务人员通过 Tasklist 的界面领取任务、完成任务、填写必要字段。后端开发则需要关注 Tasklist 的 API,以及如何把任务所有权分配给指定组或用户,如何把已完成任务的数据写回流程变量。

这里需要特别注意:Tasklist 和 Zeebe 之间是通过 job 机制连接的,用户任务本质上也会被转成一个 job,只不过这个 job 由 Tasklist 这个“Worker”来领取。所以它在集群中是类似旁路系统的角色,这一点和 Camunda 7 里任务表直接在流程引擎内部不太一样。你在做权限配置时,要从“接入方”的角度考虑 Tasklist 与 Zeebe 之间的网络连通性。

2.4 Optimize:性能分析与瓶颈定位

Optimize 是 Camunda 8 的商业组件,专门承担流程数据分析职责。它会从 Elasticsearch 拉取历史数据,按时间维度、流程维度分析每个节点的耗时、成本、频次,生成报表和热力图。很多团队会忽视这个组件,但当你接入了几十个流程后,最想知道的往往不是“实例有没有跑”,而是“瓶颈到底在哪一步”。

例如订单流程中,支付回调节点平均耗时 200 毫秒,但人工审核节点平均耗时 2 天,那么优化重点显然在审核环节。Optimize 能通过历史数据直接给出这些数字,帮团队找到优化方向。没有它也能做,只是需要自己写查询脚本去 Elasticsearch 里统计,效率和直观程度都会差一些。

2.5 Web Modeler:协作建模入口

Web Modeler 是在线 BPMN 编辑器,解决的是建模协作问题。开发和业务人员可以直接在浏览器里画流程图、配置任务类型、设置条件表达式,然后一键部署到 Zeebe。新版还支持多分支版本管理和协作评审,比原来只能靠本地建模工具或用 Camunda 7 的插件画图要顺畅很多。

建模时经常有人忽略流程的版本管理。Web Modeler 里保存的版本和部署到 Zeebe 后的版本是一一对应的,所以当流程图修改后再次部署,新流程实例会使用新定义,而已经在跑的实例会继续基于旧版本推进,这是非常关键的行为。如果理解错了这一点,生产上很可能出现“我改了流程但没生效”的误解。

3. 必须吃透的七个核心概念

3.1 流程定义与流程实例

流程定义就是那张 BPMN 图,部署后成为某个引擎内的可执行模型。每次部署会生成一个版本号,Zeebe 默认使用最新版本处理新发起的流程实例。流程实例则是这一张图被真正执行的一次运行记录,相当于“类”和“对象”的关系。

从数据结构上看,一次流程实例包含当前所在节点、已流转的路径、流程变量、错误状态等。实例之间彼此隔离,变量不能直接互通,除非通过消息事件等方式显式关联。排查问题时我经常强调先确认“是哪个定义的哪个版本”,因为同一个 key 在不同版本里可能代表完全不同的节点顺序。

3.2 Job 与 Worker

在 Camunda 8 里,服务任务对应的不是直接的方法调用,而是产生一个待领取的 Job。Worker 是外部进程或服务,它订阅某个 Job Type,收到任务后执行业务逻辑,然后向 Zeebe 返回 complete 或 fail。这个设计把流程引擎和业务代码拆开了:引擎不关心你的处理逻辑在哪个服务里,只负责把任务发出去、接收完成指令、推进后续节点。

一个常见误区是觉得“一个流程里所有服务节点都要一个 Worker”。其实每个服务任务只要指定了同一个 type,就可以由同一个 Worker 统一处理,Worker 内部也可以用变量和分支决定做什么。Worker 的并发数、超时时间、重试次数都需要根据实际处理能力配置。如果 Worker 崩溃或不响应,Job 会等到超时后重新被其他 Worker 领取,但这种机制也意味着业务逻辑必须天然具备幂等性。

3.3 消息事件与消息关联

消息事件用来处理流程和外部世界之间的异步交互。比如订单流程等待支付回调、等待外部系统推送结果,都可以通过中间捕获消息事件来实现。触发消息时,核心不是简单发送数据,而是告诉 Zeebe“我希望哪一类的消息,关联到哪个流程实例”。

消息关联需要两个关键信息:消息名称(Message Name)和关联键(Correlation Key)。你发送的消息里会被提取一个变量,和流程定义中关联键表达式的结果做匹配,只有匹配上实例的消息才会被真正消费。实际使用中,关联键通常就是订单号、业务主键这类唯一标识。消息还有 TTL,超时没被关联消费就会过期,所以发送消息时要把关联键和 TTL 当作一等公民设计,而不是事后补救的参数。

3.4 定时器与时间控制

定时器事件让流程有能力等待一段时间或在某个时间点触发。比如订单超时未支付,就启动一个定时器事件,到时间后流转到关闭订单节点。Camunda 8 的定时器依赖引擎集群里的时间机制,它以一定精度检测到期,不是数据库秒级精确调度,所以不要拿着秒级的强一致预期去使用它。

我曾经在测试环境验证过,一个设置为 5 秒的定时器,实际触发可能在 6 到 8 秒之间。更关键的是,当环境暂停或集群切换后,时间恢复机制是否能把过期定时器补上,这取决于配置和分区状态。线上项目如果强依赖定时精度,建议把业务上的宽限期也设计进去,并在定时器到达后再次校验外部状态。

3.5 流程变量

流程变量是附着在流程实例上的数据,负责在节点之间传递上下文。比如订单金额从创建节点算出来,传给审批节点使用;审批结果的布尔值再传给网关,决定走通过分支还是驳回分支。变量在 Zeebe 里以 JSON 形态存储,支持多种类型,使用条件表达式时要注意类型匹配。

一个容易踩的坑是变量值过大。有些团队习惯把第三方接口返回的完整 JSON 塞进流程变量,结果每个节点都会带着这包数据流转,既浪费存储,也拖慢性能。更合理的做法是只保存关键字段或引用地址,需要详情时再由 Worker 回查数据源。

3.6 网关:排他、并行与包容

网关解决了分支和合并的问题。排他网关根据条件表达式选一条分支,条件从上到下按顺序判断,第一个为 true 的生效。并行网关会把流程拆成多条同步执行的分支,并且必须等待所有分支都到达合并网关后才继续向下,这是 BPMN 里常用但容易造成“死等”的设计点。

包容网关是排他和并行的结合体,它允许每条条件为 true 的分支执行,并且合并时等待所有被激活的分支完成。用错网关会出现很隐蔽的问题:并行分支中有一条抛异常未处理,其他分支已经完成,整个实例可能一直悬在那里不前进,靠操作监控才能发现。画流程时建议每个并行网关都配好边界事件或补偿机制。

3.7 错误处理与 BPMN Error

真实流程不可能每一步都成功,所以错误处理是 BPMN 里不能跳过的部分。Camunda 8 支持 BPMN Error,流程节点中可抛出业务错误,并由边界错误事件捕获,从而转向异常处理分支。特别注意:这里说的是业务意义上的错误,例如支付超时、库存不足、审批被拒,不是代码抛异常那么简单。

设计错误边界事件时,要看它挂在哪个节点上、错误码是什么、抛出端是否匹配。错误码是一个字符串,必须两端一致才能在运行时被捕获。烘焙到流程图上的错误路径也要尽量避免过宽,否则异常分支会吞掉太多正常逻辑,导致流程图失去可读性。

4. 实操:从建模到运行一条流程

4.1 在 Web Modeler 画一条订单审批流程

实际画流程时,我会从一个纯流程的角度先定义节点:开始事件、一个接收订单信息的服务任务、一个判断金额的用户审批任务、一个排他网关、两条分支,以及结束事件。服务任务需要指定 Job Type,比如“create-order”,审批用户任务则分配给一个用户或权限组。

建模时的几个小建议:节点名称尽量用业务语言而不是代码语言,这样业务同事也能看明白;网关条件的表达式写成amount >= 1000这类可读性强的形式;关键节点加上文档说明,方便后续维护。如果公司不让用 SaaS 或没有 Web Modeler 授权,也可以直接用本地的 BPMN 建模工具,画好后保存为 bpmn 文件,后续部署时交给 Zeebe 来解析。

4.2 用 Docker Compose 搭一套本地环境

本地开发时,用 Docker Compose 起一套齐全的环境是最快的办法。最完整的一套至少要包含 Zeebe、Elasticsearch、Operate、Tasklist。我常用类似下面这种精简拓扑:

services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.30 environment: - discovery.type=single-node - xpack.security.enabled=false ports: - "9200:9200" zeebe: image: camunda/zeebe:8.5.0 ports: - "26500:26500" - "9600:9600" environment: - ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_CLASSNAME=io.camunda:zeebe-exporter-elasticsearch:8.5.0 - ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_URL=http://elasticsearch:9200 depends_on: - elasticsearch operate: image: camunda/operate:8.5.0 ports: - "8081:8080" environment: - CAMUNDA_OPERATE_ZEEBE_GATEWAYADDRESS=zeebe:26500 - CAMUNDA_OPERATE_ELASTICSEARCH_URL=http://elasticsearch:9200 - CAMUNDA_OPERATE_ZEEBEELASTICSEARCH_URL=http://elasticsearch:9200 depends_on: - elasticsearch - zeebe tasklist: image: camunda/tasklist:8.5.0 ports: - "8082:8080" environment: - CAMUNDA_TASKLIST_ZEEBE_GATEWAYADDRESS=zeebe:26500 - CAMUNDA_TASKLIST_ELASTICSEARCH_URL=http://elasticsearch:9200 - CAMUNDA_TASKLIST_ZEEBEELASTICSEARCH_URL=http://elasticsearch:9200 depends_on: - elasticsearch - zeebe

这套环境启动后,Zeebe 的运行指标接口在 9600 端口,客户端接入口在 26500 端口。Operate 和 Tasklist 的 Web 界面分别在 8081 和 8082。这里有个现实问题:整套镜像的内存占用可能超过 4GB,尤其是 Elasticsearch 对内存占用比较敏感。开发机内存不足时,建议给 Docker 分配至少 6GB 可用内存,并且不要同时启动不需要的组件。

4.3 写一个 Worker 并注册

以 Java 客户端为例,创建一个订阅create-order类型的 Worker,核心逻辑是从 Job 的变量里取出订单数据,处理后调用业务服务,再通过newCompleteCommand().variables()把结果写回去。

ZeebeClient client = ZeebeClient.newClientBuilder() .gatewayAddress("127.0.0.1:26500") .build(); client.newWorker() .jobType("create-order") .handler((jobClient, job) -> { Map<String, Object> variables = job.getVariablesAsMap(); String orderId = String.valueOf(variables.get("orderId")); // 调用业务服务创建订单 Map<String, Object> result = new HashMap<>(); result.put("orderStatus", "CREATED"); result.put("createTime", System.currentTimeMillis()); jobClient.newCompleteCommand(job.getKey()) .variables(result) .send() .join(); }) .name("order-worker") .open();

Worker 启动后会长期运行,靠长连接从 Zeebe 拉取任务。这里的设计要点是:Worker 本身不需要声明“订阅某个流程”,只需要订阅某个 Job Type,所以同一个 Worker 可以被多个流程复用。错误的异常处理不要用complete返回错误状态,而应该使用newFailCommand或抛出 BPMN Error,确保外部系统能感知到失败并走重试逻辑。

4.4 部署并启动第一个流程实例

部署 BPMN 文件可以直接用客户端 API。把建好的order-process.bpmn放在资源目录下,然后执行部署命令:

DeploymentEvent deployment = client.newDeployCommand() .addResourceFromClasspath("order-process.bpmn") .send() .join(); long processDefinitionKey = deployment.getProcessDefinitionKey();

随后发起一个流程实例,只需要提供流程定义 key 或 bpmnProcessId,再加上初始变量:

ProcessInstanceEvent instance = client.newCreateInstanceCommand() .bpmnProcessId("order-process") .latestVersion() .variables(Map.of( "orderId", "ORDER-10001", "amount", 1500 )) .send() .join();

启动后,流程会在第一个服务任务处产生一个 Job。如果此时你的 Worker 没启动,Operate 界面上会看到这个实例一直停留在该节点。把 Worker 跑起来后,任务被领取并完成,流程继续推进到用户任务节点,Tasklist 里会出现一条待办任务。整个流程跑通的过程,也是把概念转换为实际操作的最有效路径。

5. 常见问题与排查技巧

5.1 Worker 接收不到任务

最常见的原因是 Job Type 对不上。检查 BPMN 服务任务里配置的 type 是否和 Worker 订阅的 type 完全一致,包括大小写和空格。其次是网关地址连不上,Worker 的 gatewayAddress 必须指向 Zeebe 的 Gateway 地址,不是 broker 的端口,更不是 Operate 的端口。

如果确认地址和 type 没问题,再看 Worker 是否设置了 fetchTimeout。默认超时时间如果太短,而业务处理又很慢,可能导致任务被重复领取和重复处理。我会把 fetchTimeout 设置在 30 秒以上,并把 Worker 并发数调到和下游系统处理能力匹配的数量。

5.2 消息关联失败

关联失败通常表现为:消息发送成功,但流程实例没有任何响应。排查时先确认消息名称是否正确,再看关联键表达式的值是否和消息变量一致。这里有个很容易被忽视的点:关联键表达式是写在流程定义里的,比如=orderId,它取自流程变量;而你在发送消息时携带的变量名也要叫orderId,如果发送端传的是order_id,就会匹配不上。

关联失败后消息并不会永远留在系统里,它有一个存活时间。这个时间由 Zeebe 配置决定,默认值在实际调试中显得偏短,尤其当上游和下游之间存在较长异步链路时。我的建议是把 TTL 设置为业务可以接受的最长等待时间,宁长勿短,因为一次超时丢失后,想再精确触发同一个实例会非常痛苦。

5.3 变量类型导致条件分支判断异常

网关表达式看似简单,但类型坑很常见。如果流程变量里amount是从接口返回的字符串型数字,而网关表达式写的amount > 1000,运行时会因为类型不一致而判断失败或走默认分支。因此创建流程实例或 Worker 写回变量时,要确保数字类型保持为数字,而不是 JSON 里的字符串。

排查这类问题最好的工具就是 Operate。点开实例详情,查看变量面板中的实际类型和值,一眼就能看出是不是类型错配。养成写网关条件时顺手在表达式中用内置函数做显式转换的习惯,可以省去很多半夜排查的麻烦。

5.4 本地环境资源占用过高

一套完整的 Camunda 8 本地环境包含 Zeebe、Elasticsearch、Operate、Tasklist,整体内存占用很容易超过 4GB。有些人起完环境电脑直接卡死,不是组件有 bug,而是内存没限制住。可以给每个 JVM 容器设置显式内存上限,并限制 Elasticsearch 的堆内存大小。

另一个技巧是,日常只启动 Zeebe 和 Elasticsearch,不启动 Operate 和 Tasklist。调试客户端、Worker 和流程推进时,这两个组件完全够用。只有需要排查任务、查看历史数据时再按需启动 Operate。这样整体资源占用会下降一个档次,开发体验也更清爽。

5.5 定时器和时间恢复问题

流程里用到定时器时,要清楚 Camunda 8 对时间的处理是基于内部时钟。如果你在测试环境中为了验证一个 1 小时定时器而手动改操作系统时间,这通常不会生效,因为 Zeebe 集群节点可能从多个渠道感知时间,直接改节点本地时间很容易造成状态不一致。

更科学的做法是利用 Zeebe 的时钟控制 API,在测试中注入可控时间。这只是测试环境能力,生产环境还是要依赖集群内部时间同步。多个节点之间存在一定时钟偏差时,定时器不一定会在精确时刻触发,这个误差范围需要被业务容忍,否则就要考虑换成消息事件等外部触发方式。

6. 实操体会:从概念到生产的一些建议

概念真正能落地,靠的是一遍遍调试和踩坑。我自己最大的体会是,不要试图一下子把全部组件都接入生产,最好先从最小的闭环开始:一个流程、一个 Worker、一个真实业务场景,跑通后再逐步加组件、加分支、加事件。这样不仅问题容易定位,也更容易让团队里的其他人理解这套体系带来的价值。

另外一个很实用的建议是,维护一个“流程定义版本变更记录”。因为 Zeebe 里新部署的流程定义会立即成为新实例使用的最新版本,但存量实例继续按旧版本执行。没有版本记录,大量新老版本并存时你很难说清楚某个异常实例到底是哪个图形产生的。这个记录不需要多复杂,一张包含时间、部署人、变更点、版本号的表格就足够。

最后想提醒的是,工作流引擎不是银弹,它解决的是流程编排和状态管理的复杂度,不解决业务本身的可靠性。Camunda 8 的所有事件、任务、变量处理,最终还是要落到业务代码的质量上。把 Worker 的幂等性、消息关联的 key、变量的大小都设计好,再用这个引擎去承载业务,才会真正省心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进