【问题标题】:An event store could become a single point of failure?事件存储可能成为单点故障?
【发布时间】:2020-10-27 20:15:57
【问题描述】:

几天以来,我一直在尝试弄清楚如何通知其他微服务,在微服务 A 中创建了一个新实体,该微服务 A 将该实体存储在 MongoDB 中。

我想:

  • 微服务之间的耦合度低

  • 避免微服务之间的分布式事务,如两阶段提交 (2PC)

起初,像 RabbitMQ 这样的消息代理似乎是完成这项工作的好工具,但后来我发现 commit MongoDB 中的新文档和 publish 消息的问题在代理中不是原子的。

Why event sourcing? by eventuate.io:

解决此问题的一种方法是通过添加一个标记来说明文档是否已在代理中发布并具有计划的后台进程来搜索 MongoDB 中未发布的文档并将其发布到使用confirmations 的代理,当确认到达时,文档将被标记为已发布(使用至少一次和幂等语义)。此解决方案在this 和this 答案中提出。

阅读 Chris Richardson 的 Introduction to Microservices,我最终看到了 Developing functional domain models with event sourcing 的精彩演示文稿,其中一张幻灯片问道:

如何原子地更新数据库和发布事件和发布事件没有2PC? (双写问题)。

答案很简单(在下一张幻灯片上)

更新数据库和发布事件

这是基于CQRS a la Greg Young 的this one 的不同方法。

域存储库负责发布事件,这 通常会与存储一起在单个事务中 事件存储中的事件。

我认为将存储和发布事件的责任委托给事件存储是一件好事,因为它避免了 2PC 或后台进程的需要。

然而,在某种意义上,这是真的that:

如果您依赖事件存储来发布事件,您将拥有一个 与存储机制紧密耦合。

但如果我们采用消息代理来实现微服务之间的通信,我们也可以这么说。

让我更担心的是事件存储似乎变成了单点故障。

如果我们从eventuate.io 看这个example

我们可以看到,如果事件存储发生故障,我们将无法创建帐户或汇款,从而失去了微服务的优势之一。 (尽管系统会继续响应查询)。

那么,确认最终示例中使用的事件存储是单点故障是正确的吗?

【问题讨论】:

标签: microservices cqrs event-sourcing eventstoredb


【解决方案1】:

您面临的是Two General's Problem 的一个实例。基本上,您希望网络上的两个实体就 the network is not fail safe 之外的某事达成一致。 Leslie Lamport 证明这是不可能的。

因此,无论您向网络中添加多少新实体(消息队列为一个),您都无法 100% 确定会达成一致。事实上,情况正好相反:您添加到分布式系统中的实体越多,您就越无法确定最终会达成一致。

您的案例的一个实际答案是,如果您考虑增加更多的复杂性和单点故障,2PC 并没有那么糟糕。如果你绝对不想要单点故障,想假设网络是可靠的(也就是说网络本身不可能是单点故障),你可以试试DHT这样的P2P算法,但对于两个同行,我敢打赌它会简化为简单的 2PC。

【讨论】:

  • 同意另见拜占庭将军问题。我讨厌持久队列提供的虚假保证,当它失败时(磁盘/配额不足、磁盘写入、毒消息、错误)没有考虑,结果往往是灾难性的。
【解决方案2】:

我们使用 NServiceBus 中的发件箱方法来处理这个问题:

http://docs.particular.net/nservicebus/outbox/

这种方法要求整个操作的初始触发器作为消息进入队列,但效果很好。

【讨论】:

  • 感谢乌迪!两个问题:Bus.Send(new PreparePurchasedProducts());和Bus.Publish(new PurchaseOrderReceived());是什么意思? Dispatch 步骤中没有发布消息? Queue Tx. 括号是什么意思?我了解如果 Dispatch 步骤失败,Db Tx 不会回滚。
  • 消息在分派步骤中被分派给代理,是的,但这可能会失败(例如,因为代理不可用)。较大的 Queue Tx 括号表示在处理传入消息结束时发送给代理的最终确认。任何失败都会导致该 ack 不被分派,从而导致传入的消息回滚并重试。此时,所有先前记录的操作都将从存储中提取并由基础架构重新调度,而无需再次调用业务代码。
【解决方案3】:

您还可以为事件存储中的每个条目创建一个标志,以告知此事件是否已发布。另一个进程可以轮询事件存储以查找那些未发布的事件,并将它们放入消息队列或主题中。这种方法的缺点是这个队列或主题的消费者必须设计为对传入消息进行重复数据删除,因为这种模式只保证至少一次传递。由于轮询频率,另一个缺点可能是延迟。但是由于我们已经在这里进入了最终一致的区域,所以这可能不是一个大问题。

【讨论】:

    【解决方案4】:

    如果我们有两个 事件存储,并且每当创建一个 域事件 时,它都会被排入它们的队列中。查询端的事件处理程序处理从两个事件存储中弹出的事件。

    当然,每个事件都应该是幂等的。 但这难道不能解决我们的事件存储是单一入口点的问题吗?

    【讨论】:

      【解决方案5】:

      不是特别是 mongodb 解决方案,但您是否考虑过利用 Redis 5 中引入的 Streams 功能来实现可靠的事件存储。看看这个介绍here

      我发现它具有丰富的功能,例如消息拖尾、消息确认以及轻松提取未确认消息的能力。这肯定有助于实现至少一次消息传递保证。它还支持使用“消费者组”概念的消息负载平衡,这有助于扩展处理部分。

      关于您对成为单点故障的担忧,根据文档,流和消费者信息可以跨节点复制并持久化到磁盘(我相信使用常规的 Redis 机制)。这有助于解决单点故障问题。我目前正在考虑将其用于我的一个微服务项目。

      【讨论】:

        猜你喜欢
        • 2017-12-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多