【问题标题】:Can DDD/CQRS Aggregate Roots be microservices?DDD/CQRS 聚合根可以是微服务吗?
【发布时间】:2018-09-13 13:42:31
【问题描述】:

我目前正在设计新系统,我坚信 CQRS+ES 非常适合。我想验证我的“大规模”设计预设是否听起来不错,并且我没有走错方向。

对我来说,由于一致性和网络边界一致,让每个聚合根(写入模型)都存在于自己的微服务中似乎是个好主意。

由于一致性保证,我认为可以安全地假设每个聚合实例可能只有自己的事件流。在实践中,为每个聚合 instance 结束一个事件存储将是一种矫枉过正,但在相同聚合类型的微服务之间共享一个或几个复制的事件存储对我来说似乎是明智的选择。如果不需要复制,我们甚至可以根据聚合 ID 对事件存储进行分片。

这样,由于很少有聚合类型范围的微服务,每个处理大量聚合实例的命令,扩展应该对系统的其余部分足够透明。

那么让投影仪(读取模型)也存在于它们自己的微服务中是有意义的,每个微服务都有自己的数据库,应该在相同类型的投影仪之间共享。

因为投影仪的查询界面对外部世界不是很友好(我假设投影仪提供类似 Repository 的界面来发出查询,在我看来,这在访问控制、速率限制等方面表现不佳),每个投影仪应该提供一些统一的网络接口,供 BFF (backend-for-frontend) 使用,实际上为一些 API 端点提供服务,确保安全,提供版本控制等。

tl;dr: 我提供了上面的图形表示(带圆圈),以用我糟糕的绘图来弥补我的糟糕措辞。 PS:重播服务是在新事件附加到事件存储时监视它们的服务,并将它们广播到感兴趣的订阅投影仪或流程管理器(未绘制),或者为具有陈旧或空数据库的投影仪重播整个事件序列。

这种 CQRS+微服务适配听起来不错,还是我从根本上误解了某些东西,整个设计都是垃圾?

UPD1:

为什么要在事件源和投影仪之间设置负载平衡器,如何平衡负载?

如果我生成多个相同类型的投影仪实例来处理来自繁重查询的额外负载,它们将如何监听事件?对我来说,只分配一个实例来完成所有事件处理工作、更新数据库等似乎很奇怪,因为随着负载的增加,它可能会过载。所以,分发事件处理也是有意义的,对吧?

另外,虽然我一直在写这篇文章,但我想将投影仪进一步拆分为“投影仪编写器”(监听事件并更新数据库中的共享状态)和“投影仪读取器”是否是个好主意"(那些侦听查询并返回状态的人),数据库作为事实来源和整合点。通过这种方式,我们可以更好地扩展非对称负载(小事件、大量查询)而无需任何成本。

必要条件之一是防止不同的投影仪编写器实例同时处理来自同一聚合的事件,因为使用无序事件更新表示会导致内部一致性的丧失和直接的灾难。

至于“如何”,我能想到几个解决方案:

  1. 为所有传入事件保留单个 RabbitMQ 队列,并让所有投影仪使用确认队列中的事件。 DB 更新后,投影仪编写器向 RabbitMQ 发出 ack,并且从队列中丢弃事件。否则,如果投影仪编写器由于某种原因死亡,事件将再次重新排队到下一个投影仪。

    对于每个聚合,我们应该保留高度/修订号,并且只有在下一个修订号(包含在事件中)正好比当前修订号大一时才允许 UPDATE 成功。如果此条件不成立,则重新排队此事件,希望届时可以解决不一致问题,然后获取下一个。

    最终,它会完成,并且给定足够多的聚合,它永远不需要重新排队。

  2. 放置某种调度器服务来监听事件,每个投影仪类型一个调度器。此调度程序应根据聚合 ID 的哈希将事件分发给投影仪编写器,因此,同一聚合始终由索引为 hash(event.aggregateId) % numberOfProjectorWriters 的同一聚合处理。

    这将永远不会重新排队,但会提前终止 MQ,引入单点故障,并且如果由于某些节点死亡或动态缩放而导致投影仪写入器的数量发生变化,或者......

  3. 以某种方式使用标头交换来实现 #1 和 #2 的组合,以使消费者更喜欢同一组聚合 ID,但不会在中间消费者数量发生变化时搞砸。

