下载本文的PDF版本 PDF

幂等性并非医学疾病

可靠系统的一项基本属性


帕特·海兰德 (Pat Helland)


分布式计算的定义可能会令人困惑。有时,它指的是紧密耦合的计算机集群,它们协同工作,看起来像一台更大的计算机。然而,更常见的情况是,它指的是一群松散相关的应用程序,它们在没有太多系统级支持的情况下相互通信。

分布式计算环境中缺乏这种支持,使得编写协同工作的应用程序变得困难。系统之间发送的消息没有明确的交付保证。消息可能会丢失,因此,超时后会进行重试。通信的另一方可能会收到多条消息,即使本意只发送了一条。这些消息可能会被重新排序,并与其他消息交错。确保应用程序按预期运行,在设计和实现上都非常困难。甚至测试也更加困难。

在一个充满重试消息的世界中,幂等性是可靠系统的一项基本属性。幂等性是一个数学术语,意思是多次执行操作与执行一次操作的效果相同。当消息彼此相关并可能具有顺序约束时,挑战就出现了。消息如何关联?可能出现什么问题?应用程序开发人员如何在不失去灵感的情况下构建正确运行的应用程序?

消息传递环境

本节通过描述正在考虑的应用程序类型来构建问题框架。它着眼于服务以及长时间运行的工作如何在服务之间运行,并考虑在应用程序之间进行消息传递的需求,这些应用程序在一个共享相对简单标准的世界中独立发展。

服务器和服务

本文考虑在面向服务的环境中进行消息传递,该环境由一组共享工作的服务器组成。希望您可以通过在需求增加时添加更多服务器来扩展规模。这就引出了一个问题,即后续消息如何找到一个服务(在服务器上运行),该服务能够记住之前发生的事情。

消息和长时间运行的工作

有时,相关的消息会延迟很久才到达,可能几天或几周之后。这些消息是应用程序尝试跨多台机器、部门或企业工作时执行的同一项工作的一部分。某种程度上,通信应用程序需要具备关联相关消息的必要信息。

即使某些参与者崩溃并重启,可预测和一致的行为对于消息处理至关重要。要么之前的消息无关紧要,要么参与者需要记住它们。这意味着某种形式的持久性,以捕捉早期消息中重要的本质,以便长时间运行的工作可以继续进行。有些系统确实需要来自早期消息的信息来处理后续消息,但没有为跨系统崩溃记住这些信息做出规定。

当然,当系统崩溃介入时出现异常行为,有一个技术术语;它被称为bug(缺陷)。

在不断变化的世界中构建应用程序

随着岁月的流逝,应用程序的形态和形式不断演变,从大型机到小型机,再到个人电脑,再到部门网络。现在,可扩展的云计算网络为实现应用程序提供了新的方式,以支持不断增加的消息传递合作伙伴。

随着实现应用程序的机制发生变化,消息传递的微妙之处也在发生变化。大多数应用程序都致力于使用半稳定的互联网标准来包含与多个合作伙伴的消息传递。这些标准支持不断发展的环境,但无法消除一些可能的异常情况。

想象两个服务之间的对话

本文考虑了两个参与方之间通信的挑战。(本文未涉及其他消息传递模式,例如发布-订阅。)它想象这两个参与方之间任意的消息通信序列彼此相关,并假设这两个程序以某种方式了解通过此对话完成的工作。即使有最好的“消息传递管道”支持,两个服务之间的双向消息传递对话也存在需要解决的复杂性。

管道系统和应用程序

应用程序通常运行在某种形式的“管道系统”之上。除了其他服务外,应用程序的管道系统还提供消息传递方面的帮助。它可能在命名、交付和重试方面提供帮助,并可能提供将消息关联在一起的抽象(例如,请求-响应)。

作为应用程序设计者,您只能理解您在客户端、服务器或移动设备上与管道系统交互时看到的内容。您可以预测或推断更远端发生的事情,但所有这些都由您的本地管道系统进行中介(见图 1)。

身份和关系

