消息队列大指南

2021-01-01 08:09:14

消息队列现在相当普遍-出现的消息队列是如此之快,以至于您认为它们是芹菜供应量无限的兔子,从而导致卡夫卡式风格的情况,做出决定就像试图抓住一条小溪你的双手。如果只有很少的简单服务可以帮助发布和订阅,那么做出零努力的选择就会容易得多😕

无论我们是按原样使用它们来在应用程序的各个部分之间移动数据,还是作为体系结构的组成部分(如事件驱动的系统),消息队列都将保留下来。从某种意义上说,他们一直在这里-只是名字不多。但是他们是什么?为什么有用?以及我们如何有效地使用它们?我们选择哪种实现?我们使用哪一个都不重要?我们是否需要分别学习它们,还是有适用于所有消息队列的更一般的概念?

扇出和扇入的模式:将一条消息传递到多个系统,或将消息从许多系统组合成一个。

消息队列是在两个系统之间传输信息的一种方式。此信息(一条消息)可以是数据,元数据,信号或这三者的组合。发送和接收消息的系统可以是同一台计算机上的进程,同一应用程序的模块,可能在不同计算机或技术堆栈上运行但可能完全不同种类的系统上运行的服务,例如将信息从软件传输到电子邮件或手机网络上的SMS。

消息传递系统的想法已经存在了很长的时间,从用于在人员或办公室部门之间移动信息的消息框(实际上是收件箱和发件箱的字眼来自哪里),电报到您当地的邮政或快递服务。物理世界中最接近我们计算能力的消息传递系统可能是气动管道,这些管道利用压缩空气将消息传递通过建筑物和城市,直到几十年前(如今仍在某些地方使用)。

我们今天传输的消息类型可能表明发生了某些技术问题,例如CPU使用率超出限制;或感兴趣的商业事件,例如客户下订单;或信号,例如告诉其他服务执行某项操作的命令。每条消息的内容将完全由您的应用程序的体系结构及其用途来决定-因此,在本指南的其余部分中,我们不必担心消息中的内容-我们;更关注消息如何从消息起源的系统(生产者,源,发布者或发送者)到达应该发送到的系统(消费者,订户,目的地或接收者) )。

我们需要消息队列,因为没有系统存在或无法孤立地工作-所有系统都需要以它们都可以理解的结构化方式并以它们都可以处理的受控速度与其他系统通信。任何非平凡的流程都需要一种在流程的各个阶段之间移动信息的方法。任何工作流程都需要一种在该工作流程的各个阶段之间移动中间产品的方法。消息队列是处理此移动的好方法。有很多方法可以使用API​​调用,文件系统或许多其他对事物自然顺序的滥用来获取这些消息。但是所有这些都是消息队列的临时实现,有时我们拒绝承认我们需要这些实现。

消息队列的最简单思维模型是一个很长的管,您可以将它扔进去。您将您的消息写在一个球上,将其滚动到管子中,另一端收到某人或其他东西。该模型有很多有趣的好处,其中包括:

我们不必担心接收消息的是谁或什么,这使发件人不必担心。

我们不必担心接收方何时接收消息。

我们可以按照自己喜欢的速度将任意数量的消息放入管道中(假设我们有无限长的管道)。

接收者永远不会受到我们行动的影响-他们将以他们希望的任何速率拉出任意数量的消息。

发送方和接收方都不关心对方的容量或负载。

两个系统都不关心另一个系统的位置–它们可能位于或不在同一台计算机,网络,大陆或同一星球上。

所有这些优点(甚至不是详尽的清单)在软件开发中都具有非常重要的好处-它们的共同点是去耦。一个系统在职责,时间,带宽,内部工作方式,负载和地理位置方面与另一个系统是分离的。解耦是任何分布式或复杂系统中非常需要的部分-系统各部分之间的解耦越多,独立构建,测试,运行,维护和扩展它们就越容易。

大多数系统也可以与其他外部或第三方系统进行交互-如果我们建立购物网站,则可能会与支付处理器进行交互,并且假设我们尝试在每次用户点击时直接与支付处理器进行通信。如果我们的系统负载过重,我们还会使另一个系统承受相同的负载。反之亦然-如果我们的付款提供商需要向我们发送数百万条有关过去付款方式的信息,那么我们的系统就更好了。这两个系统现在已耦合。一个系统做出的决定和动作会对另一个系统产生重大影响,因此在做出每个决定时都必须考虑到两者的需求。将足够多的其他系统(例如物流或交付系统)添加到组合中,我们很快就会陷入瘫痪状态,这使得根本无法决定任何事情。如果一个系统发生故障,则其他系统也有效地发生故障,而它们本身没有故障。

如果我们想将这些系统中的任何一个切换为另一个系统,例如新的付款处理器或交付系统,也会遇到麻烦。我们必须在应用程序的多个位置进行深刻的更改,构建代码以在多个提供商之间拆分消息甚至更加困难。我们可能需要使用比率来负载均衡它们或按地理位置拆分它们;或根据每个提供商的可用性或成本在它们之间动态切换。

