微服务:处理最终一致性


22

假设我们有一个更新用户密码的功能。

单击“更新密码”按钮后,会将UpdatePasswordEvent发送到预订了3个其他服务的主题:

  1. 实际更新用户密码的服务
  2. 一种更新用户密码历史记录的服务
  3. 发出电子邮件通知用户其密码已更改的服务。

根据我对最终一致性的了解,所有这些服务(消费者)将同时接收事件并分别处理它们,在良好的情况下,这将导致数据保持一致。

但是,如果服务无法处理事件怎么办?例如突然断开连接,数据库错误等...处理这些事务失败的好的模式/做法是什么?

我当时正在考虑创建一个RollbackTopic,如果任何事件都无法处理,将在一个主题中创建一个RollbackEvent,其中“ rollback services”将完成其工作并将数据还原


11
您无法撤消已发送的电子邮件:-)
Laiv

2
因为它们都应该属于同一服务。微服务与整体相反,这并不意味着您必须尽可能“物理地”设计它们。尽管这并不直接相关,但您应该阅读此问题以及两个最高答案:softwareengineering.stackexchange.com/questions/339230/…–
Walfrat

1
您可能希望考虑同步更新数据库中的用户密码,以便向用户提供即时反馈,并通过发出有关主题密码已更改的消息来异步触发其他服务,因此您的消息不必包含密码。
cr3

是用于告知用户交易已完成的电子邮件,还是用于告知用户某人(希望他们)已更改密码的电子邮件?“如果不是您,那么您需要采取行动”。如果是第二个,那么现在就尽力发送电子邮件。
ctrl-alt-delor '18

Answers:


29

根据我对最终一致性的了解,所有这些服务(消费者)将同时接收事件并分别进行处理,在良好的情况下,这将导致数据保持一致。

不,不一定。正如我所评论的,我们无法撤消已发送的电子邮件,因此我们仍然需要某种“顺序”。IPC事件驱动型数据管理不免除orchestation的1

例如,除非先前的交易成功完成并且电子邮件服务得到证明,否则不应发送电子邮件。3

但是,如果服务无法处理事件怎么办?例如突然断开连接,数据库错误等...处理这些事务失败的好的模式/做法是什么?

向分布式计算谬论问好。它们使事情变得复杂,并且像往常一样,没有处理问题的灵丹妙药。

在开始寻找失落的方舟之前,我们必须考虑首先询问组织。通常,解决方案是组织如何在现实世界中面对这些问题

当某些数据丢失或不完整时,每个人(部门)会做什么?

我们将认识到,不同部门具有不同的解决方案,这些解决方案总共构成了要实施的解决方案。

无论如何,这里有一些实践可以帮助我们制定后续策略。

最终一致性

与其确保系统始终处于一致状态,不如说我们可以接受该系统将在将来的某个时刻得到它。这种方法对于长期的业务运营特别有用。

系统达到一致性的方式因系统而异。它可能涉及从自动化过程到某种人工干预。例如,典型的稍后再试与客户服务联系

中止所有操作

通过补偿交易使系统回到一致状态。但是,我们必须考虑到,这些事务也可能失败,这可能导致我们难以解决不一致问题。同样,我们无法撤消已发送的电子邮件。

对于少量事务,此方法是可行的,因为补偿事务的数量也很少。如果IPC中涉及几笔业务交易,那么为每笔交易处理一项补偿交易将是一个挑战。

如果我们进行补偿交易,我们会发现断路器设计模式非常有用- 而且我敢说强制性的 -

分布式交易

这个想法是通过一个称为“ 事务管理器”的总体管理流程,在单个事务中涵盖多个事务。处理分布式事务的常见算法是两阶段提交

分布式事务的主要关注点在于它们依赖于在其生命周期内锁定资源,而且我们知道,事务管理器也会出错。

如果事务管理器遭到破坏,我们可能会在不同的有界上下文中最终得到几个锁,由于消息的排队而导致意外行为。2

分解操作。为什么?

