最大化消息吞吐量:自动确认模式与手动确认模式
在RabbitMQ中实现峰值消息吞吐量需要掌握确认模式。本指南比较了自动确认(Auto-Ack)和手动确认策略,详细说明了自动确认如何以牺牲消息安全性为代价换取原始速度。通过理解消费者预取(QoS)设置在最大化吞吐量同时维持关键交付保证中的关键作用,学习实用的性能调优方法,适用于高容量系统。
最大化消息吞吐量:自动确认模式与手动确认模式
RabbitMQ的确认模式是客户端代码中看似微小但具有巨大操作影响的设置之一。它决定了代理何时可以忘记一条消息。这一选择会影响吞吐量、内存压力、重试、重复工作以及消费者在处理过程中崩溃时的行为。
简而言之:自动确认速度快,因为RabbitMQ在消息交付后立即将其视为已处理。手动确认更安全,因为您的消费者明确告知RabbitMQ处理何时成功。大多数生产系统应从手动确认开始,并在考虑自动确认之前调整预取。
确认的真正含义
确认不是业务收据。它是一个代理级别的信号。当消费者发送basic.ack时,它告诉RabbitMQ:此交付可以从队列中移除。
这一区别很重要。如果您的消费者将订单写入数据库、发送电子邮件并更新搜索索引,正确的确认点通常是在工作的持久部分成功之后。如果在数据库提交之前确认且进程崩溃,RabbitMQ已完全按照您的要求执行:它移除了消息。您的应用程序丢失了工作。
自动确认
使用自动确认时,客户端以启用自动确认的方式订阅。RabbitMQ发送消息并立即将其视为成功交付。消费者稍后不会发送basic.ack。
在许多客户端库中,该设置作为消费时的布尔值出现。例如,Java在basicConsume中使用autoAck;几个库以略有不同的名称暴露相同的概念。
其吸引力显而易见。协议操作更少,簿记更少。消费者可以以RabbitMQ和网络能够交付的速度接收消息。对于遥测、临时进度更新或可丢弃的工作负载,这可能是可以接受的。
一旦在生产中看到,风险也同样明显。如果消费者接收了一万条消息,然后在处理其内存缓冲区之前崩溃,这些消息将从队列中消失。RabbitMQ无法重新交付它们,因为它们已被自动确认。
自动确认在消息非关键、可重新生成或代表旧数据无用的实时流时是合理的。示例包括尽力而为的指标、UI存在更新或日志类事件,其中单独的持久管道是记录源。它不适合支付、订单、库存变更、账户更新或错过消息会导致手动清理的工作。
手动确认
使用手动确认时,RabbitMQ将已交付的消息保持在未确认状态,直到消费者响应。如果消费者连接在确认前关闭,RabbitMQ会将这些未确认的消息重新入队,并可以再次交付它们。
这种行为是手动确认成为重要工作正常默认值的原因。这并不意味着恰好一次处理。消息可能被处理,然后消费者在发送确认前崩溃。RabbitMQ将重新交付它,您的应用程序可能看到相同的逻辑工作两次。手动确认提供至少一次交付,因此您的处理程序仍然需要幂等性,以防重复的副作用造成损害。
一个安全的消费者循环通常遵循以下形状:
接收消息
验证负载
执行持久工作
提交数据库事务或外部副作用
确认消息
对于失败,决定消息应被重试、延迟还是死信。立即重新入队每个失败可能会创建一个热循环,其中相同的坏消息整天消耗CPU。死信交换、重试队列或延迟重试模式通常更好。
预取是真正的吞吐量杠杆
许多团队比较自动确认和手动确认,看到手动确认在默认设置下较慢,并得出错误结论。缺失的部分是预取。
RabbitMQ预取,通过basic.qos配置,限制消费者一次可以持有的未确认消息数量。使用手动确认和prefetch=1,消费者接收一条消息,处理它,确认它,然后才获得另一条。这是安全的,但对于任何可以并发处理或容忍小本地缓冲区的工人来说,它留下了吞吐量。
更高的预取让RabbitMQ保持消费者忙碌:
prefetch = worker_concurrency * expected_work_buffer
如果一个工人并发处理8个作业,16或32的预取是一个合理的起点。如果每条消息很大或处理内存密集,从较低值开始。如果每条消息很小且处理主要是网络I/O,较高的数字可能有所帮助。
不要将随机的250预取复制到每个服务中。高预取可能导致不均匀分布。一个消费者可能接收大量批次并持有它,而其他消费者闲置。它还会在消费者死亡时增加重新交付的突发。RabbitMQ将重新入队该连接的所有未确认交付,这可能导致另一个工人突然继承大量积压。
吞吐量与安全性的权衡
以下是实际比较:
| 模式 | RabbitMQ的行为 | 优势 | 主要风险 |
|---|---|---|---|
| 自动确认 | 交付时移除消息 | 最高原始交付率 | 消费者崩溃时丢失工作 |
| 手动确认,低预取 | 等待每个确认后再发送更多 | 简单的失败行为 | 消费者利用不足 |
| 手动确认,调优预取 | 保持受控数量的在途消息 | 良好的吞吐量与恢复 | 需要幂等处理程序和重试设计 |
重要细节是手动确认不一定慢。调优不佳的手动确认是慢的。具有合理预取、并发工人和短数据库事务的手动确认可以处理大量流量,同时保留恢复行为。
具体的调优工作流程
从手动确认和保守的预取开始:
prefetch = 每个工作线程1到4
测量消费者利用率、队列深度、消息处理时间、内存和重新交付。如果消费者在队列有消息时闲置,提高预取。如果内存攀升或一个消费者囤积工作,降低预取。如果重新交付激增,在再次更改预取之前检查崩溃、超时和nack行为。
也要监控代理。高吞吐量不仅仅是消费者数字。磁盘I/O、发布者确认、队列类型、消息大小、持久性、镜像或仲裁复制以及网络带宽都会影响结果。确认模式是更大系统中的一个杠杆。
错误处理比标志更重要
没有失败计划的手动确认消费者只完成了一半。成功时,确认。临时失败时,仅当立即重试有意义时才nack并重新入队。对于毒消息,拒绝或不重新入队nack,如果配置了死信交换,则路由到它。
还要在消费者主队列之外设置最大重试策略。RabbitMQ不会神奇地知道格式错误的JSON消息已失败5次,除非您的设计通过标头、重试队列或应用程序状态跟踪尝试。
我默认会选择什么
对于业务事件和后台作业,使用手动确认。根据工人并发性和内存调优预取。使处理程序幂等。在坏消息教会您为什么无休止的立即重试是痛苦之前,添加死信。
仅在损失可接受且已记录时使用自动确认。这句话应该在事件审查期间容易辩护。如果团队发现已交付但未处理的消息消失会感到不安,自动确认是错误的设置。
消息大小改变答案
对2 KB消息效果良好的预取值可能对5 MB消息是鲁莽的。预取控制数量,而不是总字节数。如果一个消费者可以持有100条未确认消息且每条消息很大,本地内存占用可能迅速增加。代理也必须跟踪这些交付直到它们被确认。
当消息很大时,从较低的预取开始并测量消费者进程中的常驻内存。如果可能,保持消息体小,并将大负载存储在其他地方,例如对象存储,消息携带引用和校验和。这种设计并不总是合适,但它防止代理成为大文件传输。
批量确认可以减少协议通信
许多客户端库允许您使用multiple标志一次确认多个交付。当消费者按顺序处理消息并且可以安全地确认一系列交付标签时,这可以减少协议开销。
陷阱是失败处理。如果您并发处理消息,交付标签顺序可能与完成顺序不匹配。因为最新消息成功而确认多条消息可能会意外确认仍在运行或已失败的早期消息。对于并发工人,每条消息确认通常更简单、更安全。
一个有用的规则:仅当消费者的处理模型足够有序以至于您可以准确解释确认覆盖哪些消息时,才进行批量确认。
在事件期间监控未确认消息
RabbitMQ暴露就绪和未确认消息计数。具有许多就绪消息的队列意味着消费者跟不上或未连接。具有许多未确认消息的队列意味着RabbitMQ已将工作交付给消费者但尚未收到确认。
第二种情况指向消费者行为:处理慢、外部调用卡住、预取过高、线程阻塞或消费者在异常后停止确认。这与发布者以比消费者接收速度更快的速度淹没队列不同。
使用管理UI或rabbitmqctl,查看:
rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers
如果messages_unacknowledged很高且消费者存活,在更改代理设置之前检查消费者日志和线程转储。代理可能只是在等待应用程序完成工作。
重新交付是正常的,但重复重新交付是异味
手动确认意味着消息可以在消费者失败后重新交付。这是预期的。您不想要的是相同的毒消息被交付、失败、重新入队并永远再次交付。
添加足够的元数据以诊断重试。一些团队使用标头跟踪尝试。其他团队将失败移动到重试交换,然后在达到限制后移动到死信队列。确切模式各不相同,但操作目标相同:临时失败获得另一次机会,永久失败变得可见并停止阻塞有用工作。
当处理程序不是幂等的时,重新交付变得危险。假设一个工人扣款,然后在确认前崩溃。RabbitMQ将重新交付消息。如果处理程序再次扣款,代理没有创建错误;它揭示了缺少幂等性键。对于外部副作用,存储一个持久的操作ID并使副作用安全重复。
发布者确认是单独的问题
消费者确认告诉RabbitMQ消费者处理了交付。发布者确认告诉发布者RabbitMQ接受了发布的消息。它们解决流程的不同侧面。
一个系统可以使用手动消费者确认,如果发布者没有确认就发送并忘记,并且在错误时刻连接断开,仍然可能在发布时丢失消息。同样,发布者确认在消费者接收消息后不保护工作。对于可靠管道,在业务需要的地方使用两者:发布侧的确认,消费侧的手动确认,适当位置的持久队列,以及应用层的幂等处理。
队列类型和持久性影响相同的吞吐量讨论
确认模式不是孤立存在的。具有非持久消息的临时经典队列与具有持久消息的持久仲裁队列具有不同的性能和安全性特征。如果您在可丢弃队列上基准测试自动确认,然后将结果应用于持久生产队列,比较是无用的。
对于重要工作负载,持久队列和持久消息很常见,但它们增加了磁盘和复制工作。仲裁队列与较旧的镜像经典队列模式相比提高了数据安全性,但它们也改变了吞吐量特征。测量您实际运行的队列类型。
一个公平的测试保持这些变量稳定:
相同的消息大小
相同的队列类型
相同的持久性设置
相同的发布者确认行为
相同的消费者数量
相同的预取
相同的下游处理
一次只更改一个杠杆。否则您将不知道结果来自确认模式、预取、队列类型、消息大小还是消费者代码。
消费者并发性应与工作匹配
如果每条消息大部分时间在等待HTTP或数据库,消费者可能受益于并发处理。如果每条消息是CPU密集型的,过多的并发性可能使每条消息变慢。预取应遵循这一现实。
对于单线程消费者,100的预取可能只是创建一个大的本地等待室。对于具有20个活动处理槽的工人,40的预取可能保持这些槽被填充。对于具有四个核心的CPU绑定进程,100的并发性可能增加上下文切换而不提高吞吐量。
测量消费者内部的处理时间,而不仅仅是队列深度。添加接收时间、开始时间、完成时间、确认时间、失败原因和重新交付标志的日志或指标。这些时间戳使判断工作是在RabbitMQ中等待、在消费者内部等待还是卡在下游系统中变得容易得多。