一些队列理论 吞吐量、延迟和带宽
你有一个队列在Rabbit中。你有一些客户端从该队列消费。如果你完全不配置QoS设置(basic.qos),这样Rabbit将以网络和客户端容许的尽可能快地速度发送队列中的所有消息到客户端。因为消费者在自己的RAM中缓存所有的消息,他们的内存将暴涨。如果你查询Rabbit,可能出现队列中是空的,但可能有以百万计的未确认的消息,因为它们在客户端,等待客户端应用程序处理。如果您添加了一个新的消费者,没有消息保留在队列中以被发送到新的消费者。消息只是被缓存在现有客户中,并有可能在那很长一段时间,即使有其它变为可用的消费者可以更快处理这些消息。这很不理想。
因此,默认QoS 设置“预取(prefetch )”带给客户端不受控制的缓存,并且这样可能导致不佳的行为和性能。但是,你应该设置多大的QoS 预取缓冲区?我们的目标是使消费者一直被工作饱和,但是最小化客户端的缓冲区的大小,使更多的消息留在Rabbit的队列中,从而提供给新的消费者,或只是被发送到成为空闲的消费者。
假设说,Rabbit从这个队列中取出消息,发送到网络上,并等它到达消费者需要50ms的时间。客户端需要4ms来处理消息。一旦消费者处理完消息,它发送一个ACK返回给Rabbit,这需要另一个50ms发送到Rabbit并被处理。因此,我们得到一个104ms的往返时间。如果我们有一个1条消息的QoS 预取设置 ,这样Rabbit不会发送出去下一个消息,直到这个往返完成后。因此,客户端将每104ms只有4ms或者3.8%的时间在忙。我们想它在100%的时间都是繁忙的。
如果我们将总的往返时间 / 客户端上处理每个消息的时间,我们得到104/4 = 26。如果我们有一个26消息的QoS预取,它可以解决我们的问题:假设客户端缓存26个消息,准备好并等待处理。(这是一个合理的假设:一旦你设置basic.qos,然后从队列中消耗(and then consume from a queue),Rabbit将从你指定的队列中发送它可以的尽可能多的消息到客户端,直至达到QoS限制。如果你假设消息并不是非常大且带宽很高,可能Rabbit将能够向你的消费客户端发送比你的客户端可以处理的消息更多的消息。因此基于满的客户端缓冲区的假设来进行所有的数学计算是合理的(和更简单地))。如果每个消息花4ms的处理来完成,那么将要花总共26 * 4 = 104ms处理整个缓冲区。第一个4ms是第一条消息的客户端处理时间。客户端接着发出一个ACK,继续从缓冲区处理下一个消息。该ACK需要50ms以到达中间层(broker)。中间层发出一个新的消息给客户端,这需要50ms的时间,因此这时104ms已经过去了,并且客户端已处理完其缓冲区,从中间层的下一个消息已经到达,准备好并等待的客户端来处理。这样,客户端所有时间都是保持繁忙的:有一个更大的QoS 预取,不会让它处理得更快;但我们最小化缓冲区的大小,从而减小在客户端的消息延迟:消息被客户端缓存的时间不比为了保持客户端被工作饱和需要的时间更长。实际上,客户端能够在下一个消息之前到达之前完全耗尽缓冲区,从而缓冲区实际上保持为空。
该解决方案是绝对好的,提供的处理时间和网络行为保持不变。但考虑如果网络速度突然减半会发生什么:你的预取缓冲不再足够大,现在客户端将处于空闲状态,等待新消息到达,因为客户端能够比Rabbit提供新消息更快地处理消息。
为了解决这个问题,我们可能只是决定增加一倍(或近一倍)的QoS 预取大小。如果我们把它从26增加到51,然后如果客户端的处理仍然是每消息4ms,我们现在有51 * 4 = 204ms的消息在缓冲区中,其中4ms将用于处理消息,留下的200ms发送ACK回Rabbit和接收下一条消息。因此,我们现在可以配合网络速度减半。
但是,如果网络正常执行,增加一倍我们的QoS预取现在意味着每个消息会在客户端缓冲区等待一会儿,而不是在到达客户端后立即被处理。再一次,从一个现在51个消息的完整缓冲区开始,我们知道,客户端处理完第一条消息后100毫秒,新的消息将开始出现在客户端。但在那100ms中,客户端将已处理50个可用消息中的100/4 = 25个消息。这意味着,作为一个新到达客户端的消息,它会被添加到缓冲区的末尾,因为客户端从缓冲区的头部删除。因此,缓冲区将永远保持50 - 25 = 25消息的长度,因此,每个消息将等在缓冲区25 * 4 = 100毫秒,增加Rabbit把它发送到客户端和客户端开始处理之间的延迟从50毫秒到150毫秒。
因此,我们可以看到,增加预取缓冲区,使客户端可以应付网络性能恶化,虽然保持客户端繁忙,但当网络正常执行时,大大增加了延迟。
同样,不是网络的性能恶化,如果客户端开始花40ms来处理每个消息而不是4ms,会发生什么情况?如果Rabbit中的队列之前在一个稳定的长度(即入列和出列速率是相同的),它就会开始迅速增长,因为出列速率已经下降到本来的十分之一。您可能会决定尝试加入更多的消费者渡过这一不断增长的积压工作,但目前有消息正在由现有客户端缓存。假设原来的缓冲区大小为26个消息,客户端将花费40ms的处理第一条消息,然后将发送到的ACK回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个消息。100毫秒后(假设网络正常运行),一个新的消息将从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”)算法是一个“不需要调整”(”knob free”)的算法。这是一个小小的谎言:有两个参数(knob),且它们确实需要适当地设置。但他们并不需要在每一次性能变化时改变,这是一个很大的好处。我已经为我们的AMQP的Java客户端实现了该算法,作为队列消费(QueueingConsumer)的一个变种。虽然原算法的目标是在只是丢弃数据包就有效的TCP层(TCP本身将重新传输丢失的数据包),在AMQP中,这不是那么客气!作为结果,我的实现使用Rabbit的basic.nack扩展明确返回消息到队列中,这样他们就可以被他人处理。
使用它和正常队列消费(QueueingConsumer)几乎一样,除了你应该提供三个额外的参数给构造函数以获得最佳的性能。
首先是requeue,也就是说,当消息是未确认的时,它们是否应该被重新排队或丢弃。如果为假,他们将被丢弃,如果他们成立这可能引发dead letter exchange mechanisms(they will be discarded which may trigger the dead letter exchange mechanisms if they’re set up.)。
第二个是targetDelay,这是可接受的消息在客户端的QoS预取缓冲中等待的时间的毫秒数。
第三是interval,是预期的最坏的情况下以毫秒为单位的一个消息处理时间。这不必是实际值(This doesn’t have to be spot on),但在一个量级范围中有帮助。
你还是应该设置适当的QoS 预取大小。如果不这样做,那么可能的是,客户端将发送大量消息,接着如果它们在缓冲区等得太久,算法将不得不把它们返回到Rabbit。因为消息会返回给Rabbit,这很容易最终导致很多额外的网络流量。CoDeL算法在于仅当性能偏离正常时才开始丢弃(或者拒绝)消息,因此一个有效的例子肯能会有帮助。
再次,假设网络传输时间在各个方向为50ms,我们希望客户端平均花4ms处理每个消息,但它可以瞬时达到20ms。因此,我们设定CoDel的interval参数为20。有时网络速度为平常的一半,因此各个方向的传输时间可能在100毫秒。为了配合它,我们设置的 basic.qos的预取为204/4 = 51。是的,这意味着当网络运行正常时缓冲区大部分时间将保持25个消息长(参见之前的计算),但我们认为这是可以的。因此,每个消息都会在缓冲区等待预期的25 * 4 = 100ms,所以我们设置CoDel的targetDelay 为100。
当一切都运行正常,CoDel不应该介入,几乎没有任何消息应该被否定确认(nacked)。但当客户端开始比正常处理消息速度慢,CoDel会发现消息已被客户端缓存时间过长,并将返回这些消息到队列中。如果这些消息都重新排队,那么它们将变为可用,以发送到其它客户端。
当前这是非常实验性的,并且有可能看到为什么CoDel没有像处理普通IP包那样适合处理AMQP消息的原因。也值得记住的是通过NACKS 重新排队消息是相当昂贵的操作,所以设置CoDel的参数以确保在正常操作下,如果有的话也是极少数消息被否定确认,是一个好主意。管理插件是一种简单的检查有多少个消息正在被否定确认的方法。一如以往非常欢迎,评价,反馈和改进!
英文原文来源:
Some queuing theory: throughput, latency and bandwidth
建议继续学习:
- 无锁消息队列 (阅读:12820)
- 多线程队列的算法优化 (阅读:6516)
- TSQ 的原理 (阅读:5951)
- 两个精彩的比喻:吞吐量和延迟、信号量和互斥锁 (阅读:5988)
- 系统架构的一些思考 (阅读:5614)
- 各消息队列软件产品大比拼 (阅读:5155)
- Gearman Server 使用 MySQL UDFs 来管理和保持队列 (阅读:4865)
- qperf测量网络带宽和延迟 (阅读:4598)
- 限制单个进程的带宽 (阅读:3661)
- 时延和带宽的关系 (阅读:3422)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:johnny0924 来源: MySQLOPS 数据库与运维自动化技术分享
- 标签: 吞吐量 带宽 延迟 队列
- 发布时间:2012-05-15 23:31:31
- [55] Oracle MTS模式下 进程地址与会话信
- [55] IOS安全–浅谈关于IOS加固的几种方法
- [54] 如何拿下简短的域名
- [54] 图书馆的世界纪录
- [53] android 开发入门
- [52] Go Reflect 性能
- [49] 读书笔记-壹百度:百度十年千倍的29条法则
- [49] 【社会化设计】自我(self)部分――欢迎区
- [38] 程序员技术练级攻略
- [33] 视觉调整-设计师 vs. 逻辑