消息队列提供了解决许多这些问题的解耦功能。如果我们在两个需要相互通信的系统之间建立一个队列,那么他们现在就可以继续工作而不必担心彼此—我们将针对任何系统的消息放入队列中,我们期望获得信息也可以通过另一个队列来访问我们。现在,我们有了明确的要点,可以在其中添加规则或进行所需的更改,而无需系统了解或关心不同之处。

消息队列是计算的圣杯吗?他们是否解决了世界上所有的问题?不,当然不。在很多情况下,我们可能不想使用它们。而且我们当然不希望仅仅因为我们有一个容易使用的队列而认为使用队列很有趣。有些系统真的很简单,只是不需要它—消息队列是一种降低通信系统复杂性的方法,但是两个通信系统总是比一个不需要通信系统更复杂。必须沟通。如果您的系统非常简单,不需要与其他任何人进行通信,那么根本就没有任何理由排队。

也有一些系统可以互相通信,但是这些通信所增加的复杂性微不足道,因此不必担心。从某种意义上说,它们都需要协同工作才能正常运行,或者更经常地,系统已经耦合。一个真正常见的示例是应用服务器和数据服务(在OLTP系统中)。将它们与队列解耦没有多大意义,因为如果没有另一个的直接参与,这两个都无法做任何有用的事情。

然后,还要考虑性能-将两个系统在时间和负载方面解耦的重点是,以便它们可以按照自己的进度处理信息-但我们当然不希望这种情况在性能敏感的情况下发生应用程序或实时系统。队列可以帮助我们同时处理更多的工作(接收者可能有许多并行处理您发送的消息的工作),但会消除我们对每项工作所需的确切时间的保证。如果可预测性比吞吐量更重要,那么我们最好不要排队。

使用队列可能会增加处理每条单个消息所花费的时间,但是将允许您在不同的计算机上同时处理更多的消息-因此,每分钟或每小时处理的消息总数或吞吐量将增加。

如果确实有多个需要通信的系统,并且通信需要持久(如果将消息放入队列中,我们要确保消息传递系统不会“忘记”它)并且解耦后,消息队列必不可少。

在没有阅读和/或争论传递保证和语义的情况下,根本无法了解消息队列,因此我们也可以很快地了解它。建立消息队列的人将声称他们的系统提供了三种传递保证之一,即您放入队列中的每条消息都将被传递:

这保证了我们正在使用该软件将对我们系统的设计和工作产生巨大影响,因此让我们将它们中的每一个都拆包。

这是最常见的交付机制,也是最简单的推理和实施机制。如果我有一条要给您的消息,我会给您朗读,并不断反复,直到您确认为止。就是这样。在至少一次运行的系统中,这意味着当您从队列中收到一条消息并且不删除/确认该消息时,您将来会再次收到它,并将继续接收它直到您明确删除/确认它为止。

这是最常见的保证,是因为它很简单并且可以100%地完成工作-不会出现消息丢失的情况。即使接收器在确认消息之前崩溃了,它也只会再次接收相同的消息。不利的一面是,作为接收方的您需要计划多次接收同一条消息,即使您不一定遇到崩溃也是如此。这是因为至少提供一次是保护排队服务也不会丢失消息的最简单方法-如果您的确认没有通过网络到达排队系统,则消息将再次发送。如果仍然存在您的确认问题,该消息将再次发送。如果排队系统在重新启动之前可以正确跟踪已发送给您的内容,则该消息将再次发送。在任何方面出现任何问题的情况下,再次发送消息的简单补救措施就是使此保证如此可靠。

但是消息重复/重复是一个问题吗?这实际上取决于您和您的应用程序或用例。例如,如果该消息是时间戳和度量,则接收一百万个副本没有问题。但是,如果您根据消息转移资金,那肯定是一个问题。在这些情况下,您将需要在接收端建立一个事务(ACID)数据库,并可能将消息ID记录在唯一的索引中,以使其无法重复。这称为使用幂等标记或逻辑删除-当您对消息进行操作时,您通常会在与执行操作本身相同的数据库事务中存储唯一的永久标记来跟踪您的操作。即使邮件重复,也会阻止您再次重复该操作。

如果您处理重复,或者您的消息天生就可以抵抗重复,则可以说您的系统是幂等的。这意味着您可以安全地处理多次收到相同的消息,而不会破坏您的工作。这通常也意味着您可以容忍发件人多次发送相同的消息-请记住,发件人在发送消息时通常也将至少一次执行一次操作。如果发件人无法记录他们已发送特定消息的事实,则只需再次发送即可。然后,发件人有责任确保在重新发送邮件时使用相同的墓碑或幂等令牌。

