RabbitMQ的ack或nack机制使用不当导致的队列堵塞或死循环问题

记录几个RabbitMQ使用过程中容易踩的那些坑:

1、自动ack机制会导致消息丢失的问题;

简要代码如下,设置消息自动ack,这种情况下,MQ只要确认消息发送成功,无须等待应答就会丢弃消息,
这会导致客户端还未处理完时,出异常或断电了,导致消息丢失的后果,
解决方法就是把代码里的true,改成false,并在消息处理完后发ack响应。

// 要监听队列,所以不能用using关闭channel通道
var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume(queue, true, consumer); // 消息自动ack

注:自动ack还有个弊端,只要队列不空,RabbitMQ会源源不断的把消息推送给客户端,而不管客户端能否消费的完

2、启用ack机制后,没有及时ack导致的队列异常;

为了解决问题1,做了改进,简要代码如下:

var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
    try
    {
        callback(e.Body, e.BasicProperties.Headers);
        // 无异常时,发ack通知mq丢弃消息        
        ((EventingBasicConsumer)sender).Model.BasicAck(e.DeliveryTag, false);  
    }
    catch (Exception exp)
    {
        LogHelper.Info(exp);
    }
};

这段代码中,先处理消息,完成后,再做ack响应,失败就不做ack响应,这样消息会储存在MQ的Unacked消息里,不会丢失,看起来没啥问题,
但是有一次,callback触发了一个bug,导致所有消息都抛出异常,然后队列的Unacked消息数暴涨,导致MQ响应越来越慢,甚至崩溃的问题。
原因是如果MQ没得到ack响应,这些消息会堆积在Unacked消息里,不会抛弃,直至客户端断开重连时,才变回ready;
如果Consumer客户端不断开连接,这些Unacked消息,永远不会变回ready状态,Unacked消息多了,占用内存越来越大,就会异常了。
解决办法就是及时去ack消息了

3、启用nack机制后,导致的死循环;

为了解决问题2,再调整一下代码,简要代码如下:

var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
    try
    {
        callback(e.Body, e.BasicProperties.Headers);
        ((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
    }
    catch (Exception exp)
    {
        LogHelper.Info(exp);
        // 出错了,发nack,并通知MQ把消息塞回的队列头部(不是尾部)
        ((EventingBasicConsumer) sender).Model.BasicNack(e.DeliveryTag, false, true);
    }
};

嗯,改成这模样总没问题了吧,正常就ack,不正常就nack,并等下一次重新消费。
果然,又出问题了,这回又是callback出异常了,但是故障现象是Ready的消息猛增,一直不见减少。
原因是出异常后,把消息塞回队列头部,下一步又消费这条会出异常的消息,又出错,塞回队列……
进入了死循环了,当然新的消息不会消费,导致堆积了……
这个咋办?只能不用nack,所有消息都ack,自己记录日志,后续走其它job恢复日志了。
就是把catch里的BasicNack改成BasicAck。

4、启用Qos和ack机制后,没有及时ack导致的队列堵塞;

这个问题跟前面的3个没啥联系,简要代码如下:

var channel = GetChannel();
// 启用QoS,每次预取5条消息,避免消息处理不过来,全部堆积在本地缓存里
channel.BasicQos(0, 5, false);
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack,配合qos进行流控
channel.BasicConsume(queue, false, consumer);

consumer.Received += (sender, e) =>
{
    try
    {
        callback(e.Body, e.BasicProperties.Headers);
        ((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
    }
    catch (Exception exp)
    {
        LogHelper.Info(exp);
    }
};

这段代码中,由于开启了QoS,当RabbitMQ的队列达到5条Unacked消息时,不会再推送消息给Consumer,
如果回调函数出异常了,就会导致消息无法ack,从而导致无法继续处理后续的消息。
你问解决办法?当然是参考问题3,全部消息都去做ack响应呗(异常里也用Ack,而不用Nack)。

5、消费者串行处理,崩溃时导致未处理的预取数据丢失;

在RabbitMQ的.Net 3.6.9版本驱动里,不支持异步处理消息的方法,如果预取了10条消息,这10条消息会在本地缓存里,一条一条串行处理,效率比较低下,
在5.0以后的驱动里添加了AsyncEventingBasicConsumer类的支持,
但是我们还在用3.6.9,只能自己去用多线程搞了,简要代码如下:

var channel = GetChannel();
// 启用QoS,每次预取5条消息,避免消息处理不过来,全部堆积在本地缓存里
channel.BasicQos(0, 5, false);
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack,配合qos进行流控
channel.BasicConsume(queue, false, consumer);

consumer.Received += (sender, e) =>
{
    ThreadPool.UnsafeQueueUserWorkItem(state => {
        try
        {
          callback(e.Body, e.BasicProperties.Headers);
        }
        catch (Exception exp)
        {
            LogHelper.Info(exp);
        }
        finally
        {    
          ((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
        }
    }, null);
};

6、心跳时间设置太短导致的异常;

RequestedHeartbeat要设置为5~20秒,我的项目中默认是设置为10秒
具体问题和解决,请参考:https://blog.csdn.net/youbl/article/details/79024061

你可能感兴趣的:(.Net技术)