【问题标题】:CQRS: project out-of-order notifications in an ElasticSearch read modelCQRS:在 ElasticSearch 读取模型中项目乱序通知
【发布时间】:2018-05-11 00:25:44
【问题描述】:

我们有一个微服务架构并应用 CQRS 模式。发送到微服务的命令会触发应用程序状态更改并在我们的 Kafka 总线上发出相应的事件。我们将这些事件投射到使用 ElasticSearch 构建的读取模型中。

到目前为止,一切都很好。

我们的微服务最终彼此一致。但在任何给定时间,它们都不是(必要的)。因此,它们发送的事件也不总是彼此一致。

此外,为了保证应用程序状态更改和相应事件的发射之间的一致性,我们在数据库中将新状态和相应事件持久保存在同一事务中(我知道我们可以使用事件源并避免持久化整个国家)。然后一个异步工作者负责在 Kafka 总线上发送这些事件。这种模式保证每次状态变化都会发送至少一个事件(这不是问题,因为我们的事件是幂等的)。但是,由于每个微服务都有自己的事件表和异步工作器,我们不能保证事件会按照各自微服务发生相应状态变化的顺序发送。

编辑:澄清一下,每个微服务都有自己的数据库、自己的事件表和自己的工作者。特定的工作人员按照事件在其相应事件表中的持久化顺序处理事件,但不同事件表上的不同工作人员,即不同的微服务,不提供这样的保证。

当从同一个 ElasticSearch 文档中的不同微服务中投射这些不连贯或无序的事件时,就会出现问题。

一个具体的例子:让我们想象三个不同的聚合 A、B 和 C(领域驱动设计意义上的聚合)由不同的微服务管理:

  • A 和 B 之间存在多对多关系。聚合 A 引用了他绑定到的聚合根 B,但 B 不知道它与 A 的关系。当 B 被删除时,管理 A 的微服务会监听相应的事件并撤消 A 与 B 的绑定。
  • 类似地,B 和 C 之间存在多对多关系。B 知道所有相关的 C 聚合,但反之则不成立。当 C 被删除时,管理 B 的微服务会监听相应的事件,并解除 B 与 C 的绑定。
  • C 有一个属性“名称”。

其中一个用例是通过 ElasticSearch 查找绑定到聚合 B 的所有聚合 A,而聚合 B 又绑定到具有特定名称的聚合 C。

如上所述,单独的事件表和工作器可能会在不同微服务的事件发射之间引入可变延迟。例如,创建 A、B 和 C 并将它们绑定在一起可能会导致以下事件序列:

  1. B 已创建
  2. B 绑定到 C
  3. 用名称 XYZ 创建的 C
  4. 创建一个
  5. A 绑定到 B

另一个批处理事件的例子:假设我们最初有聚合 B 和 C,并且同时发出两个命令:

  • 删除C
  • 将 B 绑定到 C

这可能会导致以下事件:

  1. C 已删除
  2. B 绑定到 C
  3. B 未与 C 绑定(响应事件 1)

具体而言,我们无法将这些事件投射到 ElasticSearch 文档中,因为这些事件有时会引用不再存在或尚不存在的聚合。任何帮助将不胜感激。

【问题讨论】:

  • “但是,由于每个微服务都有自己的事件表和异步工作者,我们不能保证事件会按照各自微服务发生相应状态更改的顺序发送” - 为什么?工作人员不是按照事件被持久化的顺序处理事件吗?
  • @Odsh 您的问题似乎是索引器/投影仪没有像其他微服务一样同时收到事件通知。也就是说,如果我正确地解释了你的事件顺序 - 我是吗?
  • @Constantin,每个微服务都有自己的数据库、自己的事件表和自己的工作者。特定的工作人员按照它们被持久化的顺序处理事件,但不同事件表上的不同工作人员不提供这样的保证。
  • @Guillaume,我明白你的意思了。如果管理 B 的微服务仅在收到“C 创建”事件且没有“C 删除”事件的情况下才允许将 B 绑定到 C,那么确实只有在索引器/投影仪在不同的顺序。我们不做那种验证。但即使我们这样做了,除非微服务 B 和 C 将他们的事件发布在同一个 Kafka 主题和分区中(这甚至可能/推荐吗?),我相信是的,这仍然可能发生。
  • 是的,这正是我的意思。不过有些事情让我很烦——如果 B 的微服务没有收到“C created”事件,它怎么会知道它应该将 B 绑定到 C?这似乎是事件驱动架构中的异常情况。

