你在Rabbit有一个队列,然后一些消费者从这个队列中消费。如果你根本没有设置QoS(basic.qos),那么Rabbit会把所有的队列消息都按照网络和客户端允许的速度推送给客户端。消费者将会飞速增加它们的内存占用,因为它们将所有消息都缓存在自己的RAM中。如果您询问Rabbit,队列可能会显示为空,但会有大量在客户端中,正准备由客户端应用程序处理的消息未被确认。如果您添加新的消费者,则队列中不会有消息发送给新的消费者。即使有其他消费者可用于更快地处理这样的消息,它们也只是在现有的客户端缓存,并且可能在那里很长一段时间。这是相当次优的。
因此,默认的QoS预取设置为客户提供了无限的缓冲区,这可能导致不良的行为和性能。但是,怎样的QoS预取缓冲区大小才是您应该设置的?设置的目的是让消费者保持工作饱和状态,同时尽量减少客户端的缓冲区大小,以便更多的消息留在Rabbit的队列中,来可供新消费者使用,或在消费者空闲时发送给消费者。
比方说Rabbit从这个队列中拿出一条消息需要50ms,把它放到网络上,然后到达消费者。客户端处理消息需要4ms。一旦消费者处理了消息,它就会发送一个ACK给Rabbit,这个Rabbit需要进一步发送50ms的信息给Rabbit进行处理。所以我们总共有104ms的往返时间。如果我们有1个消息的QoS预取设置,那么在这个往返行程完成之后,Rabbit不会发送下一个消息。因此,客户端每104ms只有4ms,或3.8%的时间忙碌,而我们希望百分之百的时间都在忙碌中。
如果我们在每个消息的客户端上执行总的往返时间/处理时间,则得到104/4 = 26。如果我们具有26个消息的QoS预取,就解决了我们的问题:假设客户端具有26个消息缓冲,等待处理。 (这是一个明智的假设:一旦你设置了basic.qos,然后从一个队列中消耗,Rabbit将发送尽可能多的消息到你订阅到客户端的队列,直到QoS限制。消息不是很大,带宽也很高,所以Rabbit很可能比你的客户端更快地发送消息到你的客户端,所以从假设的完整性来做所有的数学是合理的(也是更简单的)客户端缓冲区)。如果每条消息需要4ms的处理来处理,那么总共需要26×4 = 104ms来处理整个缓冲区。第一个4ms是第一个消息的客户端处理。客户端然后发出一个确认,然后继续处理缓冲区中的下一条消息。这一点需要50ms才能到达代理。代理向客户端发出一条新消息,这需要50ms的时间,所以到了104ms时间,客户端已经完成缓冲区的处理,代理的下一条消息已经到达,并准备好等待客户端来处理它。因此,客户端始终处于忙碌状态:具有较大的QoS预取不会使其更快;但是我们最大限度地减少了缓冲区的大小,从而减少了客户端消息的延迟:消息被客户端缓冲了,不再需要为了保持客户端的工作。事实上,客户端能够在下一条消息到达之前完全排空缓冲区,因此缓冲区实际上保持为空。
这个解决方案绝对没问题,只要处理时间和网络行为保持不变。但考虑一下如果网络突然间速度减半会发生什么情况:预取缓冲区不够大,现在客户端会闲置,等待新消息到达,因为客户端能够处理消息的速度比Rabbit能够提供新消息。
为了解决这个问题,我们仅仅需要翻倍(或几乎是双倍)QoS预取大小。如果我们把这个大小从26升到51,而客户仍然在每4ms处理消息,那么我们现在有51 * 4 = 204ms消息缓冲区中,其中4ms将用于处理消息,剩余200ms为发送一个ACK回Rabbit和接收下一个消息。因此,我们现在可以应对网络速度的减半。
但是,如果网络正常运行,现在将QoS预取提高一倍,意味着每个消息都会驻留在客户端缓冲区中一段时间,而不是在到达客户端时立即处理。再次,从现在51条消息的完整缓冲区开始,我们知道新消息将在客户端完成处理第一条消息100ms后开始出现在客户端。但是在这100ms内,客户端将会处理50个可用的100/4 = 25个消息。这意味着当新的消息到达客户端时,它将被添加到缓冲区的末尾,当客户端从缓冲区的头部移除时。因此,缓冲区总是保持50 – 25 = 25个消息长度,因此每个消息将在缓冲区中保持25 * 4 = 100ms,Rabbit发送给客户端以及客户端开始处理它的时间从50ms增加到150ms 。
因此,我们看到,增加预取缓冲区,使客户端可以应对恶化的网络性能,但是同时也会使得客户端繁忙,大大增加了网络正常运行时的延迟。
同样,排除掉网络的性能恶化,如果客户端开始处理每个消息40毫秒,而不是4ms,会发生什么?如果Rabbit的队列以前是稳定的(即入口和出口速率相同),它现在将开始快速增长,因为出口率降到了原来的十分之一。您可能会决定尝试通过添加更多的消费者来处理这种增长的积压,但现在有消息正在被现有客户端缓冲。假设26条消息的原始缓冲区大小,客户端将花费40ms处理第一条消息,然后将确认消息发送回Rabbit并移至下一条消息。 ack仍然需要50ms才能到达Rabbit,而Rabbit发出一个新的消息还需要50ms,但是在100ms内,客户端只处理了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条消息。 100ms后(假设网络运行正常),一条新的消息将从Rabbit到达,客户端将处理这50条消息中的第三条消息(缓冲区现在为47条消息长)的一半,因此新消息将在缓冲区中是第48位,并且不会再触及直到47 * 40 = 1880ms之后。再一次,考虑到向客户端发送消息的网络延迟仅为50ms,现在这个1880ms的延迟意味着客户端缓冲占据了超过97%的延迟(1880 /(1880 + 50)= 0.974)。这可能是不可接受的:如果数据处理得很快,而不是在客户端收到数据后2秒,数据才可能是有效的和有用的!如果其他消费客户端空闲,他们无能为力:一旦Rabbit向客户端发送消息,消息就是客户端的责任,直到他们拒绝或拒绝消息为止。一旦消息被发送到客户端,客户端不能窃取彼此的消息。你想要的是让客户端保持忙碌,但是客户端尽可能少地缓存消息,这样消息就不会被客户端缓冲区延迟,因此新消费的客户端可以快速地接收到来自Rabbit队列的消息。
因此,如果网络变慢,缓冲区太小会导致客户端空闲,但如果网络正常运行,缓冲区太大会导致大量额外的延迟;如果客户端突然开始花费更长时间来处理每个缓冲区,消息比正常。很明显,你真正想要的是一个不同的缓冲区大小。这些问题在网络设备中是常见的,并且一直是很多研究的主题。主动队列管理算法试图尝试丢弃或拒绝消息,以避免消息长时间坐在缓冲区中。当缓冲器保持空闲(每个消息只遭受网络延迟,并且根本不在缓冲器中)并且缓冲器在那里吸收尖峰时,达到最低延迟。 Jim Gettys一直从网络路由器的角度来研究这个问题:局域网和广域网性能之间的差异正在遭受同样的问题。实际上,无论何时,在生产者(在本例中为Rabbit)和消费者(客户端应用程序逻辑)之间都有一个缓冲区,双方的性能可以动态变化,您将会遇到这样的问题。最近出现了一种名为Controlled Delay的新算法,它在解决这些问题上表现得很好。
作者声称他们的CoDel(“coddle”)算法是一个“无旋钮”算法。这实际上是一个谎言:这里有两个旋钮,他们都需要适当的设置。但是每次性能改变时都不需要改变它们,这是一个巨大的好处。我已经为我们的AMQP Java客户端实现了这个算法,作为QueueingConsumer的一个变种。虽然原来的算法是针对TCP层的,那么丢弃数据包是有效的(TCP本身会处理丢失数据包的重传),但在AMQP中这不太有礼拜!因此,我的实现使用Rabbit的basic.nack扩展来显式地将消息返回给队列,以便其他人可以处理它们。
使用它几乎和普通的QueueingConsumer一样,除了你应该提供三个额外的参数给构造函数来获得最好的性能。
-
首先是requeue,它设置当消息被阻塞,是否应该重新排序或丢弃。如果设置为FALSE,那么它们将被丢弃,这样可能会触发死信交换机制。
-
第二个是targetDelay,这是消息在客户端QoS预取缓冲区中等待的可接受时间(以毫秒为单位)。
-
第三个是interval,是以毫秒为单位的一个消息的预期最坏情况处理时间。这不一定是精确的,但在一个数量级内肯定有帮助。
您仍然应该适当地设置QoS预取大小。如果不这样做,可能是客户端会收到很多消息,然后如果他们在缓冲区中的时间太长,算法将不得不将它们返回给Rabbit。消息返回给Rabbit时,很容易产生大量额外的网络流量。一旦性能偏离规范,CoDel算法就意味着只会开始丢弃(或拒绝)消息,因此一个可行的例子可能会有所帮助。
同样,假设每个方向的网络遍历时间为50ms,并且我们期望客户端平均花费4ms的时间处理每条消息,但是这可以达到20ms。因此我们把CoDel的interval参数设置为20。有时网络速度减半,所以每个方向的遍历时间可以是100ms。为此,我们将basic.qos预取设置为204/4 = 51.是的,这意味着在网络正常运行的大部分时间内,缓冲区将保持25个消息(见前面的工作),但是我们认为这可以接受。我们预期每个消息将在缓冲区中驻留25 * 4 = 100ms,因此将CoDel的targetDelay设置为100。
正常运行时,CoDel不会碍事,很少有消息会被nacked。但是,如果客户端开始处理消息的速度比正常情况慢,CoDel会发现消息已经被客户端缓存了太久,那就将这些消息返回给队列。如果这些消息被重新发送,则它们将可用于发送给其他客户端。
这在目前是非常具有实验性的,也有可能看到CoDel不适合处理纯IP的AMQP消息的原因。另外值得记住的是,通过nacks重新发送消息是一个相当昂贵的操作,所以最好设置CoDel的参数来确保极少数的消息会在正常操作中被nacked。后台管理插件会是一个来检查有多少消息被nacked的简单方法。一如以往,评论,反馈和改进是最受欢迎的!
公众号推荐:
公众号:VOA英语每日一听
微信号: voahk01
可长按扫码关注,谢谢