事件溯源的事件处理程序应该如何托管以构建读模型?

7
有各种示例应用程序和框架实现CQRS +事件溯源架构,并且大多数都描述了使用事件处理程序从事件存储中创建非规范化视图。其中一种托管此架构的方法是将其作为Web API,接受命令以进行写操作,并支持查询非规范化视图。 这个Web API很可能会在负载平衡的许多计算机中扩展。
我的问题是非规范化模型事件处理程序托管在哪里?
可能的情况:
  1. 托管在单个窗口服务中,位于不同的主机上。 如果是这样,那么这不会创建单点故障吗?这可能会使部署复杂化,但它确保了单个执行线程。缺点是读模型可能会表现出增加的延迟。

  2. 作为Web API本身的一部分托管。 例如,如果我使用EventStore进行事件存储和事件订阅处理,那么是否会为每个单个事件触发多个处理程序(在每个Web农场进程中一个),从而在处理程序中引起争用,因为它们尝试读/写其读取存储?或者我们保证对于给定的聚合实例,其所有事件将按事件版本顺序逐个处理?

我倾向于方案2,因为它简化了部署,并且还支持需要同时侦听事件的流程管理器。不过情况相同,只有一个事件处理程序应该处理单个事件。

EventStore能处理这种情况吗?其他人如何处理最终一致性架构中的事件处理?

编辑:

为了澄清,我谈论的是将事件数据提取到非规范化表中的过程,而不是读取这些表以进行CQRS中的“Q”。
我想要的是关于如何实现和部署事件处理的选项,以支持冗余和扩展,假设事件的处理以幂等方式处理。
我已经阅读了两种处理保存为事件的数据的可能解决方案,但我不知道应该使用哪一种。
事件总线
事件总线/队列用于在保存事件后发布消息,通常由存储库实现。感兴趣的各方(订阅者),例如读模型或saga /流程管理器,以某种方式使用总线/队列以幂等方式处理它。
如果队列是发布/订阅,则意味着每个下游依赖项(读模型、saga等)只能支持一个进程订阅队列。多个进程意味着每个进程都会处理相同的事件,然后竞争使更改向下游传播。幂等处理应该解决一致性/并发问题。
如果队列是竞争消费者,我们至少可以在每个 Web Farm 节点上托管订阅者以实现冗余。虽然这需要为每个下游依赖项设置一个队列;一个用于 saga/process manager,一个用于每个读模型等,因此仓库必须向每个队列发布以实现最终一致性。
订阅/源
订阅/源是感兴趣的方(订阅者)按需读取事件流,并从已知检查点获取事件以处理成为读模型的方式。
如果需要重新创建读模型,则看起来很不错。但是,按照通常的发布/订阅模式,似乎应该仅使用每个下游依赖项的一个订阅者进程。例如,在每个 Web Farm 节点中注册多个订阅者,它们都将尝试处理并更新相同的读模型。

请问有关于DDD-CQRS-ES的Slack或邮件组吗? - Ruben Bartelink
当你说“读取模型”时,你是指查询非规范化表,那么查询应用程序应该托管在哪里? - sagar
谢谢Sagar。我已经为问题添加了更多的上下文。 - Mark
2个回答

7
在我们的项目中,我们使用基于订阅的投影。原因如下:
  • 对写入端进行承诺必须是事务性的,如果您使用两个基础设施(事件存储和消息总线),则必须开始使用DTC,否则您的事件可能会保存到存储中但未发布到总线上,或者反之亦然,这取决于您的实现。 DTC和两阶段提交是很糟糕的事情,您不希望走这条路
  • 事件通常会在消息总线上发布(我们也通过订阅方式进行),用于不同边界上下文之间的事件驱动通信。如果您使用消息订阅程序来更新读取模型,当您决定重新构建读取模型时,您的其他订阅者也将收到这些消息,这将导致系统处于无效状态。我认为当你说你必须只有一个每种发布消息类型的订阅者时,你已经想过这个问题了。
  • 消息总线消费者没有关于消息顺序的保证,这可能会使您的读取模型变得混乱。
  • 消息消费者通常通过将消息发送回队列来处理重试,并且通常在队列末尾进行重试。这意味着您的事件可能会严重失序。此外,通常在一些重试后,消息消费者放弃了毒消息并将其放入某个DLQ。如果这将是您的投影,则意味着一个更新将被忽略,而其他更新将被处理。这意味着您的读取模型将处于不一致(无效)状态。
考虑到这些原因,我们使用基于订阅的单线程投影,可以做任何事情。您可以使用自己的检查点进行不同类型的投影,使用catch-up订阅来订阅事件存储。为了简单起见,我们将它们托管在与许多其他事物相同的进程中,但该进程仅在一台机器上运行。如果我们想要扩展此过程,我们将不得不将订阅/投影排除在外。这可以轻松完成,因为此部分实际上对其他模块几乎没有依赖,除了读取模型DTO本身,它可以作为程序集共享。

通过使用订阅,您始终会投影已经提交的事件。如果投影出现问题,则写入方肯定是真正的数据源,并且仍然如此,您只需要修复投影并重新运行即可。

我们有两个单独的投影-一个用于投影到读取模型,另一个用于将事件发布到消息总线。这种结构被证明非常有效。

1
抱歉回复晚了,但感谢Alexey提供的非常有信息量的答案。顺序错乱问题似乎总是让人觉得总线不是正确的选择。但我一直记在脑后的是,具有竞争消费者模式的总线可以很好地扩展。只是想知道,我们是否可以将总线作为一个ping发送到多实例消费者,以通知它们有新消息,并且应该赶上?这意味着可能不必将单个实例进程与其他域代码分开托管。 - Mark
订阅事件流非常有效,为什么还需要使用总线呢?竞争消费者非常好用,我们经常在需要可扩展性的地方使用它们,但不用于构建读模型。扩展读模型构建器的最佳方法是对读模型存储进行分区,然后可以拆分处理。但事件仍然需要被线性化处理。 - Alexey Zimarev
所以,与其尝试更新一个被负载均衡机群消耗的单个读模型存储,不如让每台机器都有自己的读模型存储,甚至可能是在内存中,并让每台机器通过事件订阅负责更新自己的存储。我理解得对吗? - Mark
分区可以通过多种方式进行,但并不要求您的数据库进行分区,只要您可以对订阅者进行分区即可。最重要的是确保单个聚合的事件顺序。 - Alexey Zimarev

0

针对EventStore,他们现在有竞争消费者,这是基于服务器的订阅,许多客户端可以订阅订阅组,但只有一个客户端收到消息。

听起来这就是你想要的,农场中的每个节点都可以订阅订阅组,接收消息的节点进行投影。


1
由于缺乏顺序保证,竞争性消费者对读取模型的构建并不十分有吸引力。在构建读取模型时,放弃事件顺序时必须非常小心。 - Alexey Zimarev
是的,很好的观点,当将事件投影到读取模型时,顺序非常重要。 - Matt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接