仲裁队列和流量控制 - 压力测试
在 上一篇文章 中,我们对单个队列进行了一些简单的基准测试,以了解管道化发布者确认和消费者确认对流量控制的影响。
具体来说,我们研究了
- 发布者:限制正在进行的消息数量(已发送但等待确认的消息)。
- 消费者:预取(代理程序允许在通道上传输的消息数量)
- 消费者:确认间隔(多标志用法)
不出所料,我们发现当我们将发布者和代理程序限制为一次少量正在进行的消息时,吞吐量较低。当我们增加这个限制时,吞吐量有所提高,但仅限于某个点,之后我们没有看到吞吐量进一步提升,反而只是延迟增加。我们还发现允许消费者使用多标志对吞吐量有利。
在本文中,我们将针对许多客户端、许多队列以及不同量的负载(包括压力测试)来查看这三个相同的设置。我们将看到发布者确认和消费者确认在流量控制中起作用,以帮助防止代理程序过载。
在数据安全方面,客户端扮演着重要角色,它们必须正确使用确认(confirms)和确认接收(acks)来实现至少一次处理。同样,成千上万的客户端不应期望能够用负载压垮一个代理(broker),却不对如何处理这种负载负责。
请注意,本文档包含大量细节,请确保您准备好一杯饮品再开始阅读。
机械同情心
我非常喜欢“机械同情心”这个词。当你慢速驾驶赛车时,几乎可以为所欲为。只有当你将赛车推向极限时,你才需要开始倾听它的声音,感受它的震动并相应地进行调整,否则它会在比赛结束前就散架了。
同样,对于 RabbitMQ,如果你负载很低,那么你可以为所欲为。你可能不会注意到改变这三个设置,或者根本不使用确认(至少在性能方面)会带来多大影响。只有当你将集群压力推向极限时,这些设置才真正变得重要。
优雅降级
当系统接收到的数据量超过其处理能力时,它应该怎么做?
- 答案 1:接受所有数据,然后变成一堆冒烟的二进制碎片。
- 答案 2:产生巨大的吞吐量和低吞吐量之间的剧烈波动,以及延迟的巨大变化。
- 答案 3:限制数据入口速率,提供稳定的吞吐量和低延迟。
- 答案 4:优先入口到出口,吸收数据,如同负载高峰一样导致高延迟,但更好地跟上入口速率。
在 RabbitMQ,我们会认为答案 3 和 4 是合理的期望,而没有人想要答案 1 和 2。
说到答案 4,什么时候高峰不是高峰?短时间的峰值何时变成慢性?这样的系统应该如何优先考虑发布者而不是消费者?这是一个艰难的选择,也是一个难以有效实现的挑战。RabbitMQ 更倾向于答案 3:限制发布者速率,并尽可能平衡发布和消费速率。
这一切都归结为流量控制。
选择合适的飞行中限制(in-flight limit)和预取(prefetch)
如果你从不期望重负载,那么这个决定很简单。在上一篇文章中,我们看到对于单个高吞吐量队列,你可以设置较高的飞行中限制,较高的预取,并且可以选择使用 consumer acknowledgements 的 multiple 标志,你这样做效果会不错。如果负载很低,那么所有设置对最终的吞吐量和延迟数字来说可能看起来都差不多。
但如果你期望有重负载时期,并且有成百上千个客户端,那么这仍然是一个好选择吗?我知道回答这些问题的最好方法是运行测试,进行各种参数的许多许多测试。
所以我们将进行一系列基准测试,其中包含不同的
- 发布者数量
- 队列数量
- 消费者数量
- 发布速率
- 飞行中限制
- 预取和确认间隔
我们将测量吞吐量和延迟。飞行中限制将是每个发布者目标速率的百分比,百分比在 1% 到 200% 之间。例如,对于每个发布者目标速率为 1000
- 1% 飞行中限制 = 10
- 5% 飞行中限制 = 50
- 10% 飞行中限制 = 100
- 20% 飞行中限制 = 200
- 100% 飞行中限制 = 1000
- 200% 飞行中限制 = 2000
和上一篇文章一样,我们将测试镜像队列和集群队列。镜像队列为一主一镜(复制因子 2),集群队列为一主两副(复制因子 3)。
所有测试都使用 RabbitMQ 3.8.4 的 alpha 版本,该版本改进了集群队列处理高负载的内部机制。此外,我们将保守使用内存,并将集群队列的 `x-max-in-memory-length` 属性设置为一个较低的值。这使得集群队列有点像惰性队列(lazy queue),一旦消息体被认为是安全的,并且队列长度达到此限制,它就会尽快从内存中移除。没有此限制,集群队列会将所有消息保留在内存中。如果消费者跟不上,这可能会降低性能,因为会有更多的磁盘读取,但这是一个更安全、更保守的配置。当我们将系统推向极限时,这会变得很重要,因为它避免了内存的大幅飙升。在这些测试中,它被设置为 0,这是最激进的设置。
所有测试都在 3 节点集群上进行,使用的是 16 vCPU(Cascade Lake/Skylake Xeon)的机器,配备 SSD。
基准测试
- 20 个发布者,1000 条消息/秒,10 个队列,20 个消费者,1kb 消息
- 20 个发布者,2000 条消息/秒,10 个队列,20 个消费者,1kb 消息
- 500 个发布者,30 条消息/秒,100 个队列,500 个消费者,1kb 消息
- 500 个发布者,60 条消息/秒,100 个队列,500 个消费者,1kb 消息
- 1000 个发布者,100 条消息/秒,200 个队列,1000 个消费者,1kb 消息
基准测试 #1:20 个发布者,每个发布者 1000 条消息/秒,10 个队列,20 个消费者
总目标速率为 20000 条消息/秒,这在所选硬件上的客户端和队列数量下,处于集群的总吞吐量限制范围内。这种负载对于此集群是可持续的。
我们有两个测试
- 无发布者确认
- 带有飞行中限制的确认,限制为目标发送速率的百分比:1% (10), 2% (20), 5% (50), 10% (100), 20% (200), 100% (1000)。
无确认的镜像队列

