跳至主内容

仲裁队列和流量控制 - 概念

·8 分钟阅读
Jack Vanlightly

作为我们仲裁队列系列的一部分,我们将深入探讨流量控制,了解它如何保护 RabbitMQ 免受过载影响,以及它与仲裁队列的关系。

什么是流量控制?

流量控制是一个在计算机网络和网络软件中存在了数十年的概念。本质上,它是一种机制,通过向发送方施加反压来避免接收方过载。接收方通常会将传入的数据包/消息缓冲起来,以应对发送速率超过其处理速率的情况。但接收方缓冲区不能无限增长,因此发送速率只能暂时超过接收方的处理能力(突发流量),或者必须减缓发送方的速度(反压)。

流量控制是一种对发送方施加这种反压的方法,减缓它们的发送速度,以防止接收方的缓冲区溢出并避免延迟过大。在一个发送方/接收方的链条中,这种反压可以沿着链条向上传播到流量的源头。在更复杂的连接组件图中,流量控制可以在快速和慢速发送方之间平衡传入流量,避免过载,同时允许系统在发送方数量、速率和负载模式(稳定或突发)不同时达到充分利用。

RabbitMQ 中的流控

RabbitMQ 的运行机制很像一个网络。每个 RabbitMQ 代理(broker)在内部都采用 Actor 模型实现,不同的组件通过消息传递进行通信,有时在本地,有时通过网络。此外,还有通过网络向代理发送消息的发布者,以及从代理接收消息的消费者。

Fig 1. A simplified depiction of message flows
图 1. 消息流的简化示意图

在此链条的任何环节,都可能出现导致拥塞的瓶颈。拥塞会导致缓冲区和端口被填满、磁盘数据增长以及硬件资源短缺。如果任其发展,RabbitMQ 可能会耗尽所有可用内存,最终导致崩溃或被操作系统杀掉。

从整个系统(代理和客户端)来看,我们有四种可用的流控机制:

  • 基于信用的流控 (Credit based flow control)
  • 内存告警 (Memory alarms)
  • 发布者确认 (Publisher confirms)
  • 消费者确认与预取 (Consumer acknowledgements and prefetch)

基于信用的流控

基于信用的流控是一种限制消息流入速率的方法。它允许系统内的各个组件保护自身,并在无法快速处理消息时施加反压。它仅针对那些出现问题的连接、通道和队列,从而不影响系统的其他部分。

其工作原理是,系统内每个处理消息的组件都使用“信用额度”(credit)作为向上游施加反压的方式。如果一个通道想要向队列发送消息,它就需要信用额度。队列会授予通道一些初始信用额度,之后,通道向队列发送的每条消息都需要消耗一个信用额度。当队列成功将消息传递给持久化层后,它会定期向通道授予更多的信用额度。如果通道没有信用额度,它将被禁止向队列发送消息,直到队列授予更多额度。通过这种方式,通道无法对队列造成过度负载。

Fig 2. Credit based flow control with classic queues
图 2. 经典队列中的基于信用的流控

因此,我们拥有一个信用流控链,可以将反压一直传递回发布者。最终,由于 TCP 读取进程因被阻塞而无法从套接字读取数据,TCP 反压将施加给发布者。

当连接、通道或队列用尽信用额度时,它们会被阻塞,直到获得更多额度,这种状态被称为“流控”(flow)。在管理界面中,你可能会看到连接、通道或队列处于 flow 状态,这表明最近发生了流控。这仅意味着它们暂时用尽了信用额度,正在等待链条中的下一个环节赶上并授予一些额度。这种情况每秒可能会触发多次。

Fig 3. Credit exhaustion.
图 3. 信用耗尽。

Fig 4. Credit grants.
图 4. 信用授予。

当队列或连接达到其吞吐量限制或遇到下游瓶颈时,流控状态可能会每秒在链条的不同位置触发多次,因为不同组件的信用额度会达到 0,然后再得到补充。

但这并不一定能阻止代理耗尽内存。传入的消息并不总是导致高内存占用的唯一主要原因,大型队列和其他多种因素也可能导致该问题。

内存告警

