如何使用Zebee构建高度可扩展的分布式工作流中间件?


Zeebe是一种全新的工作流/编排引擎,适用于云原生和云规模应用。本文介绍如何使用Zebee进入云规模的工作流程自动化的新时代。Zeebe是一个真正的分布式系统,没有任何中心组件,根据一流的分布式计算概念设计,符合反应性宣言,应用高性能计算技术。

事件溯源
Zeebe基于事件采购的想法。这意味着对工作流状态的所有更改都将捕获为事件,并且这些事件将与命令一起存储在事件日志中。两者都被认为是记录在日志中。DDD爱好者的快速提示:这些事件是Zeebe内部的,与工作流状态有关。如果您在域中运行自己的事件源系统,则通常会为域事件设置运行你们自己的事件存储。
事件是不可变的,因此此事件日志仅附加。一旦写完就没有任何改变 - 就像会计杂志一样。仅附加日志相对容易处理,因为:

  • 由于没有更新,您不能并行存在多个冲突的更新。对状态的冲突更改始终以清晰的顺序捕获为两个不可变事件,以便事件源应用程序可以确定如何确定地解决该冲突。在RDMS中实现计数器:如果多个节点并行更新相同的数据,则更新会相互覆盖。必须认识到并避免这种情况。典型的策略是乐观或悲观锁定与数据库的ACID保证相结合。仅附加日志不需要这样做。
  • 已知的策略是复制仅附加日志。
  • 保持这些日志是非常有效的,因为你总是提前写。如果您执行顺序写入而不是随机写入,则硬盘的性能会更好。

工作流的当前状态始终可以从这些事件中派生。这被称为投影。Zeebe中的投影在内部保存为利用RocksDB的快照,RocksDB是一个非常快速的键值存储。这也允许Zeebe只能通过key获取数据。纯日志甚至不允许简单的查询,例如“给我工作流实例2的当前状态”。

记录压缩
随着日志的增长,您必须考虑从中删除旧数据,这称为日志压缩。例如,在理想的世界中,我们可以删除所有已结束的工作流实例的事件。不幸的是,这非常复杂,因为来自单个工作流实例的事件可能遍布整个地方 - 特别是如果您记住工作流实例可以运行数天甚至数月。我们的实验清楚地表明,进行日志压缩不仅效率低下,而且结果日志变得非常分散。
我们决定以不同的方式做事。一旦我们完全处理了一个事件并将其应用于快照,我们立即将其删除。我稍后会回来“完全处理”。这使我们可以始终保持日志干净整洁,而不会失去仅附加日志和流处理的好处 。

存储
Zeebe 将日志写入磁盘,RocksDB也将其状态刷新到磁盘。目前,这是唯一受支持的选项。我们经常讨论使存储逻辑可插拔 - 例如支持Cassandra - 但到目前为止我们专注于文件系统,它甚至可能是大多数用例的最佳选择,因为它只是最快和最可靠的选择。

单写原则
当您有多个客户端同时访问一个工作流实例时,您需要进行某种冲突检测和解决。当您使用RDMS时,通常通过乐观锁定或某些数据库魔法来实现。使用Zeebe,我们使用Single Writer Principle解决了这个问题。正如Martin Thompson所写
争用可变状态的访问需要互斥或有条件的更新保护。这些保护机制中的任何一个都会导致在应用竞争更新时形成队列。为了避免这种争用和相关的排队效应,所有状态都应由单个编写者拥有,以便进行突变,从而遵循单一编写者原则
因此,我们机器上的线程数与Zeebe集群的总体大小无关,始终只有一个线程可以写入某个日志。这很好:排序很明确,不需要锁定,也不会发生死锁。您不会浪费时间管理争用,但可以随时进行实际工作。
如果你想知道这是否意味着Zeebe只利用一个线程来完成工作流逻辑,那么到目前为止你是对的!我将在稍后讨论缩放Zeebe的问题。