发布者没有比集群所能承受的更用力地驱动集群。我们获得了与目标速率相匹配的平稳吞吐量,延迟低于一秒。
带有确认的镜像队列

在此负载水平下,所有飞行中设置的行为都相同。我们离代理的极限还远着呢。
无确认的集群队列

达到目标速率,延迟低于一秒。
带有确认的集群队列

使用确认,并且飞行中限制较低时,集群队列略低于目标速率,但在所有百分位数下都达到了 < 200ms。随着我们增加飞行中限制,目标速率被达到,出现一条平稳的线,但延迟增加,尽管仍低于一秒。
结论
当发布速率在集群容量范围内能够传递给消费者时,低飞行中限制的确认提供了最佳的端到端延迟,而无确认或高飞行中限制的确认提供了目标吞吐量,但延迟更高(尽管仍低于一秒)。
基准测试 #2:20 个发布者,每个发布者 2000 条消息/秒,10 个队列,20 个消费者
总目标速率为 40000 条消息/秒,这大约是或高于所选硬件上集群的吞吐量极限。这种负载对于此集群来说可能是不可持续的,但可能发生在高峰负载条件下。如果需要持续运行,则建议使用更强大的硬件。
我们有三个测试
- 无发布者确认
- 带有飞行中限制的确认,限制为目标发送速率的百分比:1% (20), 2% (40), 5% (100), 10% (200), 20% (400), 100% (2000)。预取值为 2000,确认间隔为 1。
- 同上,但消费者使用 multiple 标志,确认间隔为 200(预取的 10%)。
无确认的镜像队列

发布者短暂接近目标速率,但发布者和消费者速率都稳定在较低的水平,发布速率超过消费者速率。这导致队列填满,延迟急剧上升。如果这种情况持续下去,队列会变得非常大,并给资源使用带来越来越大的压力。
带有确认的镜像队列

带有确认和 multiple 标志使用的镜像队列

确认现在确实产生了显著影响,对发布者施加了有效的反压(back pressure)。我们达到了峰值吞吐量(仍然远低于目标),最低的飞行中限制为 20(目标速率的 1%)。端到端延迟较低,约为 20ms。但随着我们增加飞行中限制,一小部分队列开始填满,导致 95% 百分位延迟飙升。
我们看到使用 multiple 标志在飞行中限制较高时,可以减少发布到消费速率的不平衡,从而在一定程度上降低最差的延迟。但在这个案例中的效果不是很强。
无确认的集群队列

当队列数量较少时,集群队列的性能通常优于镜像队列。在这里我们看到达到了 40000 条消息/秒,因此不需要对发布者进行反压。
带有确认的集群队列

