跳到主要内容

调整 RabbitMQ 的大小

·阅读时长 11 分钟
Matthew Sackman

我们在 RabbitMQ 总部面临的问题之一是,尽管我们可能非常了解 broker 的工作原理,但我们往往没有设计使用 RabbitMQ 且需要长期可靠、无人值守运行的应用程序的大量经验。我们花费大量时间回答邮件列表上的问题,并且我们也做一些咨询工作,但在某些情况下,正是由于构建应用程序的用户联系我们,我们才真正开始思考 RabbitMQ 的长期行为。最近,我们被促使长期而深入地思考队列的基本性能,这导致了一些关于配置 RabbitMQ 的认识。

当 RabbitMQ 的队列为空时,它们的速度最快。当队列为空,并且有消费者准备好接收消息时,那么一旦队列收到消息,它就会直接发送给消费者。在持久队列中的持久消息的情况下,是的,它也会写入磁盘,但这以异步方式完成,并且被大量缓冲。主要的一点是,只需要做很少的簿记工作,很少的数据结构需要修改,并且很少需要分配额外的内存。因此,消息通过空队列的 CPU 负载非常小。

如果队列不为空,则需要做更多的工作:消息实际上必须排队。最初,这也是快速且廉价的,因为底层的函数式数据结构非常快。然而,通过保留消息,队列的整体内存使用量将会更高,并且我们正在做比以前更多的工作(每条消息现在都在排队和出队,而以前每条消息都只是直接发送给消费者),因此每条消息的 CPU 成本更高。因此,您使用空队列能够达到的最高速度将高于具有固定 N 条消息的队列的最高速度,即使 N 非常小。

如果队列接收消息的速度快于其向消费者泵送消息的速度,那么事情就会变得更慢。随着队列增长,它将需要更多内存。此外,如果队列接收到发布峰值,那么队列必须花费时间处理这些发布,这会占用 CPU 时间,从而无法将现有消息发送给消费者:如果队列没有发布到达来分散其注意力,那么包含一百万条消息的队列能够以更高的速率被耗尽并发送给准备就绪的消费者。这不算什么火箭科学,但值得记住的是,到达队列的发布会降低队列驱动其消费者的速率。

最终,随着队列增长,它会变得如此之大,以至于我们必须开始将消息写入磁盘并从 RAM 中遗忘它们,以便释放 RAM。在这一点上,每条消息的 CPU 成本远高于空队列处理消息的成本。

这些看起来似乎没什么特别深刻的,但在构建应用程序时牢记这些要点却非常重要。

假设您设计并构建了您的应用程序,使用了 RabbitMQ。将会有一些发布者集合和一些消费者集合。您对此进行了测试,假设在系统的某个部分,您发现确保队列保持空或接近空的最高速率是 2000 条消息/秒。然后,您选择一台机器来运行 RabbitMQ,这可能是在某种类型的虚拟服务器上。当以 2000 条消息/秒进行测试时,您发现运行 RabbitMQ 的服务器的 CPU 负载不是很高:瓶颈在应用程序的其他地方——最有可能是在队列的消费者中(您正在测量最大稳定端到端性能)——因此 RabbitMQ 本身并没有受到过度压力,因此没有占用太多 CPU。因此,您选择了一台不是非常强大的虚拟服务器。然后您启动应用程序,果然,一切看起来都正常。

随着时间的推移,您的应用程序变得越来越受欢迎,因此您的速率也随之增加。

最终,您会到达一个点,您的消费者都在全力运行,并且您的队列几乎保持为空。但是,在您应用程序一天中最受欢迎的时间段,您的发布者向您的队列推送的消息比以前多了一些。这只是正常的增长——您现在有更多的用户,因此消息发布速度比以前快一些并不奇怪。您希望发生的是,RabbitMQ 会很乐意缓冲这些消息,并最终将它们馈送给您的消费者,他们将能够在一天中较安静的时间段处理积压的工作。

问题是这可能无法发生。因为您的队列现在(即使是短暂地)接收的消息比您的消费者能够处理的消息更多,所以队列花费在处理每条消息上的 CPU 时间比队列为空时要多(消息现在必须排队)。这会占用驱动消费者的 CPU 时间,不幸的是,由于您为 RabbitMQ 选择的机器没有大量的备用 CPU 容量,您开始使 CPU 达到最大值。因此,您的队列无法像以前那样努力地驱动您的消费者,这反过来又使队列的增长率增加。这反过来又开始将队列推向必须开始将消息推送到磁盘以释放 RAM 的大小,这再次占用了您没有的更多 CPU,到这时,您可能已经陷入困境。

