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

18
我正在尝试理解在使用微服务(使用Apache Kafka)的事件驱动世界中如何处理异常。例如,如果您考虑以下订单场景,需要在完成订单之前执行以下操作:
  • 1)使用付款服务提供商授权付款
  • 2)从库存中保留商品
  • 3.1)使用付款服务提供商捕获付款
  • 3.2)订购该商品
  • 4)发送电子邮件通知,接受有收据的订单
在此场景中的任何阶段都可能出现失败,例如:
  • 该商品已无库存
  • 付款信息不正确
  • 付款人使用的帐户没有可用资金
  • 外部调用(例如对于付款服务提供商)失败,例如停机时间
您如何跟踪每个阶段是否已经被调用和/或完成?
您如何处理出现的问题?您将如何通知前端关于失败的情况?

你听说过“死信队列”吗? - OneCricketeer
2
当然,我熟悉这个概念,这会有所帮助。如果你能读取消息但无法发送响应,你会如何处理这种情况?使用事务来解决吗? - James
1个回答

28
一些你描述的事情并不是错误或异常,而是你在分布式架构中应考虑的替代流程。例如,商品缺货是您业务流程中完全有效的替代流程之一,可能需要人工干预。您可以将消息移动到单独的队列,并提供一些UI,让人操作员处理问题,解决它并导致事件流继续。
您描述的付款问题也可以说类似的事情。如果无法成功结算订单,人工操作员将需要调查案件并解决它。为此,您的设计必须考虑该替代流程,并使人能够在消息最终进入需要人员审核的队列时以某种方式进行干预。
这些情况应与程序抛出的错误或异常区分开来。根据情况,这些情况实际上可能需要将消息移动到死信队列(DLQ),以供工程师查看。
这是一个非常广泛的话题,整本书都可以写关于它。
我认为您可能会从更多了解以下概念中受益:

补偿性事务的思想是,每个阴影都有其阳影:如果您有一个可以下订单的事务,那么您可以使用一个取消该订单的事务来撤销它。这后者的事务称为补偿性事务。因此,如果您执行了一些成功的事务,然后其中一个失败了,您可以追溯您的步骤并补偿您所做的每个成功事务,并因此撤消它们的副作用。

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

一般来说,它意味着当您执行一组交易时,它们的副作用在事务协调器得到确认它们全部成功之前不会生效。例如,如果您在 Expedia 上预订机票,您的飞行有两段路程且由不同的航空公司运营,则一个事务将预订美国航空的航班,另一个事务将预订联合航空的航班。如果您的第二个预订失败,则您想要赔偿第一个预订。但不仅如此,您还想避免在确认两者之前第一个预订生效。因此,最初的事务进行了预订但保留了其副作用 "待确认"。第二个预订也是如此。一旦事务协调器知道所有内容都已预订,它可以向所有方发送确认消息,以便他们确认他们的预订。如果在合理的时间窗口内没有确认预订,则受影响的系统会自动撤销预订。
书籍企业集成模式提供了一些关于如何实现这种类型的事件协调的基本思路(例如,参见进程管理器模式并与路由滑动模式进行比较,这些是微服务世界中的协调与编排类似的思路)。
如您所见,能够补偿事务可能会因分布式工作流的复杂程度而变得复杂。流程管理器可能需要跟踪每个步骤的状态,并知道何时需要撤消整个事务。这基本上就是微服务世界中Saga的概念。
书籍《微服务模式》有一整章叫做“使用Saga管理事务”,详细介绍了如何实现这种类型的解决方案。
我通常还考虑以下几个方面:
幂等性
我认为在分布式系统中成功实现服务事务的关键在于使它们幂等。一旦您可以保证某个服务是幂等的,那么您就可以安全地重试它,而不用担心会引起其他副作用。但仅仅重试失败的事务并不能解决问题。
瞬态错误与持久错误
当涉及到重试服务事务时,您不应该只是因为它失败了就重试。您必须首先知道它失败的原因,根据错误类型可能重试也可能没有意义。某些类型的错误是瞬态的,例如,如果一个事务由于查询超时而失败,那么重试很可能是可以的,第二次尝试它很可能会成功;但如果您收到数据库约束违规错误(例如,因为DBA向字段添加了一个检查约束),那么重试该事务就没有意义:无论您尝试多少次都会失败。

将错误视为替代流程

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

在那些涉及服务间通信(计算机之间的交互)的情况下,当工作流中的某个步骤失败时,您不一定需要撤消之前所做的一切。您可以将错误视为工作流的一部分。分类可能导致错误的原因,并将其作为另一种事件流进行记录,这只需要人工干预即可。这只是完整编排中需要人员介入进行决策、解决数据不一致或批准继续前进的另一步骤。

例如,当您处理订单时,由于资金不足,支付服务失败。因此,撤消其他所有操作没有意义。我们只需要将订单放置在系统中的某个问题解决者可以处理的状态下,一旦修复,就可以继续进行其余的工作流程。

事务和数据模型状态至关重要

我发现这种类型的事务性工作流需要对模型必须经过的不同状态进行良好的设计。就像尝试/取消/确认模式的情况一样,这意味着最初应用副作用,而不一定使数据模型对用户可用。

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

在设计事务粒度时,难点在于如何使系统保持有效状态,即使您的事务工作流程中的一步失败,只要故障原因得到纠正,就可以恢复。

分布式事务工作流程设计

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

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

正如我最初所说,这个主题太广泛了,需要更具体的问题来讨论,也许只需要详细讨论其中一两个方面。

无论如何,我希望这在某种程度上能帮助您进行调查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接