带有确认和 multiple 标志使用的集群队列

集群队列再次提供了更高的吞吐量,我们甚至以 2000 的飞行中限制达到了 40000 条消息/秒的目标速率。使用 multiple 标志带来了轻微的好处。
结论
如果没有发布者确认和飞行中限制的反压,镜像队列就会崩溃。当发布者使用确认时,它们有效地对发布者施加了反压,实现了低延迟,直到飞行中限制达到目标速率的 100%,此时延迟再次开始飙升。需要注意的是,这个目标速率超过了镜像队列的容量,我们看到了反压的重要性。
当队列数量和发布者数量相对较少时,集群队列可以实现比镜像队列更高的吞吐量。它们能够提供 40000 条消息/秒的速率,因此使用确认或不使用确认对稳定性能并不关键。
multiple 标志的使用是有益的,但不是决定性的。
基准测试 #3:500 个发布者,每个发布者 30 条消息/秒,100 个队列,500 个消费者
总目标速率为 15000 条消息/秒,这在所选硬件上的集群总吞吐量限制范围内。
我们有两个测试
- 无发布者确认
- 带有飞行中限制的确认,限制为目标发送速率的百分比:6% (2), 10% (3), 20% (6), 50% (12), 100% (30), 200% (60),且不使用 multiple 标志。
无确认的镜像队列

带有确认的镜像队列

无确认的集群队列

带有确认的集群队列

在所有情况下,我们都达到了目标速率。使用确认和较低的飞行中限制时,吞吐量有一些小的抖动,在更高的限制下得到了解决。
随着我们增加飞行中限制,延迟逐渐增加。镜像队列超过了 1 秒,而集群队列保持在 1 秒以内。
再次,我们看到当集群在其容量范围内时,我们不需要确认作为反压机制(仅用于数据安全)。
基准测试 #4:500 个发布者,每个发布者 60 条消息/秒,100 个队列,500 个消费者
总目标速率为 30000 条消息/秒,这略高于所选硬件上此客户端和队列数量的集群总吞吐量限制。这将给集群带来压力,并且不是此集群应该承受的可持续负载。
我们有三个测试
- 无发布者确认
- 带有飞行中限制的确认,限制为目标发送速率的百分比:5% (3), 10% (6), 20% (12), 50% (24), 100% (60), 200% (120),预取值为 60。
- 同上,但使用 multiple 标志,确认间隔为 6(预取的 10%)。
无确认的镜像队列

没有确认,发布者短暂地达到了目标速率,但消费者跟不上。吞吐量非常不稳定,一半队列的延迟接近 1 分钟,另一半队列达到 2-3 分钟以上。
带有确认的镜像队列

带有确认和 multiple 标志使用的镜像队列

使用确认,我们获得了更稳定的吞吐量,因为发布者受到其飞行中限制的速率限制,消费者能够跟上发布速率。multiple 标志这次确实有帮助,将吞吐量提高了 5000 条消息/秒。请注意,仅为目标速率 3% 的飞行中限制就能带来最佳性能。
无确认的集群队列

发布者达到了他们的目标,但消费者跟不上,队列正在填满。这不是一个可持续的状态。
带有确认的集群队列

带有确认和 multiple 标志的集群队列

使用发布者确认,我们看到了更稳定的吞吐量,但明显存在锯齿状模式。我们可以将飞行中限制提高到目标速率的 100%,而不会导致系统崩溃,尽管延迟在稳步上升。在 200% 时,发布速率超过消费速率,队列开始填满。这时,集群队列的 `x-max-in-memory-length` 属性就需要一个较低的值。没有它,内存使用会在这种情况下迅速飙升,导致内存警报反复出现,从而引起吞吐量的巨大波动。
结论
当集群超出其极限时,使用发布者确认和飞行中限制可以确保发布和消费速率的平衡。即使发布者会更快,他们也会自我限制速率,RabbitMQ 可以提供长期的可持续性能。
当发布者、消费者和队列数量很大时,镜像队列和集群队列的最大吞吐量已经收敛到相似的数字。集群队列不再优于镜像队列。我们看到在客户端和队列数量较少时,吞吐量更高。数量少意味着更少的上下文切换,更少的随机 I/O,所有这些都更有效率。
基准测试 #5:1000 个发布者,每个发布者 100 条消息/秒,200 个队列,1000 个消费者
这个负载远远超过了此集群能够处理的范围,总目标速率为 100000 条消息/秒,分布在 200 个队列上。超出少量队列(10 个左右)后,随着队列数量的增加,集群的最大吞吐量会下降。
如果此集群受到如此大的负载,那也应该只持续很短的时间。
我们有三个测试
- 无确认
- 带有飞行中限制的确认,限制为目标发送速率的百分比:2% (2), 5% (5), 10% (10), 20% (20), 50% (50), 100% (100),预取值为 100。
- 同上,但使用 multiple 标志,确认间隔为 10(预取的 10%)。
无确认的镜像队列