当本地应用程序(或服务)决定与另一个服务进行对话时,它必须使用目标合作伙伴的名称。第一个消息的合作伙伴服务的身份指定了与正在执行应用程序工作的服务聊天的意愿,但尚未参与某些联合项目。这与连接到已处理先前消息的同一服务实例截然不同。

当消息在对话中发送时,就建立了关系。处理后续消息意味着合作伙伴记住了之前的消息。这种关系暗示了对话中间的合作伙伴身份,这与对话开始时的新合作伙伴不同。

对话中间身份的表示,以及后续消息如何路由到正确的位置,都属于“管道系统”的一部分。有时管道系统非常薄弱,应用程序开发人员必须提供这些机制,或者管道系统可能并非在所有通信应用程序上都普遍可用,因此应用程序必须深入研究以解决这些问题。现在,让我们假设管道系统相当智能,并看看应用程序可以期望的最佳行为。

消息、数据和事务

当应用程序有一些正在操作的数据(可能在具有事务更新的数据库中)时会发生什么?考虑以下选项

• 消息消费记录在处理之前。这种情况很少见,因为构建可预测的应用程序非常困难。有时,消息被记录为已消费,应用程序开始执行工作,但事务失败。当这种情况发生时,消息实际上根本没有被传递。

• 消息作为数据库事务的一部分被消费。这是应用程序最简单的选择,但并不常见。通常,消息传递系统和数据库系统是分开的。某些系统,例如 SQL 服务代理4

提供此选项(见图 2)。有时,管道系统会将传入消息的消费与应用程序数据库中的更改以及传出消息进行事务性绑定。这使得应用程序更容易,但需要消息传递和数据库之间的紧密协调。

• 消息在处理之后被消费。这是最常见的情况。应用程序必须设计为,每个消息的处理都是幂等的。存在一个故障窗口,在此窗口中,工作已成功应用于数据库,但故障阻止消息传递系统知道消息已被消费。消息传递系统将重新驱动消息的传递。

不要同时说和听

传出消息通常作为提交处理工作的一部分进行排队。它们也可能与数据库更改紧密耦合,或者允许分开。当数据库和消息传递更改通过公共事务绑定在一起时,消息的消费、数据库的更改以及传出消息的入队都绑定在一个事务中(见图 2)。只有在入队传出消息之后,消息才会离开发送系统。允许在事务提交之前离开可能会导致消息被发送但事务中止的可能性。

按序交付:历史还是当前?

在两个合作伙伴之间的通信中,每个方向都有明确的发送顺序的概念。您可能正在向我发送消息,而我正在向您发送消息,这意味着对于相互交错的消息,排序存在模糊性,但一次只在一个特定方向发送一条消息。

侦听器可以轻松指定它不想要乱序消息。如果您按特定顺序向我发送了 n 条不同的消息,当我看到消息 n 时,我真的想看到消息 n-1 吗?如果管道系统允许应用程序看到这种重新排序,那么应用程序很可能必须添加一些额外的协议和处理代码来应对这种混乱。

假设消息总是按顺序到达。有两种合理的应用程序行为

历史(无间隙)。消息不仅会按顺序交付,而且管道系统还会尊重该序列,不允许出现间隙。这意味着如果消息 n-1 丢失,管道系统将不会将对话序列中的消息 n 交付给应用程序。它可能会在底层努力获取消息 n-1,但这对于应用程序是透明的。这对于业务流程工作流和许多其他情况非常有用。丢失消息会使应用程序的设计非常非常困难,因此应用程序不希望出现间隙。

当前(交付最新的——有间隙或无间隙)。有时,填补间隙的延迟比间隙本身更成问题。当观看特定股票的价格或工厂中化学过程的温度计时,您可能很乐意跳过一些中间结果以获得更及时的读数。尽管如此,仍然希望保持顺序,以避免股票价格或温度倒退。

发送消息时知道您不知道什么

当通信应用程序想要请求其合作伙伴完成某些工作时,需要考虑几个阶段