您能做什么?

在当前的紧急时刻,您需要耗尽您的队列。因为您的队列花费在处理新消息到达的时间比推送消息给消费者的时间更多,所以向队列投入更多消费者不太可能显着帮助。您真的需要让发布者停止。

如果您有奢侈的条件可以直接关闭发布者,那就这样做,并在队列再次变空时重新打开它们。如果您不能这样做,那么您需要将它们的负载转移到其他地方,但考虑到您的 Rabbit 正在写入磁盘以避免内存耗尽,并且 CPU 达到最大值,在您当前的 Rabbit 上添加新队列将无济于事——您需要在不同的机器上安装一个新的 Rabbit。如果您设置了集群,那么您可以在 RabbitMQ 集群中负载不太重的节点上配置新队列,然后将大量新消费者连接到该队列,并将发布者转移到那里。此时,您将意识到不使用默认的无名交换机和直接寻址队列的价值,并且会非常高兴您的发布者发布到您创建的交换机,从而允许您向新的队列添加新的绑定,并删除旧队列的绑定,转移负载,而无需中断您的发布者。然后,旧队列将能够尽可能快地驱动其消费者(您没有移除!),并且队列将被耗尽。现在在这种情况下,您可能会遇到消息乱序处理的情况(新消息到达新的队列后可能会被您的新消费者处理,而旧队列中的旧消息尚未处理),但是如果您在一个队列上有多个消费者,那么您可能已经在处理这个问题了。

我们经常被告知,预防胜于治疗。那么,您如何设计您的应用程序,使其能够帮助 RabbitMQ 应对这些潜在的灾难性情况呢?

首先,不要在您的消费者中使用非常低的 basic.qos 预取值。如果您使用值 1,那么这意味着队列向消费者发送一条消息,然后在收到确认之前无法向该消费者发送更多消息。只有当它完成此操作后,它才能发送下一条消息。如果确认需要一段时间才能返回队列(例如,网络上的高延迟,或者您的 Rabbit 承受的负载可能意味着确认需要一段时间才能完全到达队列),那么在此期间,该消费者将闲置在那里。例如,如果您使用 basic.qos 预取值 20,那么 broker 将确保向消费者发送 20 条消息,然后即使第一条消息的确认(可能很慢地)返回队列,消费者仍然有工作要做(即接下来的 19 条消息)。本质上,预取值越高,消费者就越能免受返回队列的往返时间峰值的影响。

其次,考虑不要确认每条消息,而是确认每 N 条消息,并在确认时设置 multiple 标志。当队列过载时,是因为它有太多的工作要做(这是显而易见的)。作为消费者,您可以通过确保不向其发送大量确认来减少它必须做的工作量。因此,例如,您将 basic.qos 预取设置为 20,但您仅在处理完每 10 条消息后发送确认,并在确认时将 multiple 标志设置为 true。队列现在将收到以前收到确认的十分之一。它仍然会在内部确认所有十条消息,但如果它收到一个确认来解释多条消息,而不是大量单独的确认,它可以以更有效的方式完成此操作。但是,如果您仅确认每 N 条消息,请确保您的 basic.qos 预取值高于 N。可能至少是 2*N。如果您的预取值与 N 相同,那么您的消费者将再次闲置,而确认返回队列,并且队列发送一批新消息。

第三,制定策略,如果情况变得最糟糕,将负载转移到其他机器上的其他队列。是的,使用 RabbitMQ 作为缓冲区来隔离发布者和消费者并吸收峰值是一个好主意。但同样,您需要记住,当 RabbitMQ 的队列为空时,它们的速度最快,并且我们总是说您应该设计您的应用程序,以便队列通常为空。或者换句话说,队列的性能在您可能比以往任何时候都更需要它来吸收大量峰值时最低。这意味着,除非您进行测试以确保您知道它会恢复,否则如果发生非平凡的峰值,导致您的队列长度大幅增加,您可能会感到惊讶。您可能没有考虑到,在这种情况下,您现有的消费者最终可能会比以前更慢地被驱动,仅仅是因为您的队列正忙于做其他事情(排队消息),这可能会导致恶性循环,最终导致队列的性能灾难性下降。

问题的核心在于队列是单线程资源。如果您已经设计了路由拓扑,使其已经或至少可以跨多个队列而不是仅仅将所有消息都集中到一个队列中来传播消息,那么您就更有可能通过转移负载并能够利用额外的 CPU 资源来快速轻松地响应此类问题的发生,从而确保您可以最大限度地减少每条消息的 CPU 命中。

© . All rights reserved.