发布者几乎达到了目标速率,但然后代理内部的缓冲区开始达到容量,吞吐量急剧下降。依赖 TCP 反压,在默认的基于信用的流量控制设置下,1000 个发布者以高于集群处理能力的速度发送,效果并不理想。
每个信用链中的 actor 的初始信用为 400,因此每个连接上的读取器进程将接受至少 400 条消息,然后才会被阻塞。有 1000 个发布者,仅读取器进程中就缓冲了 400,000 条消息。再加上通道和队列的缓冲区,以及所有出站端口缓冲区等等,你就可以看到,即使在 TCP 反压生效之前,代理如何能够吸收并随后被大量发布者发送的大量消息所窒息。
带有确认的镜像队列

带有确认和 multiple 标志使用的镜像队列

发布者很想达到目标速率,但它们受到了有效的速率限制。随着我们增加飞行中限制,吞吐量略有增加,延迟大幅增加。最终,当我们达到目标速率 200% 的飞行中限制时,就太多了,但发布者仍然受到节流。队列会稍微堆积,吞吐量下降,变得非常不稳定。使用 multiple 标志有帮助,它减轻了下降幅度,并将延迟保持在 25 秒以下。
如果我们查看 RabbitMQ Overview Grafana 仪表板(为在此处显示稍作修改),我们会看到当飞行中限制较低时,待确认消息和待确认消费者数量很少,但当飞行中限制达到 100% 时,这些数字达到 100,000。因此 RabbitMQ 在内部缓冲了更多消息。消费者还没有达到它们的预取限制,尽管达到峰值时为 55,000,总可能预取量为 100,000。

无确认的集群队列

与镜像队列相同。TCP 反压不足以阻止过载。
带有确认的集群队列

带有确认和 multiple 标志使用的集群队列

从低到中等飞行中限制的转变中,集群队列比镜像队列受益更多。使用 multiple 标志时,我们甚至达到了接近 35000 条消息/秒的吞吐量。在目标速率的 100% 限制时开始出现问题,然后在 200% 时情况变得非常糟糕。发布者超前,导致队列填满。这时,集群队列的 `x-max-in-memory-length` 属性需要一个低值。没有它,在这种情况下内存使用会非常快地飙升,导致吞吐量的大幅波动,因为内存警报反复开启和关闭。
在即将发布的 3.8.4 版本中,我们在集群队列的内存使用方面进行了重大改进,特别是在高负载下。所有这些测试都展示了这项工作的成果。在本文档的最后,我们将展示使用 3.8.3 进行的相同测试,以及它在这类压力测试下的表现。
在 Overview 仪表板中,我们看到队列是如何填满的。消费者已经达到了它们的预取限制。

结论
没有发布者确认,两种队列类型都无法处理此负载。每个集群都完全不堪重负。
使用确认后,在 100% 和 200% 的飞行中限制之前,镜像队列和集群队列的吞吐量和延迟大致相同,但在 100% 和 200% 的飞行中限制下,集群队列表现更差。
镜像队列在过载情况下表现相当好,即使有较高的飞行中限制。集群队列在压力测试中需要较低的飞行中限制才能实现稳定的吞吐量和低延迟。
3.8.3 及更早版本呢?
所有集群队列的测试都是在 3.8.4 的 alpha 版本上进行的,以展示即将发布的 3.8.4 版本的性能。但是你们其他人将使用 3.8.3 及更早的版本。那么你们可以期待什么?
3.8.4 版本中的改进包括:
- 段文件写入的高吞吐量。消息首先写入 WAL(Write-Ahead Log),然后写入段文件。在 3.8.3 中,我们发现段写入器在高负载、高队列数量的情况下是一个瓶颈,这会导致高内存使用。3.8.4 版本采用了并行段写入,完全解决了这个瓶颈。
- 集群队列的默认配置值经过了负载测试,我们发现一些更改在高负载下产生了更稳定的吞吐量。具体来说,我们将 `quorum_commands_soft_limit` 从 256 更改为 32,将 `raft.wal_max_batch_size` 从 32768 更改为 4096。
如果你使用的是 3.8.3 版本,好消息是现在可以轻松执行滚动升级。但如果无法升级,可以尝试上述配置。不过,你仍然可能会遇到段写入器这个潜在的瓶颈。
下面是基准测试 #5,运行时间更长,使用 3.8.3 版本(应用了配置更改)。
3.8.3 基准测试 #5