在您发送请求之前。此时,您非常有信心工作尚未完成。

在您发送之后但在收到回复之前。这是困惑的时刻。您完全不知道对方是否做了任何事情。工作可能很快完成,可能已经完成,或者可能永远不会完成。发送请求会增加您的困惑。

在您收到回复之后。现在,您知道了。要么成功了,要么失败了,但您不再那么困惑了。

跨松散耦合的合作伙伴进行消息传递本质上是一种困惑和不确定性的练习。对于应用程序程序员来说,理解消息传递中涉及的歧义非常重要(见图 3)。有趣的语义发生在应用程序与其盒子上的本地管道系统对话时。这就是它所能看到的一切。

我不会永远等待!

每个应用程序都允许感到厌倦并放弃参与工作。让消息传递管道系统跟踪自上次收到消息以来的时间很有用。通常,应用程序会希望指定它只愿意等待一段时间,直到放弃。如果管道系统在这方面提供帮助,那就太好了。如果不是,应用程序将需要自行跟踪它需要的任何超时。

一些应用程序开发人员可能会推动不超时,并认为无限期等待是可以的。我通常建议他们将超时设置为 30 年。反过来,这会产生我需要合理且不要傻气的回应。为什么 30 年很傻,但无限期是合理的?我还没有见过真正想无限期等待的消息传递应用程序……

当您的管道工不理解您时

没有什么比管道工能够减轻您的担忧并使一切“正常工作”更好的了。如果管道工与您对您和您的需求有相同的理解,那就更好了。

许多应用程序只是希望在两个服务之间进行多消息对话,每个服务完成部分工作。我刚刚描述了在您和您的管道工对以下内容有清晰概念的最佳情况下可以期望的内容

• 您应该如何与您的管道系统对话。

• 您正在与另一端的谁对话。

• 事务如何与您的数据(以及传入和传出消息)一起工作。

• 消息是在所有先前消息之后交付,还是您跳过消息以获取最新的消息。

• 发送者何时知道消息已交付,以及何时不明确。

• 服务何时可能放弃通信,以及合作伙伴如何了解它。

• 您如何测试超时。

这些挑战假设您遇到了梦想中的管道工,他为您的消息传递环境实现了出色的支持。很少有如此干净和简单的情况。相反,应用程序开发人员需要注意一系列问题。

保证消息传递

某些消息传递系统提供保证交付。这些系统(例如,MQ-Series)1 通常会在接受消息发送时将消息记录在基于磁盘的队列中。消息的消费(以及从队列中删除)要么发生在由消息激发的事务中,要么仅在事务工作完成后发生。在后一种情况下,如果出现故障,工作可能会被处理两次。

经典保证交付队列系统中的一个挑战发生在应用程序收到无法处理的消息时。保证交付意味着消息传递系统交付了它,但无法保证消息格式正确,或者即使格式正确,暴躁的应用程序是否对消息做了合理的事情。在我的妻子开始处理我们的家庭账单之前,这是我的责任,可靠地将电费账单送到我们家与电力公司收到款项之间只有松散的关联。

零次或多次……保证!

在考虑底层消息传输的行为时,最好记住承诺的内容。每条消息都保证交付零次或多次! 这是一个您可以指望的保证。有一个美丽的概率峰值显示大多数消息被交付一次。

如果您不假设底层传输可能会丢弃或重复消息,那么您的应用程序中将存在潜在的 bug。更有趣的是,在传输之上分层的管道系统可以为您提供多少帮助的问题。如果通信应用程序运行在共享消息传递通用抽象的管道系统之上,则可能存在一些帮助。在大多数环境中,应用程序必须自行应对此问题。

为什么 TCP 不够用?

TCP 对统一我们执行数据通信的方式产生了重大影响。5 它在两个通信进程之间提供精确一次和按序字节交付。一旦连接终止或其中一个进程完成或失败,它就不提供任何保证。这意味着它仅涵盖了松散耦合的分布式系统中构建可靠应用程序的开发人员可见的一小部分领域。实际上,应用程序层位于 TCP 之上,并且必须再次解决许多相同的问题。