如果要分解现有系统,并找到确实希望在单个事务边界内的概念集合,则可以将它们保留到最后。

山姆·纽曼

与上述论点一致,Sam在他的《Building Microservices》一书中指出,如果我们确实无法承担最终的一致性,那么我们现在应该避免拆分操作。

如果我们不能将某些操作分成两个或多个事务,则可能会说-可能-这些事务属于相同的有界上下文,或者至少-属于尚待建模的跨领域上下文。

例如,在我们的案例中,我们开始意识到事务#1和#2彼此紧密相关,并且很可能两者都可以属于同一有界上下文AccountsUsersRegister等等。

考虑将两个操作置于同一事务的边界内。这将使整个操作更容易处理。还可以衡量每个交易的关键程度。可能的是,如果事务#2失败,则不应损害整个操作。如有疑问,请向组织询问。


1:不是您想的那种业务流程。我不是在谈论ESB的编排。我说的是使服务对适当的事件做出反应。

2:您可能会发现Sam Newman关于分布式事务的有趣观点

3:请查看David Parker关于此主题的答案。


3
很好的答案。我只强调考虑使用分布式事务时所带来的风险的重要性,这些风险主要是资源锁定产生死锁和系统停机。在大约3年前我从事的电子商务产品上,我们必须用消息传递系统替换DT,因为系统中的可用用户数量很大,因此该系统很容易出错。DT问题通常在用户群增长时发生。
安迪

7

就您而言,您不能同时处理所有三件事。您需要的是一个过程。这是一个极其简化的示例:

命令和事件编排

重要的是要知道状态更改操作必须始终在一致的实体上进行。除非您可以保证强烈的一致性,否则必须在主记录中进行。

您的系统应保证在发生系统更改中的任何事件之前,必须首先确保事务安全。这是为了确保引发的事件确实是对实际发生情况的确认。

该过程有几个棘手的部分,而我将忽略这些显而易见的部分-例如:如果在用更改的密码来保留用户时数据库服务器死了怎么办?您只需再次发出UpdatePassword。但是,您需要照顾一些部分,这些部分是:

  • 处理消息重复,
  • 处理电子邮件发送。

在一个系统中,流程协调器(PO)就是另一个实体,它包含内部状态(在字面上也包含内部状态),并且允许状态之间的转换,有效地充当某种状态机。由于内部状态,您可以删除消息重复处理。

当PO处于一种New状态并正在处理时UserPasswordHasBeenUpdated,它会将其状态更改为UserPasswordHasBeenUpdated(或任何适合您的状态名称)。如果该PO仍在UserPasswordHasBeenUpdated且另一个UserPasswordHasBeenUpdated将到达,则该PO将知道该消息是重复项,而将完全忽略该消息。其他州也将实施类似的机制。

处理电子邮件的实际发送有些棘手。在这里,您有两个选择:

  1. 最多只能发送一次
  2. 至少发送一次。

最多发送一次

使用此选项,当PO到达UserPasswordHistoryHasBeenSaved状态时,将发送一个发送电子邮件的命令作为对状态更改的响应。您的系统将确保UserPasswordHistoryHasBeenSaved在发送电子邮件之前状态将一直保持不变,即重复的消息不会触发电子邮件的再次发送。使用这种方法,可以确保为PO保存正确的状态,但不能保证后续的任何操作。

至少发送一次

这就是我想要的。

UserPasswordHistoryHasBeenSaved您可以尝试先发送电子邮件,而不是保存并发送电子邮件作为对邮件的响应。如果发送操作失败,则PO的状态永远不会更改为,UserPasswordHistoryHasBeenSaved并且仍会处理相同类型的另一条消息。如果发送电子邮件实际上可以成功,但是在PO保持新UserPasswordHistoryHasBeenSaved状态的过程中您的系统将失败,则发送的另一条消息UserPasswordHistoryHasBeenSaved将再次触发发送电子邮件的命令,并且用户将多次收到该电子邮件。

在您的情况下,您要确保用户确实收到了电子邮件。这就是为什么我会选择第一个而不是第二个的原因。


2