3.8.3 版本的主要区别在于,随着飞行中限制的增加,段写入器跟不上,内存不断增长,直到触发内存警报。发布者被阻塞,而消费者在与发布者竞争将它们的确认写入复制日志时不再受到限制。消费速率达到高达 90k 条消息/秒的短暂峰值,直到队列被清空,内存下降,警报解除,然后再次重复。
我们可以从 Overview 仪表板中看到这一点。3.8.4 alpha 版本在飞行中限制增加时,内存增长缓慢而稳定。

3.8.3 版本反复触发内存警报。

即使在低飞行中限制下,这种来自 1000 个发布者的繁重工作负载对段写入器来说也太大了,并且在测试初期就接近内存警报。
因此,如果你有大量的发布者和队列,并且经常出现超出其极限的负载峰值,那么请考虑在 3.8.4 版本发布后升级。
最终结论
首先,如果你使用的是复制队列(镜像或集群),那么不使用发布者确认在数据安全方面是极不推荐的。消息传递不保证,所以请使用它们。
抛开数据安全不谈,这些测试表明确认(confirms)在流量控制方面也起着重要作用。
一些关键要点:
- 当队列数量大约为每核心 1-2 个时,集群队列可以提供比镜像队列更高的吞吐量。
- 在发布者和队列数量较少的情况下,你可以几乎为所欲为。TCP 反压可能足以满足镜像队列和集群队列(不使用确认)。
- 在发布者和队列数量较多且负载较高的情况下,TCP 反压不足。我们必须使用发布者确认,以便发布者能够自我限制速率。
- 在发布者和队列数量较多的情况下,两种队列类型的性能大致相似。但集群队列在压力测试中需要通过较低的飞行中限制来获得额外的帮助。
- multiple 标志的使用是有益的,但不是必需的。
- 无论你做什么,都不要在没有发布者确认的情况下将代理置于高负载下!
那么,最佳的飞行中限制是多少?我希望我已经说服了你“这取决于情况”,但作为经验法则,在发布者和代理之间的网络延迟较低的情况下,使用目标速率 1% 到 10% 的限制是最佳选择。对于发布者数量较少但发送速率较高的情况,我们倾向于 10%,而对于数百个客户端,我们则倾向于 1% 的标记。在发布者和代理之间延迟较高的链路上,这些数字可能会增加。
关于消费者预取,所有这些测试都使用了目标发布速率(每个发布者,而不是总计)的预取值,但请记住,在这些测试中,发布者数量与消费者数量匹配。当使用 multiple 标志时,确认间隔为预取值的 10%。multiple 标志的使用是有益的,但如果你不使用它,问题也不大。
如果你目前使用的是镜像队列,并且你的工作负载更接近于基准测试 #5 而不是其他任何测试,那么建议在 3.8.4 发布后进行迁移。改进流量控制和负载下的弹性很可能是一项持续的工作,但很多情况下也取决于具体的工作负载。希望你已经看到,通过使用确认,你可以调整吞吐量和延迟,并获得所需行为。
如果我不提容量规划,那将是不够的。确保 RabbitMQ 拥有足够的硬件来处理峰值负载是确保其能够提供可接受性能的最佳方法。但总会有意外的负载,预算限制等等。
请记住,与所有此类基准测试一样,不要过分关注这些具体数字。你的情况会不同。不同的硬件、不同的消息大小、不同的扇出程度、不同的 RabbitMQ 版本、不同的客户端、框架……等等。主要 takeaway 是,你不应该期望 RabbitMQ 在高负载下能够自行进行流量控制。这一切都与“机械同情心”有关。
下一篇系列文章将探讨从镜像队列迁移到集群队列。