Rabbimtmq unack问题分析

一、问题现象

应用程序通过spring amqp 操作rabbitmq,有个手动返回ack的queue,应用程序已经使用 channel.basicAck 返回ack了,但是通过rabbitmq 管理控制台看到,仍然有大量消息处于 unack 状态。

1、rabbit mq日志

通过 /var/log/rabbitmq/[email protected] 可以看到对应的错误信息
显示如下:

2018-10-03 19:15:15.653 [error] <0.8578.1> Channel error on connection <0.8298.1> (127.0.0.1:54528 -> 127.0.0.1:5672, vhost: '/', user: 'guest'), channel 3:
operation basic.ack caused a channel exception precondition_failed: unknown delivery tag 4
2、应用程序日志

应用程序同时伴有如下错误日志:

[ERROR] 2018-10-03 16:43:40.251 - Channel shutdown: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - unknown delivery tag 2, class-id=60, method-id=80)
[ERROR] 2018-10-03 16:43:41.256 - Channel shutdown: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - unknown delivery tag 3, class-id=60, method-id=80)

二、解决过程

通过日志可以发现,channel 已经被close掉了,把 cachingConnectionFactory 的 channelCacheSize 改为大于等于目前系统里 consumer 的个数就可以。

1、xml配置

如果使用application.xml配置 RabbitTemplate ,添加如下配置


2、手动使用代码控制
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setHost("localhost");
factory.setChannelCacheSize(期望的size);
3、Tips:

1、spring-rabbit 从 1.6.0.RELEASE 之后,把 默认的 channelCacheSize 从 1 改为了 25。
2、channelCacheSize 一定要大于等于目前的 consumer 个数,在 1.6.0.RELEASE 之前,如果只有 1 个 consumer,不会出现这个问题。
在 1.6.0.RELEASE(含) 以及以后,如果 consumer 个数小于等于25,不会出现问题。建议手动显式设置。
3、如果使用spring xml配置 consumer,则不会出现这个问题,因为spring 会自动检查配置,在 SimpleMessageListenerContainer 类里,如果发现 channelCacheSize 小于当前的 consumer 数量,则会修改 channelCacheSize。

三、原因分析

1、开启spring 日志,有看到如下信息:

[TRACE] 2018-10-03 17:09:45.485 - Returning cached Channel: AMQChannel(amqp://[email protected]:5672/,1)
[DEBUG] 2018-10-03 16:56:13.248 - Closing cached Channel: AMQChannel(amqp://[email protected]:5672/,1)

2、根据 Closing cached Channel 找到对应的代码,看在哪里执行了channel 的close操作。
搜索一下,在spring-rabbit源码路径下执行命令: grep -irn 'Closing cached Channel' org
发现代码的关闭操作实际上是在 CachedChannelInvocationHandlerphysicalClose() 方法进行的。
3、通过分析代码调用链路,发现如下流程:
RabbitTemplate 每次执行 execute 方法的时候,在 finally 块里最终都会释放资源,如果目前 CachingConnectionFactory 内部的 channelCacheSize 小于 配置的数量,则进行 逻辑关闭(logicalClose),即日志中的 Returning cached Channel,逻辑关闭不会真正的关闭 channel。 如果大于当前数量,则尝试进行 物理关闭(physicalClose): 会真正的关闭掉channel。

4、RabbitTemplate.execute 核心代码
    private  T doExecute(ChannelCallback action, ConnectionFactory connectionFactory) {
        Assert.notNull(action, "Callback object must not be null");
        RabbitResourceHolder resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(
                (connectionFactory != null ? connectionFactory : getConnectionFactory()), isChannelTransacted());
        Channel channel = resourceHolder.getChannel();
        if (this.confirmCallback != null || this.returnCallback != null) {
            addListener(channel);
        }
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Executing callback on RabbitMQ Channel: " + channel);
            }
            return action.doInRabbit(channel);
        }
        catch (Exception ex) {
            if (isChannelLocallyTransacted(channel)) {
                resourceHolder.rollbackAll();
            }
            throw convertRabbitAccessException(ex);
        }
        finally {
                      //释放资源
            ConnectionFactoryUtils.releaseResources(resourceHolder);
        }
    }
