DDD福音:Zeebe是一个类似Kafka的可扩展的分布式事件溯源工作流引擎


许多人认为工作流自动化仅用于人工任务管理等慢速和低频用例,这体现了当前工作流技术在可扩展性方面的局限性,传统工作流引擎基于关系数据库,因此它们自然会受到数据库处理的限制,即使这对大多数公司来说已经足够了,但是肯定有一些有趣的用例需要更高的性能和可扩展性,例如处理需要在非常高的负载下进行软实时保证的金融交易。
Zeebe是一个没有任何中心组件的真正的分布式系统,利用Raft Consensus Algorithm等概念实现可扩展性和弹性。Zeebe使用事件溯源和事件流概念以及复制的仅附加日志,分区允许扩展。根据反应性宣言,它被设计为反应Reactive系统。
Zeebe与Apache Kafka属于同一类系统,每秒同样能够处理相当巨大的事件数量,快于 Camunda 7.8几百倍。
那么怎么能实现这个呢?一个重要的想法是构建一个事件溯源系统。

事件源工作流引擎
传统工作流引擎捕获数据库表中工作流实例的当前状态。如果状态更改,则更新数据库表。使用这种方法,工作流引擎可以利用关系数据库(RDMS)的许多保证,例如ACID事务
Zeebe的工作方式非常不同,并利用事件溯源。这意味着对工作流状态的所有更改都将作为事件捕获,并且这些事件与命令一起存储在事件日志中。两者都被认为是记录在日志中。
DDD爱好者的快速提示:这些事件是在Zeebe内部,与工作流状态有关。如果您在领域中运行自己的事件溯源系统,则通常会为存储自己的域事件
记录是不可变的,因此日志仅附加。一旦写完,就不会有任何改变,就像会计杂志一样。可以非常有效地处理和扩展仅附加日志。
工作流的当前状态始终可以从这些事件中派生。这被称为Project投影或投射。Zeebe中的投影在内部利用RocksDB保存为快照,RocksDB是一个非常快速的键值存储。RocksDB允许Zeebe在内部通过键查找某些对象,因为纯日志甚至不允许简单的查询。
Zeebe 将日志存储在磁盘上。目前,这是唯一受支持的存储选项(其他选项,例如Apache Cassandra会定期讨论,但目前尚未在路线图上讨论)。RocksDB还将快照状态刷新到磁盘,这不仅创建了更快的启动时间,而且还允许Zeebe从日志中删除已处理的记录,使其保持相当紧凑。
为了实现性能,弹性和可伸缩性,我们应用了以下分布式计算概念:


Zeebe架构和用法示例
Zeebe在Java虚拟机(JVM)上作为自己的程序运行。关于运行工作流引擎的体系结构选项,这是远程引擎方法,因为使用Zeebe的应用程序与它远程对话。但是,当我们利用流式传输到客户端并使用二进制通信协议时,这非常有效且高效。它的巨大优势在于代理具有已定义的设置和环境,并且不受应用程序代码的影响。因此,这个设计决策提供了适当的隔离,我们在支持工作流引擎的多年经验中了解到它的重要性。

可视化工作流程
Zeebe使用ISO标准BPMN中的可视化工作流定义,可以使用免费的Zeebe Modeler以图形方式对其进行建模
如果您愿意,也可以使用YAML来描述工作流程,例如:

name: order

tasks:
    - id: retrieve-payment
      type: retrieve-payment-service

    - id: fetch-goods
      type: fetch-goods-service

    - id: ship-goods
      type: ship-goods-service

支持反应式编程,流媒体和背压的本地语言客户端
工作流程可以包括所谓的服务任务。当实例到达这些任务时,您需要执行一些代码。这是通过创建JobWorkers在您的应用程序中提取的作业来完成的。Zeebe提供本地语言客户端,例如Java

ZeebeClient client = ZeebeClient.newClientBuilder().brokerContactPoint("127.0.0.1:26500").build();