如果基于信用的流控无法有效刹车,或者内存使用量因其他原因达到了临界水平,内存告警将作为最后一道防线触发,以防止代理因内存耗尽而崩溃(或被操作系统杀掉)。

内存告警触发时,所有发布者都会被阻塞。这就像是关闭了整个集群中传入消息的水龙头。这不像基于信用的流控那样有针对性,而是“一刀切”的强力手段。

不过,消费者仍然可以继续消费,此时的预期是通过排空队列来降低内存占用。

在管理界面中,当内存告警生效时,你会看到连接状态显示为 blocked(阻塞)或 blocking(正在阻塞)。

发布者确认

发布者确认的主要职责是数据安全,但它们在流控中也发挥着重要作用。

采用发布者确认有三种方式:

  • 逐条发送,等待每一条确认后再发送下一条(速度非常慢)。
  • 窗口化。发送消息直到达到窗口大小(时间或消息数量),然后等待所有确认后再发送下一个窗口。
  • 流水线化(Pipelining)。允许发布者持续发送消息,但当未确认消息数(传输中的消息)达到限制时进行阻塞。当确认返回时,可以继续发送消息,直到再次达到限制。

流水线化(或简单的异步)方法提供了最高且最稳定的吞吐量。它可以用作防止代理过载的额外保护,因为发布者在给代理施压之前,自身就已经进入了“流控”状态。

如果不使用发布者确认,你只能依赖发布者与代理连接读取进程之间的 TCP 流控。在发布者数量相对较少的情况下,TCP 流控足以避免代理过载;但当客户端数量庞大时,TCP 流控就不够了,发布者确认对于重负载下的集群稳定性变得至关重要。有趣的是,AMQP 1.0 添加了链路流控来解决这个问题。

消费者确认与预取

使用手动确认和预取(prefetch)功能会对 RabbitMQ 施加反压,防止其淹没消费者客户端。它使用流水线方法发送连续的消息流,但将未确认消息的数量限制为预取大小(QoS)。在自动确认(AutoAck)模式下,我们再次仅依赖 TCP 反压。客户端的各种输入缓冲区可能很快就会被填满。

强烈建议使用手动确认和预取。

仲裁队列

仲裁队列的基于信用的流控链有所不同,因为它们拥有完全不同的持久化和复制协议。通道将所有消息发送到 Raft 集群领导者,领导者再将消息传递到预写式日志(WAL)并将其复制给追随者。仲裁队列用于复制的 Raft 共识算法不包含基于信用的流控,因此该机制在通道处结束。

当 Raft 领导者无法跟上进度时,通道不会使用信用额度作为自我阻塞的机制,而是会追踪领导者尚未应用的任务挂起的 Raft 命令数量。如果该数量超过了 *quorum commands soft limit*(仲裁命令软限制)配置,通道就会停止向读取进程授予更多的信用额度。

Fig 5. Credit based flow control in quorum queues.
图 5. 仲裁队列中的基于信用的流控。

内存告警、发布者确认和消费者确认/预取等其他控制机制在仲裁队列中与经典队列相同。

即将发布的 3.8.4 版本对仲裁队列的改进

我们发现,与镜像队列相比,流控是仲裁队列较弱的领域之一。我们发现,在面对数百或数千个客户端的重负载下,仲裁队列应用反压的效果不如镜像队列。仲裁队列在这些场景下会出现一些新的瓶颈,可能导致内存增长,从而被迫依赖内存告警而不是基于信用的流控。

我们着手为下一个版本进行改进,并已极大提升了仲裁队列的这一方面。待 3.8.4 发布后,我们将撰写更多关于这些变更的内容。

下一部分……使用发布者确认在负载下提高吞吐量

现在你对 RabbitMQ、流控和仲裁队列有了更多的了解。你知道发布者确认对于数据安全以及发布者与代理之间的流控都很重要。但我们该如何实现流水线化?传输中的限制设置多大合适?不同的限制会对吞吐量和延迟产生什么影响?

所有这些问题都将在本系列的下一部分中得到解答。我们将对镜像队列和仲裁队列运行一系列基准测试,并给出答案。

© . This site is unofficial and not affiliated with VMware.