仲裁队列和流控制 - 概念
作为仲裁队列系列的一部分,我们将研究流控制,它如何保护 RabbitMQ 免于过载,以及这与仲裁队列有何关系。
什么是流控制?
流控制是一个在计算机网络和网络软件中存在了几十年的概念。本质上,它是一种对发送者施加反压以避免接收者过载的机制。接收者通常缓冲传入的数据包/消息,以此来处理发送速率超过其处理速率的情况。但是接收者缓冲区不能无限增长,因此发送速率应该只是短暂地超过接收者处理能力(突发流量),或者必须减慢发送者的速度(反压)。
流控制是一种对发送者施加这种反压的方式,减慢他们的速度,以便接收者的缓冲区不会溢出,并且延迟不会变得太大。在发送者/接收者的链中,这种反压可以向上游传播到流量的源头。在更复杂的连接组件图中,流控制可以平衡快速和慢速发送者之间的传入流量,避免过载,同时允许系统在不同数量的发送者、不同的速率和不同的负载模式(稳定或突发)下达到充分利用。
RabbitMQ 中的流控制
RabbitMQ 看起来很像一个网络。每个 RabbitMQ 代理在内部都使用 actor 模式实现,其中不同的组件通过消息传递相互通信,有时是本地的,有时是通过网络。还有发布者将消息发送到代理,以及消费者从代理接收消息,两者都通过网络。

在此链中的任何一点,都可能发生瓶颈,导致拥塞。拥塞会导致缓冲区和端口填满,磁盘上的数据增长,硬件资源不足。如果不对其进行控制,RabbitMQ 可能会耗尽所有可用内存,从而导致崩溃或被操作系统杀死。
将系统作为一个整体(代理和客户端)来看,我们有四种可用的流控制机制
- 基于信用的流控制
- 内存报警
- 发布者确认
- 消费者确认和预取
基于信用的流控制
基于信用的流控制是一种限制消息入口速率的方法。它允许系统内的各种 actor 保护自己,并在无法足够快地处理消息时施加反压。它仅针对那些存在问题的连接、通道和队列,而系统的其他部分不受影响。
其工作方式是,系统中处理消息的每个 actor 都使用“信用”作为向上游施加反压的方式。如果通道想要向队列发送消息,则需要信用。队列授予通道一些初始信用,然后在此之后,通道发送到队列的每条消息都需要一个信用。当队列能够将消息传递到持久层时,队列将定期授予通道更多信用。如果通道没有信用,则它将被阻止向队列发送消息,直到队列授予它更多信用。这样,通道就无法凌驾于队列之上。

因此,我们有一个信用流控制链,将反压一直施加回发布者。最终,TCP 反压将应用于发布者,因为 TCP 读取器进程由于被阻止而无法从套接字读取数据。
当连接、通道或队列用完信用时,它们将被阻止,直到授予更多信用,这种状态称为“流”。在管理 UI 中,您可能会看到连接、通道或队列处于流状态,这表明最近发生了流。这只是意味着它们暂时用完了信用,并且正在等待链中的下一个环节赶上并授予一些信用。这可能会每秒触发多次。


当队列或连接达到其吞吐量限制或下游瓶颈时,流状态可能会每秒多次启动,在链中的各个点,因为各个 actor 的信用额度达到 0,然后得到补充。
但这不一定会阻止代理耗尽内存。传入消息并不总是导致高内存使用率的唯一主要原因,也可能是由于大型队列和许多其他原因。
内存报警
如果基于信用的流控制无法充分制动,或者内存使用率由于其他原因增长到临界水平,则内存报警将作为最后的手段启动,以保护代理免于因内存耗尽而崩溃(或被操作系统杀死)。
当内存报警启动时,所有发布者都会被阻止。这就像您关闭了集群中传入消息的水龙头。不是基于信用的流控制的定向速率限制,而是一记重锤。
消费者可以继续消费,此时的希望是通过在某种程度上耗尽队列来开始降低内存占用。
在管理 UI 中,当内存报警生效时,您将看到连接被阻止或正在阻止。
发布者确认
发布者确认的主要工作是数据安全,但它们在流控制中也起着重要作用。
有三种使用发布者确认的方法
- 一次发送一个,等待每次确认后再发送下一个(非常慢)。
- 基于窗口。发送消息直到达到窗口大小(时间或消息数量),并等待所有确认后再发送下一个窗口。
- 流水线。允许发布者持续发送消息,但在未确认消息计数(正在传输的消息)达到限制时阻止。当确认到达时,可以发送更多消息,直到再次达到限制。
流水线(或简称为异步)方法提供最高和最稳定的吞吐量。它可以作为针对代理过载的额外保护,因为发布者本身甚至在对代理施加压力之前就将自己置于“流”中。
当您不使用发布者确认时,您仅依赖 TCP 流控制来连接发布者和代理上的连接读取器进程。对于相对少量的发布者,TCP 流控制可能足以避免代理过载,但是当您有大量客户端时,TCP 是不够的,发布者确认对于集群在重负载下的稳定性变得必要。有趣的是,AMQP 1.0 添加了链路流控制来克服这个问题。
消费者确认和预取
将手动确认与预取一起使用会对 RabbitMQ 施加反压,以阻止其压垮您的消费者客户端。它使用流水线方法发送恒定的消息流,但将未确认消息的数量限制为预取的大小 (QoS)。在 AutoAck 模式下,我们再次仅依赖 TCP 反压。您的客户端的各种入口缓冲区可能会快速填满。
强烈建议使用手动确认和预取。
仲裁队列
仲裁队列的基于信用的流控制链与经典队列不同,因为它们具有完全不同的持久性和复制协议。通道将所有消息发送到 Raft 集群领导者,然后 Raft 集群领导者将消息传递到 WAL 并将消息复制到其追随者。仲裁队列用于复制的 Raft 共识算法不包括基于信用的流控制,因此该机制在通道处结束。
通道不是使用信用作为一种在 Raft 领导者无法跟上时阻止自身的机制,而是跟踪领导者尚未应用的待处理 Raft 命令的数量。如果该数量超过 *quorum commands soft limit* 配置,则通道将停止向读取器进程授予更多信用。

其他控制(例如内存报警、发布者确认和消费者确认/预取)对于仲裁队列和经典队列是相同的。
即将发布的 3.8.4 版本中对仲裁队列的改进
我们发现流控制是仲裁队列相对于镜像队列的较弱领域之一。我们发现在来自数百或数千个客户端的重负载下,仲裁队列的反压效果不如镜像队列。仲裁队列在这些情况下出现了一些新的瓶颈,这些瓶颈可能会导致内存增长,依赖于内存报警而不是基于信用的流控制。
我们着手改进下一个版本,并大大改进了仲裁队列的这一方面,当 3.8.4 发布时,我们将撰写更多关于这些更改的文章。
在下一部分... 使用发布者确认来提高负载下的吞吐量
因此,您对 RabbitMQ、流控制和仲裁队列有了更多了解。您知道发布者确认对于数据安全以及发布者和代理之间的流控制都很重要。但是我们如何实现流水线?我们应该使用多大的正在传输的限制?不同的限制会对吞吐量和延迟产生什么影响?
所有这些问题将在本系列的下一部分中解答。我们将针对镜像队列和仲裁队列运行一些基准测试,并获得一些答案。