这是一种非常少见的语义,用于重复非常可怕的消息(或者消息根本不重要),以至于我们不希望根本不发送消息,而不是发送两次。最多一次表示排队系统将尝试一次将消息传递给您,但仅此而已。如果您收到并确认该消息一切都很好,但是如果您不这样做或有任何错误,则该消息将永远丢失-要么是因为排队系统费了很大的劲才能在尝试发送之前记录下向您的传递发送它(以防该消息具有惊人的爆炸性),或者根本不用去记录该消息,就像路由器在UDP数据包上传递一样,只是在传递它。

这种语义通常在充当无状态信息路由器的消息传递系统中起作用。或者在重复消息具有破坏性的情况下,以防万一发生任何故障时必须进行调查或对帐。

这是消息传递的圣杯,也是许多蛇油的源泉。这意味着保证每条消息都只发送一次,处理一次,最多也不会减少。构建或使用分布式系统的每个人在生活中都有一个观点,他们认为“这有多难?”,然后他们要么(1)了解为什么这是不可能的,找出幂等性,然后至少使用-一次,或(2)他们尝试建立一个半确定的“完全一次”系统,并以高价将其出售给尚未弄清(1)的那些人。

如果您深入思考问题,那么很多事情都会出错:

邮件系统已记录邮件的确认可能无法通过网络到达发件人

发件人可能无法记录消息传递系统已收到消息的确认

假设发送消息时一切顺利-当消息传递系统尝试将消息传递给接收者时:

邮件系统的数据库可能无法记录邮件已传递

考虑到所有可能出错的地方,任何消息传递系统都不可能保证一次发送。即使消息传递系统完美无瑕,大多数可能出问题的地方还是在消息传递系统之外或在互连网络中。某些系统确实尝试使用“仅一次”一词,通常是因为它们声称其实现绝不会遇到上述任何消息传递系统问题-但这并不意味着整个系统神奇地拥有一次语义,即使声明实际上是正确的。这通常意味着排队系统具有某种形式的排序,锁定,哈希,计时器和幂等性令牌,这将确保它永远不会重新传递已经被删除/确认的消息,但这并不意味着包括发布者+队列+订阅者在内的整个系统已经获得了完全精确的一次保证。

大多数优秀的邮件系统工程师都理解这一点,并将向用户解释为什么这种语义不可行。处理消息的更简单,更可靠的方法是回到基础知识,并在发送,接收和排队过程的每个点上至少一次采用幂等度量:如果一开始您不成功,请重试,重试,重试...

在传递语义之后,人们心中的另一个常见问题是“为什么我们既不能并行处理消息,又要确保按顺序处理消息?”。不幸的是,这是逻辑专制强加给我们的另一个权衡。依次进行工作和同时进行多项工作总是相互冲突的。大多数消息队列系统都会要求您选择一个-AWS SQS是通过将并行性放在优先顺序之上来开始的;但最近又引入了一个单独的FIFO(先进先出)排队系统,该系统保持严格的顺序排序。在两者之间做出选择之前,让我们先探讨一下两者之间的区别以及为什么需要完全有所区别。

回到我们较早的隐喻队列(长的管子,我们将写在球上的消息滚动到其中),我们可能想象管子比单个球要宽一点。实际上,球不可能在管内相互超越或通过,因此接收者发出这些消息的唯一方法是按照放入的顺序一个接一个地发送消息。这确保了严格的排序,但对我们的接收器有严格的限制。接收方只能有一个处理每个消息的代理,如果有一个以上,则不能保证消息是按顺序处理的。因为每个新代理都可以独立处理每个消息,所以它们可以随时完成并从下一条消息开始。如果是两个代理,则A&A。 B,并且代理A接收第一条消息,代理B接收第二条消息;甚至在代理A完成处理第一条消息之前,代理B即可完成第二条消息的处理并从第三条消息开始。尽管严格按照发送顺序从队列中接收消息,但是如果有多个接收代理,则无法说出消息将按照该顺序进行处理。

代理可以使用某种分布式锁相互协调,但这基本上与只有一个代理相同-该锁将只允许一个代理在任何给定时间工作。这也意味着一个代理崩溃将导致死锁,而无需完成任何工作。

消息传递系统保证命令的一种方式是,试管拒绝发出下一个球,直到并且除非接收到的最后一个球被破坏(最后一条消息已被删除/确认)为止。这通常是FIFO队列会做的-只有在确认或删除了最后一个消息后,它们才会提供下一条消息-但这意味着即使只有N个代理,一次也可能只能工作。代理等待从队列中接收消息。

有时,这正是我们想要的。当我们只需要与单个代理人打交道时,某些操作就更易于有效控制,例如对金融交易执行规则;遵守速率限制;或通常假设将始终按顺序处理其格式已设计好的消息。但是,这些“好处”中的许多并非真正来自决定使用FIFO排序的情况-在任何情况下,如果我们有N个接收器,它们必须以某种方式彼此协调工作,则将受益于N = 1的特殊情况。关键的一点是,要求有保证的顺序意味着我们必须一次只在一个接收器上顺序处理消息。

这种限制也给排队系统带来了巨大压力,因此您会发现FIFO队列通常比并行队列更昂贵且容量更少。这是因为相同的逻辑限制适用于内部实现

......