排队系统并不像您想象的那样脆弱。

如果我们将所有三个进程都写到一个关系数据库中,那么我们可能会使用一个事务来处理中间进程故障。

没有最后的提交,部分工作将被丢弃。

在基于队列的系统中,当您从队列中读取一条消息来处理中间进程失败时,您将具有类似的选项。

例如,Amazon SQS仅隐藏已读取的消息。除非发送了最后的Delete命令,否则该消息将重新出现或放入死信队列中。

您可以通过多种方式实现类似的“交易”,实质上是保留消息的副本,直到收到成功处理的确认。如果未及时收到确认。您可以再次发送该消息或保留它以进行手动关注。

潜在地,您可以创建一个“回滚服务”来监视这些错误消息,了解相关消息和过去状态并执行回滚。

然而!通常最好重新发送错误的消息。毕竟,这些往往是边缘情况。服务器灾难性地发生故障,或者在处理特定消息类型时存在错误。

一旦收到错误提示,便可以修复服务并成功处理消息。使系统整体回到一致状态。


2

您在这里面临的是两个将军问题。本质上:如何确定收到一条消息并对该消息做出响应?在许多情况下,不存在完美的解决方案。实际上,在分布式系统中,通常不可能一次发送一次准确的消息。

第一个明显的说法是,更改密码的服务应该发送密码更改事件。这样,仅在密码实际更改时才触发密码历史记录和邮件发送服务,而不管密码为何更改。

为了真正解决您的问题,我将不考虑分布式事务,而是朝着至少一次消息传递和幂等处理的方向进行。

  • 至少一次

    为了确保所有用户实际上都能看到密码更改事件,您需要使用一个持久的通信通道,在该通道中可以“至少一次”使用消息。使用者仅在完全处理完消息后才确认消息已被使用。例如,如果密码历史记录服务在写入历史记录条目时崩溃,它将在重新启动后重新读取相同的密码更改事件,然后重试,并在将其自身写入历史记录后将该事件确认为只读。您应根据其重新发送消息直到确认消息的能力来选择消息队列解决方案。

  • 幂等

    在实现了至少一次发送之后,存在一个问题,即在消费者中断之前先对消息进行部分处理,然后再进行后续处理,则会发生重复操作。应该通过设计每个服务使其幂等来解决。它执行的写操作可以多次发生而不会产生不利影响,或者可以保留自己执行的操作的存储,并避免执行一次以上的操作。对于邮件发送,您会发现尝试使其具有幂等的表现可能不值得,并且偶尔发送两次邮件也可以。

无论如何,请注意您提供服务的方式。您的密码历史记录服务是否真的需要独立于密码更改服务?


1

我不同意很多答案。

  1. 立即发送电子邮件“有人更改了您的密码。如果是您,那么您无需执行任何操作。如果没有慌张,它将在到达时到达。
  2. 修改密码。尽管您最终有一致性。您要确保此会话看到用户所做的更改。

您还可以添加其他一致性保证。

  • 确保更改按时间顺序进行。
  • 确保用户永远不会看到回滚,但其他用户可能仍然看不到更改。
  • 还有其他

这些额外的一致性将需要根据应用程序的行为来实现。


我不知道您所说的“更新历史”是什么意思,但请不要更改历史。如果您只是扩展DAG,那么这将导致当前状态的更改。他们不是独立的。如果是这样,那么您就不能依靠历史来反映发生了什么。(最后但并非最不重要的一点,不要存储密码,请参阅如何不存储密码


如果您可以在一开始就发送电子邮件,那么您的方法很好。如果您必须与电子邮件一起发送邮件。也许某种链接/数据只能在达到一致性之后才能获得,因此您不能先发送电子邮件。那就是我的评论consider asking the organization first.。你可能是对的。但是,我发现限制那些我们不能撤消的事件很重要。例如,通知最终用户。位于用户数据真实状态的通知可能会给人留下不好的印象。
Laiv

就是说,对于这种特定情况(密码更改通知),我同意这种方法。一旦满足要求。
Laiv
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.