请求会丢失,因此几乎每个消息传递系统都会重试传输。消息传递系统通常使用 TCP,TCP 有自己的机制来确保字节从进程到进程的可靠交付。TCP 的保证是真实的,但仅适用于与一个其他进程聊天的单个进程。当涉及寿命更长的参与者时,挑战就出现了。

例如,考虑 HTTP Web 请求。HTTP 通常会关闭请求之间的 TCP 连接。当使用持久 HTTP 连接时,TCP 连接通常保持活动状态,但没有保证。这意味着在 TCP 之上使用 HTTP 可能会导致多次发送 HTTP 请求。因此,大多数 HTTP 请求都是幂等的。3

在可扩展的 Web 服务世界中,我们不断地重新实现与 TCP 中无处不在的相同的滑动窗口协议2。在 TCP 中,端点是正在运行的进程。任一进程的失败都意味着 TCP 连接的失败。在由一组服务器实现的长期运行的消息传递环境中,端点的语义更加复杂。随着端点的表示及其状态的演变,消息传递异常也在演变,希望这些异常能够通过管道系统进行管理。更可能的是,它们会随着令人惊讶的 bug 的补丁而逐渐被应用程序解决。无论如何,即使是应用程序开发人员也需要手边有一本 Andrew Tanenbaum 的经典著作《计算机网络》2

避免在谈论幂等性时感到尴尬

回顾一下,幂等性意味着多次调用某些工作与精确调用一次相同。

• 扫地是幂等的。如果您多次扫地,您仍然会得到干净的地板。

• 提取 10 亿美元不是幂等的。口吃和重试可能会令人恼火。

• 如果尚未处理,则处理 XYZ 提取 10 亿美元是幂等的。

• 烤蛋糕不是幂等的。

• 从购物清单开始烤蛋糕(如果您不关心金钱)是幂等的。

• 读取记录 X 是幂等的。即使该值发生变化,在发出读取和返回答案之间的窗口期内,X 的任何合法值都是正确的。

计算机使用中幂等的定义是:“即使多次使用,也表现得好像只使用了一次。” 虽然这是事实,但多次尝试通常会产生副作用。让我们考虑一些通常不被认为与语义相关的副作用

堆。 想象一个在处理请求期间使用堆的系统。您自然会期望堆可能会随着多个请求而变得更加碎片化。

日志记录和监控。 大多数服务器系统都维护日志,以便分析和监控系统。重复的请求将影响日志的内容和监控统计信息。

性能。 重复的请求可能会消耗计算、网络和/或存储资源。这可能会对系统的吞吐量造成负担。

这些副作用与应用程序行为的语义无关,因此,即使存在副作用,幂等请求的处理仍然被认为是幂等的。

多消息交互的挑战

任何消息都可能多次到达……即使在很长一段时间之后。将消息传递系统想象成包含一群马基雅维利式的小矮人,他们正在监视您的消息漂浮,以便他们可以在应用程序最糟糕的时候插入消息的副本(见图 4)。在大多数松散耦合的系统中,消息可能会多次到达。此外,相关消息可能会乱序交付。

状态和多消息交互

解决此问题的典型方法是使用请求-响应,然后确保处理的消息是幂等的。这样做的好处是应用程序可以通过肯定的响应看到消息的交付。如果应用程序从其预期合作伙伴那里收到响应,那么它确信消息实际上已到达。由于重试,接收应用程序可能会多次收到消息。

当多个消息参与完成一项长时间运行的工作时,这种挑战会进一步加剧。必须考虑工作的持续时间和工作允许的失败次数,如下列情况所示

轻量级进程状态。 有时,共享计算任一端的进程失败可能会导致整个工作流程被放弃。如果是这种情况,则中间状态可以保存在正在运行的进程中。

长时间运行的持久状态。 在这种情况下,即使其中一个合作伙伴系统崩溃并重启,长时间运行的工作也必须继续。这对于可能持续数天或数周但仍具有有意义的消息传递交互的业务流程很常见。