事件处理循环
为了更好地理解单个线程正在做什么,让我们看看如果客户端想要在工作流中完成任务会发生什么:

zeebe.newCompleteCommand(someTaskId).send()

  1. 客户端将命令发送给Zeebe,这是一个非阻塞调用,但如果您愿意,可以在以后获得一个Future来接收响应。
  2. Zeebe将命令附加到其日志中。
  3. 日志存储在磁盘上(并复制 - 我稍后解决)。
  4. Zeebe会检查一些不变量(“我现在可以真正处理此命令吗?”),更改快照并创建要写入日志的新事件。
  5. 检查不变量后,即使新事件尚未写入日志,也会立即发送对客户端的响应。这是安全的,因为即使系统现在崩溃,我们总是可以重放命令并再次获得完全相同的结果。
  6. 结果事件将附加到事件日志中。
  7. 日志存储在磁盘上并进行复制。

如果你深入了解事务思路,你可能会问一个问题:“很好 - 但是如果我们改变RocksDB状态(步骤4)并且在我们将事件写入日志之前系统崩溃(步骤6和7)会怎么样?”很好的问题!Zeebe仅在处理完所有事件后验证快照。在任何其他情况下,使用较旧的快照并重新处理事件。

流处理和导出器
我之前谈的是事件采购/溯源。实际上,有一个相关的概念很重要:流处理。由事件(或准确的记录)组成的仅附加日志是一个固定的事件流。Zeebe内部基于处理器的概念,每个处理器都是一个线程(如上所述)。最重要的处理器实际上是实现BPMN工作流引擎部分,因此它在语义上理解命令和事件,并知道下一步该做什么。它还负责拒绝无效命令。
但是有更多的流处理器,最重要的是Exporter,这些导出器还处理流的每个事件。一个开箱即用的导出器正在将所有数据写入Elasticsearch,它可以保留在未来并进行查询。例如,Zeebe操作工具Operate正在利用此数据来可视化运行的工作流实例,事件等的状态。
每个导出器都知道它读取数据的日志位置。只要所有流处理器成功处理完数据,就会删除数据,如上面的日志压缩中所述。这里的权衡是,您不能在以后添加新的流处理器,并让它从历史记录中重放所有事件,就像在Apache Kafka中一样。

点对点集群
为了提供容错和弹性,您可以运行多个Zeebe代理,这些代理形成一个对等集群。我们以不需要任何中央组件或协调器的方式设计它,因此没有单点故障。
要形成群集,您需要将至少一个其他代理配置为代理中的已知联系点。在启动代理期间,它会与此其他代理进行通信并获取当前的群集拓扑。之后,使用Gossip协议使群集视图保持最新和同步。

