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

Posted

技术标签:

【中文标题】在事件驱动的世界中处理异常【英文标题】:Dealing with exceptions in an event driven world 【发布时间】:2018-10-01 23:48:53 【问题描述】:

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

1) 向支付服务商授权支付 2) 从库存中保留商品 3.1) 通过支付服务提供商获取付款 3.2) 订购商品 4) 发送电子邮件通知接受带有收据的订单

在此场景的任何阶段,都可能出现以下故障:

该商品已无货 付款信息不正确 收款人使用的账户没有可用资金 外部调用(例如对支付服务提供商的调用)失败,例如停机

您如何跟踪每个阶段已被要求和/或完成?

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

【问题讨论】:

你听说过“死信队列”吗? 当然,我熟悉这个概念,这会有所帮助。您将如何处理能够阅读消息但无法发送响应的情况?使用交易来解决这个问题? 【参考方案1】:

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

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

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

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

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

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

Compensating Transactions Pattern Try/Cancel/Confirm Pattern Long Running Transactions Sagas

补偿交易背后的想法是,每一个 ying 都有它的 yang:如果你有一个可以下订单的交易,那么你可以通过取消该订单的交易来撤销它。后一种交易是补偿交易。因此,如果您执行了多次成功的交易,但其中一个失败了,您可以追溯您的步骤并补偿您所做的每笔成功交易,从而恢复其副作用。

我特别喜欢书中REST from Research to Practice 的一章。它的第 23 章(基于 RESTful 服务的分布式原子事务)深入解释了尝试/取消/确认模式

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

Enterprise Integration Patterns 这本书有一些关于如何实现这种事件协调 的基本思想(例如参见process manager pattern 并与routing slip pattern 进行比较,这与orchestration vs choreography 在微服务世界)。

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

Microservices Patterns 这本书有一整章叫做用 Sagas 管理事务,详细介绍了如何实现这种类型的解决方案。

我通常还会考虑以下几个方面:

幂等性

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

暂时性错误与持续性错误

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

将错误作为替代流程

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

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

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

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

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

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

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

为分布式事务工作流设计

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

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

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

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

【讨论】:

以上是关于在事件驱动的世界中处理异常的主要内容,如果未能解决你的问题,请参考以下文章

事件代码:4011 尝试使用 Handler 上传文件时发生未处理的访问异常

事件与状态机 事件驱动编程

nodejs入门总结二:事件驱动

事件驱动模型初探

java事件驱动框架都有哪些推荐

「事件驱动架构」事件溯源,CQRS,流处理和Kafka之间的多角关系