无状态应用程序服务器。 许多架构将计算与状态分离。状态可以保存在单个服务器上的持久数据库中,在多个持久数据存储中复制,或者以任何其他新颖的方式存储。

在所有这些情况下,系统都必须在将状态(这是早期消息记忆的结果)与新消息结合在一起时表现正确。消息可能会以不同的顺序合并到状态中,并且这些顺序可能会受到一个或多个系统失败的影响。

你不是我想象中的那个人

许多系统使用负载均衡服务器池来实现目标应用程序。当传入消息不对先前的通信和先前的状态做任何假设时,这种方法效果良好。第一个消息被路由到其中一个服务器并被处理(见图 5)。当与服务 Foo 通信时,多台机器可能正在实现该服务。这给服务 Foo 的实现带来了有趣的负担,并且可能会在与通信合作伙伴可见的异常行为中显现出来。

当第二个(或后续)消息到达并期望合作伙伴不会失忆时,可能会出现挑战。当通过多个消息聊天时,您假设您的合作伙伴记住了之前的消息。多消息对话的整个概念都基于此。

你是什么意思,工作不是在场所完成的?

当服务 A 与服务 B 通信时,您不知道工作真正是在哪里完成的。服务 A 认为它正在与服务 B 一起工作,而服务 B 实际上可能会将所有工作外包给服务 C(见图 6)。这本身并不是问题,但它可能会放大服务 A 看到的失败可能性。

您不能假设工作实际上是由与您聊天的系统完成的。您只知道您有一个消息传递协议,并且,如果一切顺利,根据协议的定义,适当的消息会从指定的合作伙伴返回。您根本不知道幕后发生了什么。

ACK 意味着“停止重复你自己”

只有当执行工作的合作伙伴返回答案时,您才知道消息已交付。跟踪中间航点无助于了解工作是否会完成。知道 FedEx 包裹已到达孟菲斯并不能告诉您您的祖母会收到她的巧克力盒。

当消息传输向发送者发送 ACK 时,这意味着消息已在下一台机器上收到。它没有说明消息实际交付到目的地,更不用说应用程序可能对消息进行的任何处理。如果存在将工作分包给另一个应用程序服务的中间应用程序,则情况会更加复杂(见图 7)。传输和管道系统确认对于应用程序不可见,否则当重新配置目标服务时可能会引入 bug。ACK 告诉服务 A 的管道系统服务 B 的管道系统已收到消息,但没有告诉服务 A 关于服务 C 收到消息的任何信息。服务 A 不得根据 ACK 行事。

ACK 意味着再次发送消息无济于事。 如果发送应用程序意识到 ACK 并根据该知识采取行动,那么当实际工作未能实现时,可能会导致 bug。

管道系统可以掩盖合作伙伴关系的歧义(但很少这样做)

消息传递交付管道系统有可能对长时间运行的对话有一个正式的概念。管道系统必须定义和实现以下内容

状态。 应用程序如何表示来自部分完成的对话的状态信息?请记住,状态必须在组件故障中幸存下来,否则对话必须因故障而干净地失败——仅仅忘记早期消息是不好的。

路由。 后续消息如何找到可以定位状态的服务(和服务器)并正确处理新消息,而不会忘记早期消息?

对话语义。 即使对话的实际工作外包给遥远的他方,对话如何提供正确的语义?这部分是正确地掩盖传输问题(例如 ACK),如果应用程序看到这些问题,只会引起问题。

因此,消息传递语义似乎与命名、路由和状态管理密切相关。这不可避免地意味着,当数据库来自与消息传递系统不同的管道系统供应商时,应用程序设计者将面临挑战。SQL 服务代理是提供清晰对话语义的一个系统。4 它通过将消息传递和对话状态保存在 SQL 数据库中来实现这一点。

与服务对话