使用Raft Consensus算法进行复制
现在必须将事件日志复制到网络中的其他节点。Zeebe使用分布式共识 - 更具体地说是Raft共识算法 - 来复制经纪人之间的事件日志。Atomix作为其实现。
基本思想是有一个领导者和一组粉丝。当Zeeber启动时,他们将选出一位领导者。随着集群不断来回发送消息,经纪人会认识到领导者是否已经垮台或断开连接并试图选出新的领导者。只允许领导者对数据进行写访问。领导者写的数据被复制给所有粉丝。只有在成功复制之后,才会在Zeebe代理中处理事件(或命令)。如果您熟悉CAP定理,则意味着我们决定了一致性而不是可用性,因此Zeebe是一个CP系统。(我向Martin Kleppmann道歉,他写道,请停止调用数据库CP或AP,但我认为这有助于理解Zeebe的架构)。
我们容忍对网络进行分区,因为我们必须容忍每个分布式系统中的分区,你根本不会对此产生影响(请参阅http://blog.cloudera.com/blog/2010/04/cap-confusion-problems-with- partition-tolerance /https://aphyr.com/posts/293-jepsen-kafka)。我们决定一致性而不是可用性,因为一致性是工作流自动化用例的承诺之一。
一个重要的配置选项是复制组大小。为了选举领导者或成功复制数据,您需要一个所谓的法定人数,这意味着对其他Raft成员的一定数量的确认。因为我们要保证一致性,所以Zeebe要求法定人数≥(复制组大小/ 2)+ 1.让我们举一个简单的例子:

  • Zeebe节点:5
  • 复制组大小:5
  • 法定人数:3

因此,如果有3个节点可访问,我们仍然可以工作。一个网段内必须达到法定数量才能继续工作,如果只有两个节点活着则无法执行任何操作,如果有访问这网段中的两个节点的客户端,则它无法继续访问,因为CP系统无法保证可用性。
这避免了所谓的裂脑现象,因为你不能最终得到两个并行完成冲突工作的网段。

复制
当领导者写入日志条目时,他们首先会被复制到关注者,然后才能执行。
这意味着可以保证正确复制每个被处理的日志条目。并且复制可确保不会丢失任何已提交的日志条目。较大的复制组大小允许更高的容错能力,但会增加网络上的流量。由于对多个节点的复制是并行完成的,因此实际上可能不会对延迟产生很大影响。此外,代理本身不会被复制阻止,因为这可以被有效地处理
复制也是克服虚拟化和容器化环境中写入磁盘的挑战的策略。因为在这些环境中,当数据实际物理写入磁盘时,您无法控制。即使您调用fsync并且它告诉您数据是安全的,也可能不是。但我们更喜欢将数据存储在几个服务器的内存中,而不是放在其中一个服务器的磁盘上。
虽然复制可能会增加Zeebe中命令处理的延迟,但它不会对吞吐量产生太大影响。Zeebe中的流处理器不会堵塞地等待跟随者的回复。所以Zeebe可以继续快速处理 - 但等待他的响应的客户可能需要等待一段时间。

网关
要启动新的工作流实例或完成任务,您需要与Zeebe交谈。最简单的方法是利用其中一个现成的语言客户端,例如JavaNodeJsC#GoRustRuby。并且由于gRPC,几乎可以使用任何编程语言。
客户端与Zeebe网关通信,后者知道Zeebe代理群集拓扑并将请求路由到该请求的正确领导者。这种设计使Zeebe在云端或Kubernetes中运行变得非常容易,因为只需要从外部访问网关。

通过分区扩展
到目前为止,我们讨论过只有一个线程处理所有工作。如果要利用多个线程,则必须创建分区。每个分区代表一个单独的物理追加日志。
每个分区都有自己的单写者,这意味着您可以使用分区进行扩展。可以分配分区

  • 单个机器上的不同线程或
  • 不同的代理节点。

每个分区都形成一个自己的Raft组,因此每个分区都有自己的领导者。如果运行Zeebe集群,则一个节点可以是一个分区的领导者,也可以是其他分区的跟随者。这可能是运行群集的一种非常有效的方法。
与一个工作流实例相关的所有事件必须进入同一个分区,否则我们会违反单写原则,也无法在本地重新创建代理节点中的当前状态。
一个挑战是如何确定哪个工作流实例进入哪个分区。目前,这是一种简单的循环机制。启动工作流实例时,网关会将其放入一个分区。分区ID甚至可以获得工作流实例ID的一部分,这使得系统的每个部分都可以非常轻松地了解每个工作流实例所在的分区。
一个有趣的用例是消息关联。工作流实例可能会等待消息(或事件)到达。通常,该消息不知道工作流实例ID,但与其他信息相关,假设为order-id。因此,Zeebe需要查明是否有任何工作流实例正在等待具有该order-id的消息。如何有效和横向扩展?
Zeebe只是创建一个消息订阅,它位于一个可能与工作流实例不同的分区上。该分区由相关标识符上的散列函数确定,因此可以通过递交消息的客户端或者到达其需要等待该消息的点的工作流实例来容易地找到。这种情况发生在哪个(参见消息缓冲)并不重要,因为单写者不会发生冲突。消息订阅始终链接回等待的工作流实例 - 可能生活在另一个分区上。
请注意,当前Zeebe版本中的分区数是静态的。一旦代理群集投入生产,就无法更改它。虽然这在Zeebe的未来版本中可能会有所改变,但从一开始就为您的用例规划合理数量的分区绝对非常重要。有一个生产指南可以帮助您做出核心决策。

多数据中心复制
用户经常要求进行多数据中心复制。目前尚无特别支持(尚未)。Zeebe集群在技术上可以跨越多个数据中心,但您必须为增加的延迟做好准备。如果以一种方式设置群集,只有来自两个数据中心的节点才能达到仲裁,即使是史诗般的灾难,也会以延迟为代价。

为什么不利用Kafka或Zookeeper呢?
很多人都在问我们为什么要自己编写上述所有内容,而不是简单地利用Apache Zookeeper之类的集群管理器,甚至不是完全成熟的Apache Kafka。以下是此决定的主要原因:

  • 易于使用和易于入门。我们希望避免在使用Zeebe之前需要安装和操作的第三方依赖项。Apache Zookeeper或Apache Kafka不易操作。
  • 效率。在核心代理中进行集群管理允许我们针对具体的用例(即工作流自动化)对其进行优化。如果围绕现有的通用集群管理器构建,那么一些功能将更难。
  • 支持和控制。在我们作为开源供应商的长期经验中,我们了解到在这个核心级别支持第三方依赖很难。当然,我们可以开始聘请核心Zookeeper贡献者,但由于参与者有多方,因此仍然很难,所以这些项目的方向不在我们自己的控制之下。通过Zeebe,我们投资控制整个堆栈,使我们能够全速前进到我们想象的方向。

性能设计
除了可扩展性之外,Zeebe还可以在单​​个节点上实现高性能。
因此,例如我们总是努力减少垃圾。Zeebe是用Java编写的。Java有所谓的垃圾收集,无法关闭。垃圾收集器会定期启动并检查可以从内存中删除的对象。在垃圾回收期间,系统会暂停 - 持续时间取决于检查或删除的对象数量。此暂停可能会给您的处理增加明显的延迟,尤其是如果您每秒处理数百万条消息。所以Zeebe的编程方式是减少垃圾。
另一种策略是使用环形缓冲区并尽可能利用批处理语句。这也允许您使用多个线程而不违反上述单个写入器原则。因此,每当您向Zeebe发送事件时,接收器都会将数据添加到缓冲区。从那里,另一个线程将实际接管并处理数据。另一个缓冲区用于需要写入磁盘的字节。
此方法可实现批处理操作。Zeebe可以一次性将一堆事件写入磁盘; 或者通过一次网络往返发送一些事件给跟随者。
使用二进制协议(如gRPC)到客户端以及内部的简单二进制协议,可以非常有效地完成远程通信。

总结
Zeebe是一种全新的工作流/编排引擎,适用于云原生和云规模应用。Zeebe与其他所有编排/工作流引擎的区别在于它的性能以及它被设计为一个真正可扩展且具有弹性的系统,而没有任何中央组件,或者需要数据库。
Zeebe不遵循事务工作流引擎的传统观念,其中状态存储在共享数据库中,并在从工作流中的一个步骤移动到下一个步骤时进行更新。相反,Zeebe在复制的仅附加日志之上作为事件源系统工作。所以Zeebe与Apache Kafka等系统有很多共同之处。Zeebe客户可以发布/执行工作,因此完全反应式Reactive。
​​​​​​​与市场上的其他微服务编排引擎相反,Zeebe非常关注可视化工作流,因为我们认为可视化工作流是在设计时,运行时和操作期间提供异步交互可视性的关键。