最近项目里的rabbitmq发生了消费端内存爆炸,消息堆积,消费者不消费的问题。为了解决这个问题,展开了一系列的调查,现在问题解决,也总结一下最近的调查结果。
SpringAMQP确认模式
为保证可靠性和数据安全性,RabbitMQ提供两种确认机制,发布者确认(Publisher Confirms)和消费者确认(Consumer Delivery Acknowledgements)。这里我们只里聊消费者确认。
在RabbitMQ中存在两种消费者确认模式:
-
自动确认:消息发出后会立即被认为是发送成功。俗称即发即忘。这种模式的特点是提供了高吞吐量但是不能保证数据安全,如果消费者的TCP连接在成功传递之前关闭,那消息就丢了。
-
手动确认:消息发出后需要消费者手动发出确认。如遇上连接关闭的情况,未被确认的消息将会自动重新入队(requeue)
然鹅,在SpringAMQP里却提供了三种关于确认模式(AcknowledgeMode)的设定:
- NONE: 对应RabbitMQ里的自动确认
- MANUAL: 表示spring不会做任何事情,用户需要自己手写确认
- AUTO: 文档上描述的是 “容器将根据侦听器是否正常返回或抛出异常来发出 ack/nack。”,也就是说对应到RabbitMQ里也是使用手动确认,只是不需要用户手写,相当于框架自己帮你完成了手动确认。
Prefetch count
之前项目里用的是AcknowledgeMode=NONE,也就是自动确认模式。这种方式无视了消费者的消费能力,过量的消息堆积导致了消费端内存爆炸。
为了对应这种情况,RabbitMQ其实提供了一种解决办法,即设置prefetch count。
Prefetch数限制了一个channel或一个连接上的未确认消息数量。也就是说设置了Prefetch count之后,如果队列中未确认消息的数量达到了prefetch的上限,那么RabbitMQ就会限制生产者发信,从而避免消费者内存爆炸的问题。
对应到spring 里最简单的实现,可以在applicaiton.yml里进行设定
rabbitmq:
addresses: ${CLOUDAMQP_URL}
template:
mandatory: true
listener:
type: simple
simple:
acknowledge-mode: auto #确认模式:只有在手动确认模式下,prefetch才能生效
prefetch: 30 #在此处设置prefetch count
对不同的listener使用不同的确认模式
现在项目默认的设定是acknowledge-mode:none,因为项目里有好多个不同的队列,实现着不同的业务,但是我们只想对其中一个业务繁重的队列使用prefetch设定,也就是需要对其指定acknowledge-mode: auto,同时不想影响其他已有的队列,此时该怎么办嘞?
答案就是创建一个新的ListenerContainer,在listener处指定,来看一下具体做法:
RabbitMQ设定:
@Configuration
public class RabbitmqConfig {
@Bean
public SimpleRabbitListenerContainerFactory manualAckContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 指定确认模式
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
factory.setDefaultRequeueRejected(false);
// 指定prefetch count
factory.setPrefetchCount(30);
return factory;
}
}
RabbitListener:
// 指定ListenerContainer为新定义的"manualAckContainerFactory"
@RabbitListener(queues = "queueA", containerFactory = "manualAckContainerFactory")
@RabbitHandler
public void queueAHandler(@Payload List<xxxBean> list, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
logger.info("queueA处理开始: " + tag);
// 业务逻辑......
}
这样queueA的消费者就会使用手动确认模式,并应用上prefetch的设定,而其他未做特别指定的队列将使用application.yml里的设定。同理,我们还可以使用此方法为不同队列指定不同的 Retry Setting,messageConverter等等。
By the way, 如果想对不同队列使用不同的meesageConverter ,除了在消费端的listenerContainer处设定之外,还需要在生产者端的rabbitTempalte处也指定,也就是需要新实例化一个rabbitmqTemplate。