服务被设计为黑盒子。您知道服务地址,开始使用它,来回通信,然后完成。事实证明,即使在某些管道系统的支持下为您提供对话,重复消息和丢失消息也存在问题。当目标服务以可扩展的方式实现时,这些挑战会更加复杂。当然,这意味着您可能会开始与尚未可扩展的服务交互,并且随着它的增长,某些新的障碍可能会出现。

对话的阶段

对话在其生命周期中经历三个阶段

发起。 消息从对话发起者发送到对话的目标。发起者不知道目标是作为单个服务器还是负载均衡池实现的。

已建立。 消息可以在双向全双工方式中流动。消息传递系统现在已确保多个消息将落在负载均衡池中的同一服务器上,或者处理消息的服务器将准确访问从先前消息中记住的状态(并表现得好像它是同一服务器)。

关闭。 发送最后一个消息(或同一方向上一起流动的消息)。

通信的每个阶段都带来了挑战,尤其是发起和关闭阶段。

发起阶段的歧义

当对话中的第一个消息发送时,它可能需要或不需要重试。在负载均衡服务器环境中,两次重试可能会落在不同的后端服务器上并被独立处理。

从发送者的角度来看,应用程序将消息投入管道系统,并使用某个名称,希望该名称能将其发送给所需的合作伙伴。在您听到响应之前,您无法判断消息是否已收到、消息是否丢失、响应是否丢失或工作是否仍在进行中。请求必须是幂等的(见图 8)。

发送给服务的第一个消息必须是幂等的,因为它可能会被重试以应对传输故障。后续消息可以依靠某些管道系统的帮助(前提是应用程序运行在管道系统之上)。在第一个消息之后,管道系统可以充分了解消息的目的地(在可扩展系统中),以执行自动重复消除。在处理第一个消息期间,重试可能会落在可扩展服务的不同部分,然后自动重复消除是不可能的。

我认为我的邪恶双胞胎收到了你的第一条消息

有时,服务器会收到来自对话的第一个消息(或多个消息),然后消息(或消息序列)的重试会被重新路由到负载均衡池中的另一台服务器。这可能会在两个方面出错:

• 消息协议旨在将状态保存在目标服务器中,并使用池内服务器的更详细地址响应发起者,以便后续消息使用。

• 会话状态与负载均衡服务器分开保存,协议中的后续消息包含会话身份信息,这允许无状态负载均衡器获取会话状态并从协议中断的地方继续。

这两种方法都同样受到重试的挑战。第一次尝试将与作为重试结果发生的第二次尝试无关。这意味着对话协议中的初始化消息必须是幂等的(参见图 9)。

当发起服务向负载均衡服务发送消息序列时,第一个消息必须是幂等的。考虑以下事件:

1. 消息序列(1、2 和 3)被发送到服务 Foo,负载均衡器选择 Foo A。

2. 消息的工作在 Foo A 上执行。

3. 答案被发回,指示未来的消息应以 Foo A 作为已解析的名称为目标。不幸的是,回复因不稳定的网络传输而丢失。

4. 发生对服务 Foo 的重试,消息被发送到服务器 Foo N。

5. 消息 1、2 和 3 的工作在服务器 Foo N 上执行。

6. 响应被发回给发起者,发起者现在知道要继续与 Foo N 对话。

我们必须以某种方式确保在 Foo A 上执行的冗余工作不会成为问题。

准确地结束初始化阶段

当从本地管道返回消息时,应用程序知道它已到达特定的合作伙伴。如果另一个服务已在对话中响应,则对话存在特定的服务器或绑定的会话状态。虽然应用程序可能没有直接看到与合作伙伴中特定资源的绑定,但当看到应用程序响应时,它们肯定已被连接。

在应用程序从其本地管道收到合作伙伴应用程序的消息之前,发送的任何消息都可能被重新路由到合作伙伴的新(且健忘的)实现。

只有在您听到来自另一侧应用程序的消息后,您才可以退出对话的初始化阶段(参见图 10)。应用程序只能看到其管道向其显示的内容。当应用程序开始向其合作伙伴发送消息时,它具有初始化阶段的模糊性,并且必须确保所有消息都具有用于幂等处理的语义。当本地管道从合作伙伴返回消息时,本地应用程序可以确信管道已解决初始化阶段的模糊性,现在可以发送消息,而无需确保它们是幂等的。

