以下为Google翻译,原文链接:原文链接
您在Rabbit中有一个队列。您有一些正在使用该队列的客户端。如果您根本没有设置QoS设置(basic.qos
),那么Rabbit将以网络和客户端允许的最快速度将所有队列的消息推送到客户端。使用者将所有消息缓存在自己的RAM中时,将在内存中膨胀。如果您询问Rabbit,队列可能会显示为空,但是当它们位于客户端中时,可能有数百万条未被确认的消息可供客户端应用程序处理。如果添加新使用者,则队列中将没有消息可发送给新使用者。消息只是在现有客户端中进行缓冲,并且可能存在很长时间,即使有其他使用者可以更快地处理此类消息也是如此。这是相当次优的。
因此,默认的QoSprefetch
设置为客户端提供了_无限的_缓冲区,这可能导致不良的行为和性能。但是,应将QoSprefetch
缓冲区大小设置为什么?目的是使消费者保持工作量,但是要最小化客户端的缓冲区大小,以使更多消息留在Rabbit的队列中,从而可供新消费者使用,或者在它们变得免费时将其发送给消费者。
假设Rabbit从该队列中获取一条消息并将其放入网络并到达消费者需要50毫秒。客户端需要4毫秒来处理该消息。使用者处理完消息后,它将ack
回发给Rabbit,这又需要50毫秒才能发送到Rabbit并由Rabbit处理。因此,我们的总往返时间为104毫秒。如果我们的QoSprefetch
设置为1条消息,那么Rabbit将不会发送下一条消息,直到此往返过程完成。因此,客户端将仅每104毫秒中的4毫秒忙,即占时间的3.8%。我们希望它100%的时间都很忙。
如果我们_对每条消息在客户端上进行__总往返时间_/_处理时间_,则得到104 / 4 = 26
。如果我们有prefetch
26条消息的QoS,这将解决我们的问题:假定客户端缓冲,准备好并等待处理的26条消息。(这是一个明智的假设:设置好basic.qos
之后consume
从队列中,Rabbit将从您已订阅到客户端的队列中发送尽可能多的消息,直到QoS限制。如果您认为消息不是很大并且带宽很高,那么Rabbit可能会比您的客户端处理消息的速度更快地将消息发送给使用方的客户端。因此,从一个完整的客户端缓冲区的假设出发进行所有数学运算是合理的(并且更简单)。)如果每个消息需要4毫秒的处理时间来处理,那么总共要花整个时间26 * 4 = 104ms
来处理整个缓冲区。前4ms是客户端对第一条消息的处理。然后,客户发出ack
并继续处理缓冲区中的下一条消息。该确认需要50毫秒才能到达代理。然后,代理向客户端发出一条新消息,这需要50毫秒才能到达目的地,因此,经过104毫秒并且客户端已完成对其缓冲区的处理时,来自代理的下一条消息已经到达并准备就绪并等待客户来处理它。因此,客户端始终保持忙碌状态:拥有更大的QoSprefetch
并不能使其更快。但是我们最大程度地减少了缓冲区的大小,从而最大程度地减少了客户端中消息的等待时间:客户端对消息的缓冲时间不超过为使客户端保持工作饱和所需的时间。实际上,客户端能够在下一条消息到达之前完全耗尽缓冲区,因此缓冲区实际上保持为空。
只要处理时间和网络行为保持不变,该解决方案就可以了。但是请考虑一下,如果网络突然速度减半会发生什么:您的prefetch
缓冲区不再足够大,现在客户端将处于空闲状态,等待新消息到达,因为客户端能够以比Rabbit提供新消息更快的速度处理消息。
为了解决这个问题,我们可能只是决定将QoSprefetch
大小增加一倍(或几乎增加一倍)。如果我们将其从26推至51,则如果客户端处理每条消息的时间保持为4ms,则现在51 * 4 = 204ms
缓冲区中有消息,其中4ms将用于处理消息,剩下200ms的时间用于将确认发送回Rabbit和接收下一条消息。因此,我们现在可以应对速度减半的网络。
但是,如果网络运行正常,则prefetch
现在将QoS翻倍意味着每个消息将在客户端缓冲区中停留一会儿,而不是在到达客户端后立即进行处理。同样,从现在有51条消息的完整缓冲区开始,我们知道在客户端完成对第一条消息的处理后100ms,新消息将开始出现在客户端。但是在这100毫秒内,客户端将处理100 / 4 = 25
50个可用消息中的消息。这意味着,当有新消息到达客户端时,随着客户端从缓冲区头部删除消息,它将被添加到缓冲区的末尾。因此,缓冲区将始终保持50 - 25 = 25
消息很长,因此每个消息都将保留在缓冲区中,以便25 * 4 = 100ms
,将Rabbit发送给客户端和客户端开始处理它之间的延迟从50ms增加到150ms。
因此,我们看到增加prefetch
缓冲区,以便客户端可以在保持客户端繁忙的同时处理网络性能下降的问题,从而大大增加了网络正常运行时的延迟。
同样,如果客户端开始花费40毫秒(而不是4毫秒)来处理每个消息,那么网络性能不会恶化,又会如何呢?如果Rabbit中的队列以前是固定长度的(即,入口和出口的速率相同),则现在它将开始快速增长,因为出口速率已下降到原来的十分之一。您可能会决定通过添加更多使用者来尝试解决这一不断增长的积压问题,但是现有客户端现在正在缓冲某些消息。假定原始缓冲区大小为26条消息,客户端将花费40毫秒处理第一条消息,然后将确认发送回Rabbit,然后移至下一条消息。ACK仍然需要50毫秒才能到达Rabbit,再过50毫秒才能让Rabbit发送新消息,但是在那100毫秒内,客户端只能通过100 / 40 = 2.5
而不是其余的25条消息。因此,此时缓冲区的25 - 3 = 22
消息很长。来自Rabbit的新消息而不是立即被处理,现在位于第23位,在其他22条仍在等待处理的消息之后,客户端将不会再对其进行处理22 * 40 = 880ms
。假设从Rabbit到客户端的网络延迟仅为50ms,那么这额外的880ms延迟现在是延迟的95%(880 / (880 + 50) = 0.946
)。
更糟糕的是,如果我们将缓冲区大小增加一倍至51条消息以应对网络性能下降,会发生什么情况?在处理完第一条消息后,客户端中还将缓冲50条其他消息。100毫秒后(假设网络正常运行),Rabbit会收到一条新消息,客户端将处理这50条消息中的第3条(缓冲区现在为47条消息),这将是新消息在缓冲区中排在第48位,不会再被触碰到47 * 40 = 1880ms
。同样,假设将邮件发送到客户端的网络延迟仅为50ms,那么这进一步的1880ms延迟现在意味着客户端缓冲将导致超过97%的延迟(1880 / (1880 + 50) = 0.974
)。这很可能是不可接受的:只有及时处理数据,才可能是有效且有用的数据,而不是在客户收到数据后约2秒钟!如果其他正在使用的客户端处于空闲状态,则它们无能为力:Rabbit向客户端发送消息后,该消息是客户端的责任,直到它终止或拒绝该消息为止。邮件发送到客户端后,客户端便无法互相窃取消息。您想要的是让客户端保持忙碌,但让客户端缓冲尽可能少的消息,以使消息不会被客户端缓冲区延迟,因此可以快速将Rabbit队列中的消息提供给新使用的客户端。
因此,如果网络变慢,缓冲区太小会导致客户端空闲,而如果网络正常运行,缓冲区太大会导致大量额外的延迟,如果客户端突然开始花费更长的时间处理每个缓冲区,则会导致大量额外的延迟。消息比平常多。显然,您真正想要的是变化的缓冲区大小。这些问题在网络设备之间很常见,并且已经成为许多研究的主题。_活动队列管理_算法尝试尝试丢弃或拒绝消息,以便避免长时间将消息放在缓冲区中。当缓冲区保持为空时(每个消息仅遭受网络等待时间并且根本不位于缓冲区中)并且缓冲区在那里吸收峰值,可以实现最低的延迟。吉姆·盖蒂从网络路由器的角度来看,该问题一直在解决:LAN和WAN的性能差异遭受同样的问题。的确,只要您在生产者(在我们的例子中是Rabbit)和消费者(在客户端的应用程序逻辑)之间有一个缓冲区,并且双方的性能都可以动态变化,您就会遭受这类问题的困扰。最近,已经发布了一种称为“受控延迟”的新算法,该算法似乎可以很好地解决这些问题。
作者声称他们的_CoDel_(“ coddle”)算法是“无旋钮”算法。这确实有点谎言:有两个旋钮,它们确实需要适当设置。但是他们不需要每次性能变化时都进行更改,这是一个巨大的好处。我已经为QueueingConsumer的变体为AMQP Java客户端实现了该算法。虽然原始算法是针对TCP层的,但是在AMQP中,仅丢弃数据包(TCP本身将负责丢失数据包的重新传输)是有效的,这并不是很礼貌!结果,我的实现使用Rabbit的basic.nack
扩展将消息显式返回到队列,以便其他人可以处理它们。
使用它与普通QueueingConsumer几乎相同,不同之处在于您应该为构造函数提供三个额外的参数以获得最佳性能。
- 第一个是
requeue
说,当邮件被压缩时,是否应该重新排队或丢弃它们。如果设置为false,则将其丢弃,这可能会触发死信交换机制。 - 第二个
targetDelay
时间是消息在客户端QoSprefetch
缓冲区中等待的可接受时间(以毫秒为单位)。 - 第三个是
interval
and是一个消息的预期最坏情况处理时间(以毫秒为单位)。不必一定要这样,但是一定数量级内肯定会有所帮助。
您仍应prefetch
适当设置QoS大小。如果您不这样做,则很可能会向客户端发送很多消息,并且如果它们在缓冲区中放置的时间过长,该算法将不得不将它们返回给Rabbit。当消息返回给Rabbit时,很容易导致大量额外的网络流量。一旦性能与规范不符,CoDel算法旨在仅开始丢弃(或拒绝)消息,因此,一个可行的示例可能会有所帮助。
同样,假设网络在每个方向上的遍历时间为50ms,我们希望客户端平均花费4ms来处理每个消息,但这可能会增加到20ms。因此,我们将interval
CoDel的参数设置为20。有时网络速度减半,因此每个方向的遍历时间可以为100ms。为此,我们将设置basic.qos prefetch
为204 / 4 = 51
。是的,这意味着当网络正常运行时(大多数情况下,请参见前面的工作),该缓冲区在大多数情况下会保留25条消息,但是我们认为可以。因此,每条消息将在缓冲区中保留一个期望值25 * 4 = 100ms
,因此我们将targetDelay
CoDel的值设置为100。
当一切正常运行时,CoDel不会妨碍您,并且几乎不需要整理任何消息。但是,如果客户端开始处理消息的速度比平常慢,CoDel将发现该消息已被客户端缓冲了太长时间,并将这些消息返回到队列中。如果这些消息被重新排队,则它们将可用于传递给其他客户端。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。