集群规模案例研究 – 仲裁队列第 2 部分
在上一篇文章中,我们开始使用仲裁队列对我们的工作负载进行规模分析。我们重点关注了消费者能够跟上消息处理的理想情况,这意味着没有队列积压,并且集群中的所有代理都正常运行。通过运行一系列模拟不同强度工作负载的基准测试,我们确定了按每月每 1000 条消息/秒的成本计算的前 5 个集群规模和存储容量组合。
- 集群:7 个节点,8 个 vCPU(c5.2xlarge),gp2 SSD。成本:54 美元
- 集群:9 个节点,8 个 vCPU(c5.2xlarge),gp2 SSD。成本:69 美元
- 集群:5 个节点,8 个 vCPU(c5.2xlarge),st1 HDD。成本:93 美元
- 集群:5 个节点,16 个 vCPU(c5.4xlarge),gp2 SSD。成本:98 美元
- 集群:7 个节点,16 个 vCPU(c5.4xlarge),gp2 SSD。成本:107 美元
需要进行更多测试以确保这些集群能够处理诸如代理故障和在停机或系统速度下降期间积累的大量积压等情况。
所有仲裁队列都使用以下属性声明
- x-quorum-initial-group-size=3
- x-max-in-memory-length=0
x-max-in-memory-length 属性强制仲裁队列在安全的情况下立即将消息体从内存中移除。您可以将其设置为更长的限制,这是最积极的设置 - 旨在避免大型内存增长,但代价是在消费者无法跟上的情况下进行更多磁盘读取。如果没有此属性,消息体将始终保留在内存中,这可能会导致内存增长到触发内存警报的程度,从而严重影响发布速率 - 在此工作负载案例研究中,我们希望避免这种情况。
不利条件 – 处理滚动重启和丢失的代理
我们使用仲裁队列是因为我们关心我们的数据和可用性。如果由于磁盘故障或由于我们需要作为紧急操作系统补丁的一部分重新引导而丢失了代理,那么我们将获得持续的可用性和无数据丢失,但我们能否保持我们目标的 30k 消息/秒的峰值速率?弹性不仅关乎不丢失数据和保持可用,还关乎充分处理负载。
为此,我们再次运行完全相同的测试,但在每个强度级别进行过程中强制杀死一个代理。
在使用镜像队列(一个主节点,一个镜像)的同一测试中,我们发现当一个代理被杀死时吞吐量下降了。使用仲裁队列,我们没有看到如此强烈的影响。
让我们看一下我们的 30k 消息/秒的目标速率周期。
我们看到丢失代理对镜像队列产生较大影响的原因是,镜像队列尝试通过在丢失主节点或镜像时在另一个代理上创建新的镜像来维护冗余级别。这将相同数量的消息流量集中到更少的服务器上。仲裁队列不会这样做。如果托管队列副本的代理丢失,则该队列的成员资格不会更改。只要大多数队列副本(领导者、跟随者)可用,队列就会继续运行。一旦代理重新上线,该代理上的其跟随者副本将开始再次进行复制。
因此,复制流量不会集中在更少的服务器上,只有客户端流量会集中。仲裁队列还受益于以下事实:如果消费者碰巧连接到托管他们想要使用的队列的跟随者副本的代理,那么他们可以直接从该副本读取 - 不会将消息从托管领导者的代理代理到消费者连接到的代理。
镜像队列尝试通过创建新的镜像来维护冗余级别这一事实被同步是阻塞的事实所削弱。因此,许多管理员使用手动同步来避免在剩余的代理之间出现巨大的流量峰值,因为新的镜像正在被复制。
最大的集群(7x16、7x8 和 9x8)没有看到丢失代理的明显影响。故障转移到新的领导者速度很快,吞吐量像以前一样继续。
不利条件 - 消费者速度下降
在处理消息时,消费者通常需要与其他系统(如数据库或第三方 API)交互。这些下游系统可能会因沉重的负载或某种类型的中断而速度下降,这会对消费者的速度产生连锁反应。然后,这会导致队列中的消息数量增加,进而也可能影响发布者。当队列很小或为空时(由于消息被立即消费而为空),RabbitMQ 可以提供最佳性能。
我们的要求规定,如果我们遇到消费者速度下降,发布应不受影响,即使在 30k 消息/秒的目标峰值负载下也是如此。
在此测试中,每条消息的处理时间有所不同
- 5 分钟,10 毫秒
- 在 20 分钟内从 10 毫秒增加到 30 毫秒
- 5 分钟,30 毫秒
- 在 20 分钟内从 30 毫秒减少到 10 毫秒
- 50 分钟,10 毫秒
由于这是一个高流量系统,积压会很快形成,因此消息积压可能会增长到数千万条。随着处理时间首先增加,然后减少,我们将看到消费速率呈 S 形,然后随着消费者处理积压,消费速率超过发布速率。
当消费速率恢复但队列长度仍然非常大时,这时我们可能会看到对发布者的影响。发布速率可能会下降一段时间,直到积压清除为止。性能更高的集群应该不会受到影响或只受到短暂的影响。
我们将在三种不同的发布速率下运行测试
- 10k 消息/秒,200 个消费者分布在 100 个队列中。最高消费速率为 20k 消息/秒,然后在 30 毫秒的处理时间下降到 6.6k 消息/秒。
- 20k 消息/秒,300 个消费者分布在 100 个队列中。最高消费速率为 30k 消息/秒,然后在 30 毫秒的处理时间下降到 10k 消息/秒。
- 30k 消息/秒,400 个消费者分布在 100 个队列中。最高消费速率为 40k 消息/秒,然后在 30 毫秒的处理时间下降到 13.3k 消息/秒。
首先要注意的是,仲裁队列的表现比镜像队列好得多。使用镜像队列,在本次测试中,没有哪个集群能够保持 30k 消息/秒的发布速率,但使用仲裁队列,7x16 集群勉强能够处理。
查看一些队列积压变得多么大的示例。
3x16 集群
7x16 集群
9x8 集群
尽管 100 个队列中的队列积压高达 2500 万条,但内存使用量仍远低于高水位线(即内存警报会阻止发布者的水位线)。
所有集群都处理了 10k 消息/秒的测试。在 20k 消息/秒时,只有两个集群处理了它(7x16 和 9x8)。7x16 集群是本次测试中的明显赢家,因为它设法处理了非常紧张的 30k 消息/秒测试,该测试产生了高达 2500 万条消息的积压。
您可以使用 PerfTest(从版本 2.12 及更高版本)运行类似的测试
bin/runjava com.rabbitmq.perf.PerfTest \
-H amqp://guest:[email protected]:5672/%2f,amqp://guest:[email protected]:5672/%2f,amqp://guest:[email protected]:5672/%2f \
-z 1800 \
-f persistent \
-q 1000 \
-c 1000 \
-ct -1 \
-ad false \
--rate 100 \
--size 1024 \
--queue-pattern 'perf-test-%d' \
--queue-pattern-from 1 \
--queue-pattern-to 100 \
-qa auto-delete=false,durable=false,x-queue-type=quorum \
--producers 200 \
--consumers 200 \
--producer-random-start-delay 30 \
-vl 10000:300 \
-vl 11000:60 -vl 12000:60 -vl 13000:60 -vl 14000:60 -vl 15000:60 -vl 16000:60 -vl 17000:60 -vl 18000:60 -vl 19000:60 \
-vl 20000:60 -vl 21000:60 -vl 22000:60 -vl 23000:60 -vl 24000:60 -vl 25000:60 -vl 26000:60 -vl 27000:60 -vl 28000:60 -vl 29000:60 \
-vl 30000:300 \
-vl 29000:60 -vl 28000:60 -vl 27000:60 -vl 26000:60 -vl 25000:60 -vl 24000:60 -vl 23000:60 -vl 22000:60 -vl 21000:60 -vl 20000:60 \
-vl 19000:60 -vl 18000:60 -vl 17000:60 -vl 16000:60 -vl 15000:60 -vl 14000:60 -vl 13000:60 -vl 12000:60 -vl 11000:60 -vl 10000:60 \
-vl 10000:3000
不利条件 – 发布速率峰值超过消费者容量
与消费者速度下降一样,我们最终会遇到发布速率超过消费速率导致消息积压的情况。但这次是由发布速率的大幅峰值造成的,我们的后端系统无法处理。吸收发布速率的峰值是选择消息队列的原因之一。您不需要扩展后端系统以处理峰值负载(这可能很昂贵),而是允许消息队列吸收额外的流量。然后,您可以在一段时间内处理积压。
在此测试中,我们将处理时间保持在 10 毫秒,但会增加发布速率,然后降低它
- 5 分钟,基本速率
- 在 20 分钟内从基本速率增长到峰值
- 5 分钟,峰值。
- 在 20 分钟内从峰值降低到基本速率
- 50 分钟,基本速率
我们将进行三个测试
- 10 k 消息/秒基本发布速率,20k 消息/秒峰值。200 个消费者,最高消费速率为 13k 消息/秒。
- 20 k 消息/秒基本发布速率,30k 消息/秒峰值。300 个消费者,最高消费速率为 23k 消息/秒。
- 30 k 消息/秒基本发布速率,40k 消息/秒峰值。400 个消费者,最高消费速率为 33k 消息/秒。
除了 3x16 和 5x8 集群之外,所有集群都设法达到了 20k 消息/秒的发布速率峰值。
只有较大的 5x16、7x18 和 9x8 集群设法达到了 30k 消息/秒的峰值。
7x16 集群勉强达到了 40k 消息/秒的峰值,并且其消息积压接近 600 万条消息,但它仍然处理了它。
您可以使用 PerfTest 运行类似的测试
bin/runjava com.rabbitmq.perf.PerfTest \
-H amqp://guest:[email protected]:5672/%2f,amqp://guest:[email protected]:5672/%2f,amqp://guest:[email protected]:5672/%2f \
-z 1800 \
-f persistent \
-q 1000 \
-ct -1 \
-ad false \
-c 1000 \
--size 1024 \
--queue-pattern 'perf-test-%d' \
--queue-pattern-from 1 \
--queue-pattern-to 100 \
-qa auto-delete=false,durable=false,x-queue-type=quorum \
--producers 200 \
--consumers 200 \
--producer-random-start-delay 30 \
--consumer-latency 10000 \
-vr 100:300 \
-vr 102:60 -vr 104:60 -vr 106:60 -vr 108:60 -vr 110:60 -vr 112:60 -vr 114:60 -vr 116:60 -vr 118:60 -vr 120:60 \
-vr 122:60 -vr 124:60 -vr 126:60 -vr 128:60 -vr 130:60 -vr 132:60 -vr 134:60 -vr 136:60 -vr 138:60 -vr 140:60 \
-vr 142:60 -vr 144:60 -vr 146:60 -vr 148:60 -vr 150:60 \
-vr 148:60 -vr 146:60 -vr 144:60 -vr 142:60 -vr 140:60 -vr 138:60 -vr 136:60 -vr 134:60 -vr 132:60 -vr 130:60 \
-vr 128:60 -vr 126:60 -vr 124:60 -vr 122:60 -vr 120:60 -vr 118:60 -vr 116:60 -vr 114:60 -vr 112:60 -vr 110:60 \
-vr 108:60 -vr 106:60 -vr 104:60 -vr 102:60 -vr 100:60 \
-vr 100:3000
不利条件测试 – 结论
在执行理想条件测试后,我们有很多集群可以处理峰值负载,因此我们最终根据每月每 1000 条消息/秒的成本,得出了前 5 名集群的排行榜。现在,在运行了不利条件测试后,我们从最初的集合中缩减到了两个潜在的集群。7x16 是唯一一个处理所有测试的集群,但如果您也对成本敏感,那么更便宜的 9x8 几乎通过了积压测试。
- 集群:7 个节点,16 个 vCPU,gp2 SSD。成本:每 1000 条消息/秒 104 美元
- 集群:9 个节点,8 个 vCPU,gp2 SSD。成本:每 1000 条消息/秒 81 美元
在理想条件下,扩展较小的虚拟机为我们提供了最佳的吞吐量和成本效益。但在考虑不利条件时,7x16 是最佳的全面解决方案。
对于此工作负载,我将选择仲裁队列(使用 gp2 卷)而不是镜像队列,因为它们在消息积压的情况下能够继续处理入口速率的能力更强。当然,除了这一点之外,还有其他选择仲裁队列的理由。
- 更好的数据安全
- 处理滚动重启时更高的可用性
仲裁队列案例研究要点
要点与镜像队列案例研究相同:不要只在理想条件下运行测试。确保将不利条件测试纳入您的方法,否则您可能会发现您的集群在您最需要它的时候无法处理您的工作负载。系统在负载过重时更容易出现此类问题,因此请在您预期的峰值负载水平及以上进行测试。
底线是 RabbitMQ 可以很好地处理代理丢失,它更难以应对的是队列积压。我们的顶级集群,7x16 和 9x8 配置在理想条件下达到 65-70k 消息/秒,但在我们施加的最不利条件下仅达到 20-30k 消息/秒。我说只有 20-30k 消息/秒,但这每天是 17-25 亿条消息,高于 RabbitMQ 的大多数用例。
最后...这是一个特定的工作负载,请查看第一篇文章中的其他建议,这些建议可能适用于其他工作负载和场景。