在微服务和异步架构盛行的今天,为了应对不可避免的消息处理失败问题,RabbitMQ 提供了死信队列机制。
消息被 nack 或重试超时就会进入死信队列。但是如果死信队列也一直超时得不到ack无法消费,那这些已经死亡的消息会去哪?
什么是死信队列
在消息系统中,消息消费失败(重试多次,超时收不到ack)是一种必然存在的异常。例如:
消费者业务逻辑抛出异常,无法处理消息;
消息格式不符合预期,字段缺失;
某些系统服务下线、数据库不可用,导致消息积压;
队列满了、新消息无法入队;
在这些情况下,默认行为是将消息丢弃或者反复尝试消费(根据配置),这显然不够健壮。为了提升系统的稳定性,需要一个机制把这些失败的消息收集起来进行后续处理或报警,这就是死信队列的价值所在。
死信队列并不是 RabbitMQ 内部定义的特殊队列类型,而是你通过给业务队列配置死信交换机(Dead Letter Exchange, DLX)之后,构造出来的一个普通队列。
这个过程是这样的:
给业务队列配置 x-dead-letter-exchange 参数;
当业务队列中的消息满足死亡条件”时,会被转发到这个 DLX;
DLX 会将消息投递到你绑定的死信队列(也是普通队列)中;
也就是说,死信队列的本质仍是一个普通的队列,只是其消息来源是别人的失败。
消息变成死信
RabbitMQ 将某条消息标记为死信的条件包括:
消息被消费者 reject 或 nack,且设置了 requeue=false;
消息在队列中滞留超过了 TTL(Time-To-Live)时间;
队列超过了最大长度或最大容量(字节数)限制;
消息因为路由失败被丢弃(比如未找到绑定队列);
举个例子,你的业务系统中存在一个支付订单处理队列,当其中某条消息消费时因库存系统挂了抛出异常,并设置 requeue=false,这条消息就会立刻进入你为其配置好的死信交换机,并被投递到你指定的死信队列中,等待后续处理。
死信队列中的消息同样需要有消费者来处理,如果你没有为死信队列设置消费者,消息将会持续堆积;如果消费者处理失败又不 ack,还可能陷入重复重试的死循环。
死信又死信
这是一个非常现实而又容易被忽略的问题。想象你已经非常严谨地为你的业务队列配置了死信机制,但死信队列本身也存在问题,例如:
死信消费者挂掉;
死信消费逻辑同样有 bug;
消息本身问题导致反复 nack;
这时,消息将陷入一种已死复生又死的状态,但 RabbitMQ 默认并不会再将其转发。这些消息会长期卡在死信队列中,除非你主动消费并 ack,否则它们永远不会离开这个队列。
换句话说:
RabbitMQ 的死信机制只支持一级死信处理,死信队列中再失败的消息不会再次被自动死信。
当然,为了让你的系统在面对严重异常时也不会消息丢失,也可以构建死信链机制:
给死信队列也配置一个死信交换机,当其消费失败时,消息再次被转发到一个更底层的死信队列中,进行最终处理。
还有一些可以做的:
目标 | 做法 |
---|---|
消息最多重试 N 次 | 使用 header 设置 retry-count,自定义逻辑控制 |
防止死信队列死循环 | 设置 TTL + 死信链,限制最大重试时间 |
联动告警系统 | 最终死信消费者集成钉钉、短信、邮件等告警平台 |
支持人工补偿 | 最终死信队列入库后建立后台页面人工重推 |
而另外两个,RocketMQ 本身并不内建死信队列的概念,但它通过消费者重试机制 + 特殊 Topic 命名规则来实现类似效果。而Kafka 作为一个高吞吐量的分布式日志系统,本质上并不支持消费失败的概念,消费失败只是消费者程序自己定义的行为。