微服务事件驱动架构的设计选择

Posted

技术标签:

【中文标题】微服务事件驱动架构的设计选择【英文标题】:Design choice for a microservice event-driven architecture 【发布时间】:2018-05-13 05:54:59 【问题描述】:

假设我们有以下内容:

DDD聚合A和B,A可以引用B。

一个管理 A 的微服务,它公开以下命令:

创建一个 删除A 将 A 链接到 B 取消 A 与 B 的链接

一个微服务管理 B,它公开以下命令:

创建 B 删除B

成功的创建、删除、链接或取消链接总是会导致执行该操作的微服务发出相应的事件。

为这两个微服务设计事件驱动架构的最佳方法是:

    A 和 B 最终将始终保持一致。通过一致性,我的意思是如果 B 不存在,则 A 不应引用 B。 来自两个微服务的事件可以轻松地投射到单独的读取模型中,在该模型上可以进行跨越 A 和 B 的查询

具体来说,以下示例可能会导致暂时的不一致状态,但在所有情况下都必须最终恢复一致性:

示例 1

初始一致状态:A 存在,B 不存在,A 未链接到 B 命令:将 A 链接到 B

示例 2

初始一致状态:A存在,B存在,A链接到B 命令:删除B

示例 3

初始一致状态:A存在,B存在,A未链接到B 同时执行两个命令:将 A 链接到 B 并删除 B

我有两个解决方案。

解决方案 1

微服务 A 仅允许将 A 链接到 B,前提是它先前已收到“B 创建”事件且未收到“B 已删除”事件。 仅当微服务 B 之前未收到“A 链接到 B”事件,或者该事件之后出现“A 与 B 未链接”事件时,微服务 B 才允许删除 B。 微服务 A 侦听“B 已删除”事件,并在接收到此类事件后,将 A 与 B 取消链接(针对 B 在收到链接到 B 事件的 A 之前被删除的竞争条件)。

解决方案 2:

微服务 A 始终允许将 A 链接到 B。 微服务 B 侦听“A 链接到 B”事件,并在收到此类事件后验证 B 是否存在。如果没有,它会发出“拒绝 B 的链接”事件。 微服务 A 侦听“B 已删除”和“链接到 B 被拒绝”事件,并在收到此类事件后,将 A 与 B 取消链接。

编辑:Guillaume 提出的解决方案 3:

微服务 A 仅允许在之前未收到“B 已删除”事件的情况下将 A 链接到 B。 微服务 B 始终允许删除 B。 微服务 A 侦听“B 已删除”事件,并在收到此类事件后,将 A 与 B 取消链接。

我看到的解决方案 2 的优势是微服务不需要跟踪其他服务发出的过去事件。在解决方案 1 中,基本上每个微服务都必须维护另一个微服务的读取模型。

解决方案 2 的一个潜在缺点可能是在读取模型中投射这些事件会增加复杂性,特别是如果将更多遵循相同模式的微服务和聚合添加到系统中。

一种或另一种解决方案是否有其他(不利)优势,甚至是我不知道的反模式,应该不惜一切代价避免? 有比我建议的两个更好的解决方案吗?

任何建议将不胜感激。

【问题讨论】:

【参考方案1】:

微服务 A 仅在之前收到“B 创建”事件且没有“B 删除”事件的情况下才允许将 A 链接到 B。

这里有一个潜在的问题;考虑两条消息之间的竞争,link A to BB Created。如果B Created 消息恰好先到达,那么一切都按预期连接。如果B Created 恰好第二个到达,则链接不会发生。简而言之,您的业务行为取决于您的消息管道。

Udi Dahan, 2010

时间上的微秒差异不应影响核心业务行为。

解决方案 2 的一个潜在缺点可能是在读取模型中投射这些事件会增加复杂性,特别是如果将更多遵循相同模式的微服务和聚合添加到系统中。

我根本不喜欢那种复杂性;听起来工作量很大,但商业价值并不高。