JobWorker workerRegistration = client
  .newWorker()
  .jobType(
"my-service-task")
  .handler(new JobHandler() {
    public void handle(JobClient client, ActivatedJob job) {
     
// here: business logic that is executed with every job
      System.out.println(job);
     
// and let the workflow engine know we are done.
     
// The API can be used blocking or non-blocking
      client.newCompleteCommand(job.getKey()).send().join();
    }  
  )
  .timeout(Duration.ofSeconds(10))
  .open();

正如您可能在代码中发现的那样,您可以在应用程序中使用反应式编程模型。
您可以根据需要将尽可能多的客户端连接到Zeebe,并且将分发作业(目前采用循环方式),从而实现工作人员的灵活可扩展性(上下)。Zeebe很快将支持背压,因此确保仅以客户可以处理的速率提供工作。没有客户可以被工作所淹没。如果有疑问,在新客户连接之前,作业将保存在Zeebe中。
客户端是竞争消费者,这意味着一个作业将只由一个客户执行。这是使用锁定事件实现的,需要在执行作业之前将其写入Zeebe。只有一个客户端可以编写该锁定事件,其他尝试这样做的客户端会收到错误消息。锁定在保持一段时间之后被自动删除,因为Zeebe认定客户端在这种情况下已经意外死亡。

事务和至少一次语义
值得注意的是,Zebee客户端不实现任何形式的ACID事务协议。这意味着在发生故障的情况下,不会回滚任何事务。通过此设置,您有两种设计选择:

  1. 您将事务提交到您的域,然后通知Zeebe完成该作业。现在你的应用程序可能会在提交和完成之间崩溃。因此Zeebe不会知道作业已完成并在锁定超时后将其交给另一个客户端。该工作将再次执行。语义是“ 至少一次 ”。
  2. 您首先完成工作,然后提交您的交易。如果应用程序崩溃,您可能已完成作业但未提交事务。工作流程将继续进行。语义是“ 至多一次 ”。


大多数时候你会决定使用“最多一次”,因为它在大多数用例中最有意义。
由于您的代码可能被多次调用,因此您必须使应用程序逻辑具有幂等性。这在您的域中可能很自然,或者您可能会想到其他策略并创建一个幂等接收器(请参阅例如Spring Integration)。我在微服务集成的三个常见陷阱中简要地解决了幂等性- 以及如何避免它们并计划一篇关于它的扩展文章。

通过CQRS查询
Zeebe代理负责执行正在运行的工作流程。它被优化为将新命令应用于当前状态,以达到开头提到的性能和可伸缩性目标。但是,Zeebe代理无法提供任何查询,例如“今天早上在8到9之间启动了哪些工作流实例但尚未完成?”。由于我们不再使用关系数据库,因此无法执行简单的SELECT语句。在这种情况下,我们确实需要一种不同的方式来处理所谓的查询模型。
这种分离命令和查询模型的方式称为命令查询责任分离(CQRS),具有很大的优势:
CQRS允许您将负载与读取和写入分开,允许您独立地进行扩展。[...]您可以向双方应用不同的优化策略。一个例子是使用不同的数据库访问技术进行读取和更新。
这正是我们对Zeebe所做的。Broker利用事件流和优化高吞吐量和低延迟。但它不提供查询功能。这就是为什么Zeebe提供所谓的Exporters,它可以访问整个事件流。一个开箱即用的Exporters是Elasticsearch。通过使用它,所有事件都写入Elastic并存储在那里,随时可以供您查询。

Zeebe现在提供了一个操作工具,您可以使用它来查看工作流引擎:Operate.。您可以看到正在发生的事情,识别问题(所谓的事件)以及根源和修复事件
Operate也可以扩展并在Elasticsearch上使用自己的优化索引:

源代码可用的许可证
您可以下载,修改和重新分发Zeebe代码。您可以将Zeebe包含在商业产品和服务中。只要您不提供通用工作流程服务,类似云提供商那样。

总结
Zeebe被设计为一个真正可扩展且具有弹性的系统,没有中央数据库。它非常高效。它可以与几乎任何编程语言一起使用。它使用BPMN中的可视化工作流,允许真正的BizDevOps。这种组合使它与我所知道的任何编排或工作流引擎区别开来。