慢消息处理故障排除:识别 RabbitMQ 瓶颈
通过分离生产者、代理、队列、消费者、磁盘和确认瓶颈来诊断 RabbitMQ 速度下降问题。
慢消息处理故障排除:识别 RabbitMQ 瓶颈
当 RabbitMQ 队列积压时,队列只是显示症状。瓶颈可能是消费者速度慢、发布者被阻塞、磁盘警报、预取值设置不当、消息负载过大,或下游数据库悄然开始超时。重启 RabbitMQ 可能让图表在几分钟内恢复正常,但很少能解决消息处理缓慢的根本原因。
最快的故障排除路径是将流程分解为多个部分:发布到 RabbitMQ、路由到队列、存储消息、投递给消费者、处理工作以及确认完成。每个部分都会留下不同的证据。
第一步:区分就绪与未确认
从队列计数器开始:
rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers state
messages_ready 表示消息正在队列中等待投递。如果这个数字增长,说明 RabbitMQ 没有可用的消费者、消费者已达到预取限制,或投递被其他条件阻塞。
messages_unacknowledged 表示消息已投递给消费者,RabbitMQ 正在等待确认、拒绝、拒绝或通道关闭。如果这个数字增长,瓶颈通常出现在消费者内部或消费者调用的某些环节。
这种区分很重要。如果就绪消息多而未确认消息少,增加代理内存不会让消费者出现。如果未确认消息多,增加队列分区可能也无济于事,因为工作已经离开队列。
检查消费者是否实际存在
令人惊讶的是,许多“RabbitMQ 速度慢”的事件实际上是“消费者未运行”的事件。部署失败、自动缩放归零、凭据更改、使用了错误的虚拟主机,或者服务连接到测试环境而生产者发布到生产环境。
使用:
rabbitmqctl list_consumers queue_name channel_pid consumer_tag ack_required prefetch_count active
如果没有消费者,先解决这个问题。如果消费者存在但不活跃,检查应用程序日志和连接状态。如果每个消费者的预取值为 1,且每条消息需要几秒钟,那么低投递并发可能是预期的。如果每个消费者的预取值为 500,且未确认消息数量巨大,消费者可能囤积了无法快速完成的工作。
测量消费者处理时间
RabbitMQ 可以告诉你消息未确认。但它无法告诉你消费者是在解析巨大的负载、等待 PostgreSQL、重试 HTTP 调用,还是被锁阻塞。
在实际处理程序周围添加计时:
message_received_at
decode_ms
business_logic_ms
database_ms
external_api_ms
ack_ms
message_completed_at
你不需要完美的追踪系统来获取信息。即使是一些结构化的日志字段也能显示处理程序通常需要 80 毫秒,但现在却花费 4 秒等待下游 API。
如果工作速度慢但可并行化,增加消费者实例或提高内部工作线程并发度。如果下游系统是限制因素,增加消费者可能使情况更糟。你可能需要速率限制、批处理、缓存或单独的重试队列。
在理解处理程序后调整预取值
预取值控制 RabbitMQ 可以发送给每个消费者的未确认消息数量。它经常涉及慢速处理事件,因为它改变了积压可见的位置。
预取值低时,消息在 RabbitMQ 中保持就绪状态,直到消费者准备好接收更多。这很公平且易于观察,但可能未充分利用非常快的消费者。
预取值高时,消息快速进入消费者。这可以提高快速处理程序的吞吐量,但也可能隐藏延迟。一个具有大预取值的慢速消费者可能持有数百条消息,而其他消费者则没有工作可做。
一个实用的故障处理方法是降低慢速或不稳定消费者的预取值,并观察尾部延迟是否改善。对于 CPU 使用率低且就绪计数高的快速消费者,谨慎提高预取值并再次测量。
查找发布者端瓶颈
有时队列积压不是因为消费者慢,而是因为发布者以突发方式发布消息,然后低效地等待确认。
当发布者需要知道 RabbitMQ 已接受消息时,发布者确认是正确的工具。缓慢的模式是在发布下一条消息之前等待每个确认。这使每次发布都变成一次往返。
更好的模式使用异步确认或有界批处理。发布者可以保持有限数量的消息在飞行中,处理拒绝,并仍然避免阻塞每条消息。限制很重要。无限的在途发布可能将瓶颈转移到发布者内存或代理压力。
检查发布者指标:发布速率、确认延迟、在途确认计数、重新连接、返回消息和通道异常。在管理 UI 中,比较发布速率与投递/确认速率。如果发布速率低但应用程序繁忙,生产者可能正在等待确认、事务或连接波动。
除非有特定原因,否则避免为高吞吐量发布使用 AMQP 事务。对于典型的可靠发布,它们比发布者确认昂贵得多。
在责怪 RabbitMQ 之前检查磁盘
持久消息、仲裁队列、流和大量积压都涉及磁盘。当磁盘延迟上升时,消息流可能急剧减慢。
在 RabbitMQ 节点上,检查:
rabbitmq-diagnostics status
rabbitmq-diagnostics alarms
rabbitmq-diagnostics memory_breakdown
在操作系统级别,使用 iostat、vmstat 或云监控图表等工具。查看磁盘延迟和 I/O 等待,而不仅仅是吞吐量。一个已耗尽突发信用额度的云磁盘在配置上可能看起来正常,但在实践中表现糟糕。
如果磁盘是瓶颈,可能的修复包括更快的存储、更少的持久写入、更小的消息、更好的发布者确认批处理、队列拆分,或将重放型工作负载迁移到流或其他日志导向系统。不要为了图表变绿而禁用业务不能丢失的消息的持久性。
检查警报和阻塞连接
RabbitMQ 通过内存和磁盘警报保护自身。当警报激活时,发布者可能被阻塞。从生产者端看,这可能表现为应用程序缓慢。
运行:
rabbitmq-diagnostics alarms
rabbitmqctl list_connections name user state channels send_pend recv_cnt send_cnt
如果内存警报激活,找出内存是由队列、连接、未确认消息、二进制文件还是插件占用。如果磁盘警报激活,在尝试通过代理推送更多消息之前,释放空间或增加容量。
阻塞连接本身不是错误。它们是 RabbitMQ 告诉发布者减速,因为节点正在保护可用性。
消息大小可能是隐藏的罪魁祸首
一个每秒处理 10,000 条小消息的系统可能难以处理每秒 500 条大消息。大型负载会增加网络传输、内存压力、磁盘写入、垃圾回收工作和消费者处理时间。
如果消息包含大型文档、图像、报告或大型数组,考虑将负载存储在对象存储或数据库中,并通过 RabbitMQ 发送引用。包含足够的元数据用于路由和幂等性,但尽可能避免代理承担批量存储的角色。
同时检查压缩选择。压缩大型负载可能减少网络和磁盘使用,但增加 CPU。是否有助于取决于瓶颈所在。
重试可能成为瓶颈
一个失败的下游服务可能将一条消息变成多次尝试。如果消费者立即重新排队失败的消息,它们可能反复处理相同的坏消息,而新工作则等待。队列深度可能上升,CPU 看起来繁忙,但完成的有用工作很少。
查找高重新投递率和重复的错误日志,其中包含相同的消息 ID。如果相同的负载反复失败,将其移出主流程。死信交换、延迟重试队列或定时重试机制为依赖项提供恢复时间,并防止毒药消息阻塞正常工作。
小心重试风暴。如果 API 宕机十分钟,每条消息每秒重试一次,那么当 API 恢复时,恢复将变得更加困难。使用退避策略。限制尝试次数。使最终失败在死信队列中可见,并附带足够的上下文以供调查。
幂等性也是性能故障排除的一部分。如果消费者在部分完成工作后重试,重复可能导致数据库争用、唯一键错误或额外的下游调用。一个能够安全处理同一条消息两次的处理程序更容易扩展和恢复。
管理 UI 速率需要上下文
RabbitMQ 管理 UI 很有用,但如果只读一行,速率图表可能误导。高投递速率与低确认速率意味着工作被分发出去的速度快于完成速度。高确认速率与高就绪计数可能意味着消费者正在工作但不足以赶上。事件期间的低发布速率可能意味着发布者被阻塞或等待确认。
同时查看几个速率:
publish:消息进入交换器。deliver/get:消息发送给消费者。ack:消费者完成的消息。redeliver:先前失败或通道关闭后再次投递的消息。
对于健康稳定的工作队列,发布和确认速率应随时间接近。短暂突发是正常的。长时间差距意味着积压在累积或消耗。如果重新投递急剧上升,不要只是增加消费者。找出消息返回的原因。
采样窗口很重要。一分钟图表可能隐藏一个影响用户的五秒停顿。一秒图表可能使正常的突发性看起来像混乱。将图表窗口与用户或下游系统关心的延迟匹配。
区分正常积压与异常积压
并非所有积压都是紧急情况。批处理系统可能有意在白天排队工作,并在夜间消耗。面向用户的工作流如果消息等待三十秒可能不健康。相同的队列深度在一个系统中可能可接受,在另一个系统中则严重。
定义基于年龄的信号,而不仅仅是计数。消息计数告诉你等待的数量;消息年龄告诉你业务是否落后。如果你的监控可以跟踪最旧消息年龄或从发布到确认的端到端时间,它将比队列深度更早捕捉到速度下降。
将警报与该预期关联。对 10,000 条消息发出警报可能对夜间导出队列来说是噪音,而对密码重置队列来说则太晚。对“最旧消息年龄超过服务目标”发出警报通常更接近用户关心的内容。
一个热点队列仍然是一个热点队列
增加集群节点不会自动将一个队列分布到所有节点。单个热点队列可能仍然受限于其领导者、消费者和存储路径。
如果一个队列承载不相关的工作类型,根据实际处理行为拆分。例如,图像调整大小、电子邮件发送和计费捕获不应共享一个通用的 jobs 队列,如果它们有不同的延迟和重试需求。单独的队列让你独立扩展消费者并隔离毒药消息。
如果一种工作类型仍然太热,仅在排序要求允许时进行分片。按客户 ID、租户、区域或其他稳定键分片可能有效,但会将复杂性推入路由和运维。不要仅仅为了避免修复慢速处理程序而分片。
冷静的故障排除顺序
在事件中,我使用以下顺序:
- 检查警报:内存、磁盘和阻塞连接。
- 检查队列计数器:就绪、未确认、消费者。
- 检查消费者日志和处理程序计时。
- 检查每个消费者的预取值和未确认分布。
- 检查发布者确认延迟和返回消息。
- 检查磁盘延迟和节点资源压力。
- 检查消息大小和最近的负载变化。
- 只有在那之后才更改拓扑或添加代理节点。
这个顺序防止了一个常见错误:当瓶颈是工作线程时扩展代理,或当瓶颈是磁盘时扩展工作线程。
一旦你读取了正确的计数器,RabbitMQ 通常非常清晰。就绪计数增长意味着工作正在等待。未确认计数增长意味着工作正在进行但未完成。发布者被阻塞意味着代理正在保护自身。将每个信号视为线索,修复将变得不那么戏剧化。