6、代码流程如下:
image.png
7、logicalClose 代码如下:

通过代码可以看到,close 只是把它放回到 channel pool list里,并没有做真正的关闭操作。

        private void logicalClose(ChannelProxy proxy) throws Exception {
            if (target == null) {
                return;
            }
            if (this.target != null && !this.target.isOpen()) {
                synchronized (targetMonitor) {
                    if (this.target != null && !this.target.isOpen()) {
                        if (this.channelList.contains(proxy)) {
                            this.channelList.remove(proxy);
                        }
                        this.target = null;
                        return;
                    }
                }
            }
            // Allow for multiple close calls...
            if (!this.channelList.contains(proxy)) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Returning cached Channel: " + this.target);
                }
                this.channelList.addLast(proxy);
            }
        }
8、physicalClose 代码,真实的关闭 channel, 会通过 rabbit 包下的 channelN 真正的关闭掉这个channel(向rabbitmq server发送消息)
        private void physicalClose() throws Exception {
            if (logger.isDebugEnabled()) {
                logger.debug("Closing cached Channel: " + this.target);
            }
            if (this.target == null) {
                return;
            }
            try {
                if (CachingConnectionFactory.this.active &&
                        (CachingConnectionFactory.this.publisherConfirms ||
                                CachingConnectionFactory.this.publisherReturns)) {
                    ExecutorService executorService = (getExecutorService() != null
                            ? getExecutorService()
                            : CachingConnectionFactory.this.deferredCloseExecutor);
                    final Channel channel = CachedChannelInvocationHandler.this.target;
                    executorService.execute(new Runnable() {

                        @Override
                        public void run() {
                            try {
                                if (CachingConnectionFactory.this.publisherConfirms) {
                                    channel.waitForConfirmsOrDie(5000);
                                }
                                else {
                                    Thread.sleep(5000);
                                }
                            }
                            catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                            catch (Exception e) {}
                            finally {
                                try {
                                    if (channel.isOpen()) {
                                        channel.close(); //真实关闭掉这个channel
                                    }
                                }
                                catch (IOException e) {}
                                catch (AlreadyClosedException e) {}
                            }
                        }

                    });
                }
                else {
                    this.target.close();
                }
            }
            catch (AlreadyClosedException e) {
                if (logger.isTraceEnabled()) {
                    logger.trace(this.target + " is already closed");
                }
            }
            finally {
                this.target = null;
            }
        }

    }

四、总结

归根结底,是因为channel 被关闭了,但是仍然通过这个channel 回复 ack,在basicAck的时候,会把当前的 channelNumber 一起发送给rabbitmq server。
有兴趣的可以深入了解下 rabbitmq server的实现原理。
代码在 https://github.com/rabbitmq/rabbitmq-server
rabbitmq 是erlang 开发的,上面的错误日志在

collect_acks(ToAcc, PrefixAcc, Q, DeliveryTag, Multiple) ->
    case queue:out(Q) of
        {{value, UnackedMsg = {CurrentDeliveryTag, _ConsumerTag, _Msg}},
         QTail} ->
            if CurrentDeliveryTag == DeliveryTag ->
                    {[UnackedMsg | ToAcc],
                     case PrefixAcc of
                         [] -> QTail;
                         _  -> queue:join(
                                 queue:from_list(lists:reverse(PrefixAcc)),
                                 QTail)
                     end};
               Multiple ->
                    collect_acks([UnackedMsg | ToAcc], PrefixAcc,
                                 QTail, DeliveryTag, Multiple);
               true ->
                    collect_acks(ToAcc, [UnackedMsg | PrefixAcc],
                                 QTail, DeliveryTag, Multiple)
            end;
        {empty, _} ->
            precondition_failed("unknown delivery tag ~w", [DeliveryTag])
    end.

你可能感兴趣的:(Rabbimtmq unack问题分析)