异常报告可能是一个可行的替代方案。 Greg Young talked about this in 2016。简而言之;拥有一个检测不一致状态的监视器并修复这些状态可能就足够了。

稍后会添加自动修复。 Rinat Abdullin described这个进展真好。

自动化版本最终看起来类似于解决方案 2;但是通过职责分离——修复逻辑存在于微服务 A 和 B 之外。

【讨论】:

感谢您的回答 VoiceOfUnreason。确实,我可以看到解决方案 1 确实有问题,感谢您指出这一点。我仍在阅读您的其他 cmets 和您提供的链接。我同意很难看出我的示例的商业价值,但我想提供一个更复杂的解决方案的简化视图,并抽象出与手头问题没有直接关系的所有内容。总的来说,在我看来,我所揭露的情况迟早会出现在任何微服务架构中。 通常:当您尝试抽象出领域的细节时,很难获得关于领域驱动设计的好的建议。几乎所有真正好的课程都来自于在真实模型中讨论不同的问题/解决方案。【参考方案2】:

您的解决方案看起来不错,但有些事情需要澄清:

在 DDD 中,聚合是一致性边界。聚合始终处于一致状态,无论它收到什么命令以及该命令是否成功。但这并不意味着从业务角度来看,整个系统处于允许的永久状态。有时整个系统处于不允许的状态。这没关系,只要最终它会在允许的状态下转换。 Saga/Process managers 来了。这正是它们的作用:使系统处于有效状态。它们可以部署为单独的微服务。

我在 CQRS 项目中使用的另一种类型的组件/模式是最终一致的命令验证器。他们使用私有读取模型在命令到达聚合之前验证命令(如果它不是有效,则拒绝它)。这些组件最大限度地减少了系统进入无效状态时的情况,并补充了 Sagas。它们应该部署在包含聚合的微服务内部,作为域层(聚合)之上的一层。

现在,回到地球。您的解决方案是聚合、Sagas 和最终一致的命令验证的组合。

解决方案 1

微服务 A 仅允许将 A 链接到 B,前提是它先前已收到“B 创建”事件且未收到“B 已删除”事件。 微服务 A 侦听“B 已删除”事件,并在收到此类事件后,将 A 与 B 取消链接。

在此架构中,微服务 A 包含 Aggregate ACommand validator,微服务 B 包含 Aggregate BSaga。重要的是要了解验证器不会阻止系统的无效状态,而只会降低概率。

解决方案 2:

微服务 A 始终允许将 A 链接到 B。 微服务 B 侦听“A 链接到 B”事件,并在收到此类事件后验证 B 是否存在。如果没有,它 发出“拒绝 B 的链接”事件。 微服务 A 侦听“B 已删除”和“链接到 B 被拒绝”事件,并在收到此类事件后,将 A 与 B 取消链接。

在这个架构中,微服务 A 包含 Aggregate A 和一个 Saga,Microservice B 包含 Aggregate B 和一个 Saga。如果 B 上的 Saga 将验证 B 的存在并向 A 发送 Unlink B from A 命令而不是产生事件,则可以简化此解决方案。

无论如何,为了应用SRP,您可以将 Sagas 提取到他们自己的微服务中。在这种情况下,您将拥有每个聚合和每个 Saga 的微服务。

【讨论】:

感谢康斯坦丁并指出 sagas 和最终一致的命令验证器的存在。正如 VoiceOfUnreason 解释的那样,最终一致的命令验证器会通过拒绝有效命令而导致问题,因为它不是立即一致的。我还注意到您建议用“从 A 取消链接 B”命令替换“拒绝 B 的链接”事件。发生该事件的原因是,一个聚合体指挥另一个聚合体似乎很奇怪。但由于这是 saga 的责任,可能托管在它自己的微服务中,所以一切都很好。 确实,@VoiceOfUnreason 的观点很有趣,但在某些情况下,这种验证器是可以的,例如在本地微服务中检查授权时:你永远不会允许未经授权的操作用户,即使它是以一种偶然性一致的方式完成的。 已注意到,谢谢。我很好奇:您允许 saga 向其他微服务发送消息,例如“从 A 取消链接 B”,而不是依赖事件。您是否也允许将其用于命令验证器?具体来说,如果一个微服务公开了他管理的聚合的立即一致的读取模型,那么另一个微服务中的验证器是否可以向该微服务发送查询命令以访问其读取模型,从而成为“立即一致”的命令验证器“最终一致”的命令验证器? @Odsh 是的,命令验证器可以访问另一个 MS 读取模型,只要您记住它始终是最终一致的(无论它保持同步的速度有多快)。 IE。在您允许提款之前,您不应使用此类验证器来检查银行账户的余额。 (已编辑) 我编辑了我之前的评论,因为我在假设事件溯源时犯了一个错误。对不起。【参考方案3】:

我将从与@ConstantinGalbenu 相同的前提开始,但随后提出不同的主张;)

最终一致性意味着整个系统最终将 收敛到一致的状态。

如果您添加到那个“无论接收消息的顺序如何”,您就会得到一个非常有力的声明,您的系统在没有帮助的情况下自然会趋向于最终的连贯状态外部流程管理器/传奇。

如果您从接收者的角度使最大数量的操作可交换,例如link A to B 是在 create A 之前还是之后到达都没有关系(它们都导致相同的结果状态),你几乎就在那里。这基本上是解决方案 2 的第一个要点,概括为最多事件,但不是第二个要点。

微服务 B 侦听“A 链接到 B”事件,并在收到 这样的事件,验证 B 存在。如果没有,它会发出一个“链接 对 B 拒绝”事件。

您无需在名义情况下执行此操作。在您知道 A 没有收到 B deleted 消息的情况下,您会这样做。但是,它不应该成为您正常业务流程的一部分,这是消息平台级别的交付失败管理。我不会通过原始数据来源的微服务对所有内容进行这种系统的双重检查,因为事情变得太复杂了。看起来好像您正试图将一些即时一致性恢复到最终一致的设置中。

该解决方案可能并不总是可行,但至少从不发出事件以响应其他事件的被动读取模型的角度来看,我想不出您无法管理的情况以可交换的方式处理所有事件。

【讨论】:

感谢您的回复。您提到我不需要发送“拒绝 B 的链接”事件,即解决方案 2 在没有第二个要点的情况下也可以工作。在这种情况下,例如,假设我为从未存在的 B 发送命令“将 A 链接到 B”。系统如何收敛到一致状态? 如果 B 不存在,B 的标识符从何而来? 很公平。 B 的标识符已经被删除,并且微服务 A 过去已经收到了“B 已删除”事件? 我可以看到你在这里扮演了一些魔鬼的拥护者,因为你在之前的评论中写道(对另一个 Q)“我们这样做的方式是我们允许将 B 绑定到 C在所有情况下” :) 但这是一个有趣的观点。微服务只能根据自己的状态和之前收到的事件做出决策。将自己绑定到您知道在原始微服务中不再存在的实体是不合理的行为。对我来说,只有当每个参与者合理地与系统中发生的事情同步时,才能收敛到一致的状态。 我的意思是,如果你这样做,微服务几乎可以忽略它收到的X created 事件,并决定将其视为Y created。那么它就不是合作了,它在做自己的事情。您不能将其视为“试图收敛到一致状态的系统”IMO 的一部分。

以上是关于微服务事件驱动架构的设计选择的主要内容,如果未能解决你的问题,请参考以下文章

事件驱动的微服务-总体设计

领域驱动设计 领域事件DDD分层架构

软件架构设计学习总结(22):软件架构——分层架构事件驱动架构微内核架构微服务架构基于空间的架构

微服务实践:微服务的事件驱动数据管理

大数据事件驱动的微服务架构

软件架构入门-分层架构、事件驱动、微服务架构和云原生架构