在事件驱动的世界中处理异常

Dealing with exceptions in an event driven world

我正在尝试了解如何使用微服务(使用 apache kafka)在事件驱动的世界中处理异常。例如,如果您采用以下订单场景,在订单完成之前需要执行以下操作。

在这种情况下的任何阶段,都可能出现如下故障:

您如何跟踪每个阶段都被要求 and/or 完成?

你如何处理出现的问题?您将如何通知前端失败?

您描述的一些事情不是错误或异常,而是您应该在分布式架构中考虑的替代流程。

例如,商品缺货是您业务流程中一个完全有效的替代流程。一个可能需要人为干预的。您可以将消息移动到单独的队列并提供一些 UI 人工操作员可以处理问题、解决问题并使事件流继续进行的地方。

您描述的付款问题也有类似的情况。如果订单无法成功结算,人工操作员将需要调查并解决问题。就此而言,您的设计必须考虑将替代流程作为其中的一部分,并使其能够在消息最终进入需要人员查看的队列中时以某种方式进行干预。

这些情况应该与程序抛出的错误或异常区分开来。根据具体情况,这些情况实际上可能需要将消息移动到死信队列 (DLQ) 以供工程师查看。

这是一个非常广泛的话题,可以写整本书。

我相信您可能会受益于对以下概念的更多理解:

补偿交易背后的想法是,每个阴都有阳:如果你有一个可以下订单的交易,那么你可以用一个取消它的交易来撤销它命令。后一项交易是 补偿交易 。因此,如果您执行了许多成功的交易,然后其中一个失败了,您可以追溯您的步骤并补偿您所做的每一个成功的交易,从而恢复它们的副作用。

我特别喜欢书中的一个章节REST from Research to Practice。它的第 23 章(Towards Distributed Atomic Transactions over RESTful Services)深入解释了 Try/Cancel/Confirm 模式

一般来说,这意味着当您进行一组事务时,它们的副作用只有在事务协调器确认它们全部成功后才会生效。例如,如果您在 Expedia 进行预订并且您的航班有两条不同航空公司的航班,那么一笔交易将预订美国航空公司的航班,另一笔交易将预订联合航空公司的航班。如果您的第二次预订失败,那么您想补偿第一次预订。但不仅如此,您还想避免第一次预订有效,直到您能够确认两者。因此,初始交易进行了预订,但保留其副作用 等待确认 。第二个预订也会做同样的事情。一旦事务协调器知道一切都已保留,它可以向所有各方发送确认消息,以便他们确认他们的保留。如果未在合理时间内确认预订 window,受影响的系统会自动撤销预订。

《微服务世界》一书 Enterprise Integration Patterns has some basic ideas on how to implement this kind of event coordination (e.g. see process manager pattern and compare with routing slip pattern which are similar ideas to

如您所见,能够补偿事务可能很复杂,具体取决于分布式工作流的复杂程度。流程管理器可能需要跟踪每个步骤的状态,并知道何时需要撤消整个操作。这几乎就是微服务世界中 Sagas 的想法。

Microservices Patterns 一书有一整章,名为“使用 Sagas 管理事务”,详细介绍了如何实施此类解决方案。

我通常还考虑的其他几个方面如下:

幂等性

我认为在分布式系统中成功实施服务事务的关键在于使它们 idempotent。一旦您可以保证给定的服务是幂等的,那么您就可以安全地重试它而不必担心造成额外的副作用。但是,仅重试失败的交易并不能解决您的问题。

暂时性错误与持久性错误

在重试服务事务时,您不应该因为失败就重试。您必须首先知道失败的原因,并根据错误重试或不重试是否有意义。某些类型的错误是暂时的,例如,如果一个事务由于查询超时而失败,那么重试可能没问题,而且很可能第二次会成功;但是如果您遇到数据库约束违规错误(例如,因为 DBA 向字段添加了检查约束),那么重试该事务就没有意义:无论您尝试多少次,它都会失败。

拥抱错误作为替代流程

正如我在回答开头提到的,并不是所有的事情都是错误的。有些东西只是替代流程。

在服务间通信(计算机到计算机交互)的那些情况下,当您的工作流的给定步骤失败时,您不一定需要撤消您在之前步骤中所做的所有操作。您可以将错误作为工作流程的一部分。对可能的错误原因进行分类,使它们成为仅需要人工干预的替代事件流。这只是整个编排中的另一个步骤,需要一个人进行干预以做出决定、解决与数据的不一致或只是批准要走的路。

例如,当您正在处理订单时,可能由于您没有足够的资金而导致支付服务失败。因此,撤消其他所有内容毫无意义。我们所需要的只是将订单置于某个问题解决者可以在系统中解决的状态,一旦解决,您就可以继续其余的工作流程。

事务和数据模型状态是关键

我发现这种类型的事务性工作流需要对模型必须经历的不同状态进行良好的设计。与 Try/Cancel/Confirm 模式的情况一样,这意味着最初应用副作用而不一定使数据模型对用户可用。

例如,当您下订单时,您可能将其以“待处理”状态添加到数据库中,这种状态不会出现在仓库系统的 UI 中。确认付款后,订单将出现在 UI 中,以便用户最终可以处理其发货。

这里的难点在于发现如何设计事务粒度,即使您的事务工作流程的一个步骤失败,系统仍保持有效状态,一旦失败的原因得到纠正,您就可以从该状态恢复。

分布式事务工作流设计

因此,如您所见,设计以这种方式工作的分布式系统比单独调用分布式事务服务要复杂一些。现在每次服务调用都可能由于多种原因而失败,并使您的分布式工作流处于不一致的状态。并且重试交易不一定总能解决问题。并且您的数据需要像状态机一样建模,以便应用副作用但在整个编排成功之前不确认。

这就是为什么整个事情可能需要以不同于您通常在单一客户端-服务器应用程序中所做的方式进行设计的原因。在解决冲突方面,您的用户现在可能是设计解决方案的一部分,并考虑到事务编排可能需要数小时甚至数天才能完成,具体取决于他们的冲突是如何解决的。

正如我最初所说的,这个话题太宽泛了,可能需要一个更具体的问题来详细讨论这些方面中的一两个方面。

无论如何,我希望这对您的调查有所帮助。