【问题讨论】:

  • 为什么/如何平衡投影仪(在右侧,“建筑”侧)?
  • @ConstantinGalbenu 我已经用答案更新了问题。欢迎反馈。此外,那些不是“建筑物”中的窗户,那些是圆柱体,例如。 DB象形图...
  • 您正试图保持事件的顺序以尊重聚合;一般来说,读取模型(投影)需要total顺序的事件;否则,虽然有可能,但会导致非常复杂的 readmodel 更新程序
  • 但是总的事件顺序一般不是不可能的吗?据我了解,由于没有“全局状态”,也没有单点一致性,怎么可能有同时独立发生的事件的总排序?当然,可以通过简单地遵循时间戳来提供某种顺序,希望最终保持一致,但这不是全部,对我来说,它与来自不同聚合的随机交错事件没有太大区别。我错了吗?
  • 这不是不可能的。有使这成为可能的 EventStore 实现。例如,在 MongoDB 中,有时间戳使用客户端和服务器之间甚至分片之间的因果时钟保持同步

标签: architecture domain-driven-design cqrs event-sourcing


【解决方案1】:

我相信,虽然技术上聚合的根可以存在于它们自己的微服务中,但这种粒度级别可能会带来不必要的复杂性。通常他们会说,从架构良好的单体开始。

通常,如果 BC 中有几个聚合,它们可能会共享一些服务、存储库,因此这些聚合在一起可以显着降低复杂性,并形成一个有凝聚力的组件。但这可能取决于扩展需求。

顺便问一下,您的意思是什么事件存储一致性保证?事件流最有用的保证是事件的顺序。这在分布式环境中真的很难实现。如果您为候选事件商店提供此类保证的链接,那就太好了。

我同意你的观点,读取模型可能位于单独的微服务中,并受到单独的缩放。实际上,读取模型管理器可以是单独的反应式微服务。但是这些管理器生成的读取模型是普通资源,可以使用简化的 REST API 存储在单个读取优化、缩放和分片的资源存储中,其中读取模型管理器只是 PUT 准备好的资源,并用一些超时和标记它们其他元数据。

不仅如此,聚合也可能具有 REST API。命令和事件可以是此类 API 服务的资源。您可以将命令发布到聚合、返回命令结果的 URL、GET 聚合的事件或 GET 聚合类的合并事件流...

【讨论】:

  • 我认为事件存储通过充当特定聚合实例的单一事实来源来提供一致性保证。例如。事件顺序保证 = 内部一致性保证,只要每个事件离开聚合处于一致状态。
  • 至于细粒度 - 好吧,因为网络和一致性边界重合,并不意味着我们应该总是为每个微服务放置一个聚合。也许,在某些情况下,将几个聚合捆绑在一起是有意义的,但是,从我的角度来看,只要它们使用的服务和存储库是远程的,或者假装是远程的,这不会有任何区别。
  • jepsen.io/analyses 在这里非常需要证明一致性,尤其是在集群模式下。
  • 好的,但是为什么事件流会在不同节点之间聚集/复制呢?事件流是原子的。只要事件流中的每个事件都使您的聚合保持一致状态,并且只要您的系统是从正确定义的聚合构建的(例如,不共享事件流,不从写入模型端查询其他聚合,不做坏事 Greg Young不希望你这样做,并且总是检查你的不变量),为什么最终会不一致?据我了解,聚合完全零保证它们的外部引用甚至最终达到某个目标。
  • 是的,对于预期为真的单个聚合的事件流。但我还是要找到一个明确的证据。就像当您阅读有关可序列化事务隔离级别时一样 - 您知道会发生什么。我想对活动商店有类似的要求。也许我不够专心。
猜你喜欢
  • 2018-08-12
  • 2018-06-24
  • 2016-03-29
  • 2022-03-23
  • 2016-03-21
  • 1970-01-01
  • 1970-01-01
  • 2021-05-30
  • 2020-04-01
相关资源
最近更新 更多