保证第一个消息的幂等性

在第一次回答之前,您在对话中作为应用程序所说的一切都可能被重试。如果早期消息导致发生一些严重(且非幂等)的工作,那可能会引起很多麻烦。应用程序开发人员该怎么办?

有三种广泛的方法可以确保您在初始化阶段没有错误:

琐碎的工作。 您可以简单地来回发送消息,除了建立与可扩展服务器中特定合作伙伴的对话之外,什么也不做。这就是 TCP 对其 SYN 消息所做的事情。

只读工作。 发起应用程序可以从可扩展服务器读取一些内容。如果发生重试,这不会留下混乱。

待处理的工作。 发起应用程序可以发送一堆东西,合作伙伴将累积这些东西。只有在随后的往返发生(并且您知道哪个特定合作伙伴已连接以及对话的实际状态)之后,累积的状态才会被永久应用。如果服务器累积状态并超时,则可以丢弃累积的状态而不会引入错误。

使用这三种方法之一,应用程序开发人员(而不是管道)将确保没有错误潜伏在等待对不同后端合作伙伴的重试中。为了完全消除这种风险,管道可以使用 TCP 的技巧,发送一组琐碎的往返消息(TCP 中的 SYN 消息),这会在不打扰顶层应用程序的情况下连接合作伙伴。另一方面,允许应用程序通过往返执行有用的工作(例如,读取一些数据)是很酷的。

关闭阶段的模糊性

在任何交互中,无法保证从一个应用程序服务到另一个应用程序服务的最后一个消息。知道它被接收的唯一方法是发送一条消息说它被接收了。这意味着它不再是最后一个消息。

不知何故,应用程序必须处理在同一方向发送的最后一个或多个消息可能只是在网络中消失的事实。这可能是在简单的请求-响应中,也可能是在复杂的全双工聊天中。当发送最后一个消息时,它们是否真的被传递只是运气问题(参见图 11)。在两个合作伙伴之间的每次交互中,在同一方向发送的最后一个消息都无法保证。它们可能 просто 消失。

倒数第二个混乱

倒数第二个消息可以得到保证(通过接收最终消息中的通知)。最终消息必须是尽力而为的。这种复杂性通常是应用程序协议设计的一部分。应用程序实际上不必关心是否收到最后一个消息,因为您无法真正知道它是否被收到。

与幂等性共存

正如我们所见,大多数松散耦合的系统都依赖于应用程序设计人员来考虑请求的重复处理。在服务的可扩展实现中,复杂性变得更加糟糕。应用程序设计人员必须学会在其日常生活中与幂等性的现实共存。

结论

分布式系统可能会给发送消息的应用程序带来挑战。消息传输可能非常调皮。消息的目标可能是由一组工蜂实现的合作伙伴的幻觉。反过来,这些工蜂可能在协调您的工作状态方面面临挑战。此外,您认为您正在与之交谈的系统实际上可能将工作分包给其他系统。这也可能会增加混乱。

有时,应用程序具有捕获其通信合作伙伴模型、合作伙伴生命周期、可伸缩性、故障管理以及在通信应用程序组件之间进行良好双向对话所需的所有问题的管道。即使在强大的支持管道存在的情况下,消息传递仍然存在固有的语义挑战。

本文概述了一些经验丰富的老手使用的原则,即使在“事情发生”时也能提供弹性。在大多数情况下,这些编程技术在生产中出现罕见异常时被用作应用程序的补丁。总的来说,它们很少被提及,也很少在测试中出现。它们通常发生在应用程序承受最大压力时(这可能是意识到您有问题时代价最高的时刻)。

一些基本原则是:

• 每个消息都可能被重试,因此,必须是幂等的。

• 消息可能会被重新排序。