标签: elasticsearch apache-kafka domain-driven-design microservices cqrs


【解决方案1】:

我不认为您提出的问题是系统的投影部分独有的 - 它也可能发生在微服务 A、B 和 C 之间。

通常情况下,投影仪会与 B 同时获得C created。只有这样 B 才能将自己绑定到 C,这使得您提到的特定顺序不可能发生在投影仪上。

但是,如果 B 和 C 之间的网络通信比 C 和投影仪之间的网络通信快得多,则消息可能以错误的顺序到达是正确的。

我从来没有遇到过这样的问题,但我想到了几个选项:

  • 不要在读取模型级别强制执行“外键”。即使您现在对 C 知之甚少,也将 B 与其 C 引用一起存储。换句话说,使B bound to CC created 可交换。

  • 在您的活动中添加causation ID。这允许客户端识别和处理乱序消息。您可以选择自己的策略 - 拒绝、等待因果事件到达、无论如何都尝试处理等等。不过,这并不是一件容易实现的事情。

  • 消息平台可以保证在特定条件下的订购。您提到了 Kafka,在同一主题和分区下。我认为 RabbitMQ 有更强大的先决条件。

    我不是消息传递专家,但看起来可行的微服务间通信场景是有限的。它似乎也与最终一致性的当前趋势背道而驰,在这种趋势中,我们倾向于支持交换操作(参见 CRDT)而不是确保总顺序。

【讨论】:

  • 澄清一下,在我们的解决方案中,聚合 B 引用了它所绑定的 C 的聚合根,但 C 不知道它与 B 的绑定。换句话说,管理 B 的微服务还管理绑定到 C。此外,始终允许将 B 绑定到 C,即使 C 不存在。但是,管理 C 的微服务会侦听这些“B 绑定到 C”事件,如果 C 不存在,则发出一个事件来反驳该绑定。在接收到最后一个事件后,绑定被撤消。另外,当C被删除时,相应的事件也会触发B与C的解除绑定。
  • 这更清楚了,谢谢。不确定这是否使我的回答无关紧要。而且我仍然想知道除了系统某些部分的大消息传递延迟之外,您如何以 Q 中描述的无序事件序列结束。也许交换组件和消息的序列图会有所帮助。
  • 澄清是关于您的假设,即 B 只有在之前收到“C created”事件时才能将自己绑定到 C,至少在我们当前的解决方案中,这不是真的。但是你是对的,它没有解释我在示例中给出的事件顺序,这确实是纯粹的延迟问题。这种延迟可能是由网络引起的,也可能是由每个服务的事件表以及每个表被其相应的工作人员清空的速度引起的。我相信因果 ID 是解决该特定问题的不错选择,谢谢(仍在考虑中)。
  • 我在原始问题中添加了我认为可能发生的一批事件的示例,即使如您所建议的那样,如果 B 之前收到“C created”,则它只允许将自己绑定到 C没有“C 已删除”事件,只有当它从未收到“B 绑定到 C”且没有“B 未从 C 绑定”事件并且读取模型以与其他服务相同的顺序接收事件时,才允许删除 C .
  • 顺便说一句,我问了一个与您的建议有关的问题here
猜你喜欢
  • 1970-01-01
  • 2017-04-03
  • 1970-01-01
  • 2019-02-16
  • 1970-01-01
  • 2021-03-25
  • 2017-05-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多