• 您的合作伙伴可能会因故障、管理不善的持久状态或负载均衡切换到其邪恶双胞胎而失忆。

• 无法保证最后一个消息的交付。

牢记这些原则可以带来更强大的应用程序。

虽然管道或平台有可能从应用程序中消除其中一些担忧,但这只有在通信应用程序共享通用管道时才会发生。这种通用环境的出现并非迫在眉睫(甚至可能永远不会发生)。与此同时,开发人员在构建应用程序时需要对这些潜在的故障保持警惕。

致谢

感谢 Erik Meijer 和 Jim Maurer 的评论和编辑改进。

参考文献

1. IBM. WebSphere MQ; http://www-01.ibm.com/software/integration/wmq/.

2. Tanenbaum, A. S. 2002. 计算机网络, 第 4 版。Prentice Hall。

3. 万维网联盟,网络工作组。1999. 超文本传输​​协议 – HTTP1.1; http://www.w3.org/Protocols/rfc2616/rfc2616.html.

4. Wolter, R. 2005. SQL Server Service Broker 简介; http://msdn.microsoft.com/en-us/library/ms345108(v=sql.90).aspx.

5. 传输控制协议; http://www.ietf.org/rfc/rfc793.txt

喜欢或讨厌?请告诉我们

[email protected]

PAT HELLAND 自 1978 年以来一直从事分布式系统、事务处理、数据库和类似领域的工作。在 20 世纪 80 年代的大部分时间里,他是 Tandem Computers 的 TMF(事务监控工具)的首席架构师。除了在亚马逊工作过两年之外,Helland 从 1994 年到 2011 年在微软工作,在那里他是 Microsoft Transaction Server 和 SQL Service Broker 的架构师。他还为 Cosmos 做出了贡献,Cosmos 是一个分布式计算和存储系统,为 Bing 提供后端支持。他于 2011 年离开微软,搬到旧金山,离家人更近。这让他有更多时间为撰写文章。

© 2012 1542-7730/12/0400 $10.00

© 2012 1542-7730/12/0300 $10.00

acmqueue

最初发表于 Queue vol. 10, no. 4
数字图书馆 中评论本文





更多相关文章

Shylaja Nukala, Vivek Rau - 为什么 SRE 文档很重要
SRE(站点可靠性工程)是一种职位职能、一种思维模式以及一套工程方法,用于使 Web 产品和服务可靠地运行。 SRE 在软件开发和系统工程的交叉点运作,以解决运营问题并设计解决方案,从而可扩展、可靠且高效地设计、构建和运行大规模分布式系统。成熟的 SRE 团队可能拥有与许多 SRE 职能相关的明确定义的文档体系。


Taylor Savage - 组件化 Web
在当今的软件工程中,没有哪项任务比 Web 开发更艰巨。 Web 应用程序的典型规范可能写为:该应用程序必须跨各种浏览器工作。它必须以 60 fps 的速度运行动画。它必须立即响应触摸。它必须符合特定的一组设计原则和规范。它必须在几乎所有可以想象到的屏幕尺寸上工作,从电视和 30 英寸显示器到手机和手表表面。它必须在长期内得到良好的工程设计和可维护性。


Arie van Deursen - 超越页面对象:使用状态对象测试 Web 应用程序
Web 应用程序的端到端测试通常涉及通过诸如 Selenium WebDriver 之类的框架与 Web 页面进行棘手的交互。隐藏此类 Web 页面复杂性的推荐方法是使用页面对象,但是首先要回答一些问题:在测试 Web 应用程序时应该创建哪些页面对象?您应该在页面对象中包含哪些操作?给定您的页面对象,您应该指定哪些测试场景?


Rich Harris - 消除准入壁垒
一场战争正在 Web 开发世界中进行。一方是工具制造者和工具用户的先锋,他们以破坏糟糕的旧观念(在这个环境中,“旧”意味着任何在一个月前以上在 Hacker News 上首次亮相的东西)以及关于转译器等的热烈辩论为乐。





© 保留所有权利。

© . All rights reserved.