【spring cloud 配置中心 + rabbit mq】网络断连恢复引起的配置无法动态刷新

最近帮别人看一个问题,其项目使用了 rabbit mq,一是业务代码使用;二是配合 spring cloud config & spring cloud bus做配置动态刷新。
在测试环境偶尔会瞬时的网络中断,在几秒内即恢复,但之后项目日志内会一直报 rabbit mq 的重连错误:一开始的报错是 :

#method(reply-code=405, reply-text=RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'springCloudBus.anonymous.xxxxx' in vhost '/', class-id=50, method-id=20)

过了一段时候后,报错变成 :

#method(reply-code=405, reply-text=NOT_FOUND.....

经过测试发现,业务队列收发没有收到影响,结合报错内容内的 springCloudBus.anonymous.xxxxxx ,明显是和 spring cloud bus 相关的队列,项目里用到的地方只有配置中心的配置动态刷新,一测果然配置无法动态刷新了。

原因分析:

第一个报错里,rabbit mq 返回了405 RESOURCE_LOCKED,搜一下就知道是因为 rabbit 排他性 队列的特性,通过查阅 spring cloud bus 源码确认了这一点(RabbitExchangeQueueProvisioner.provisionConsumerDestination):

@Override
    public ConsumerDestination provisionConsumerDestination(String name, String group,
            ExtendedConsumerProperties properties) {
        boolean anonymous = !StringUtils.hasText(group);
        String  baseQueueName = anonymous ? groupedName(name, ANONYMOUS_GROUP_NAME_GENERATOR.generateName())
                    : properties.getExtension().isQueueNameGroupOnly() ? group : groupedName(name, group);
        if (this.logger.isInfoEnabled()) {
            this.logger.info("declaring queue for inbound: " + baseQueueName + ", bound to: " + name);
        }
        String prefix = properties.getExtension().getPrefix();
        final String exchangeName = applyPrefix(prefix, name);
        Exchange exchange = buildExchange(properties.getExtension(), exchangeName);
        if (properties.getExtension().isDeclareExchange()) {
            declareExchange(exchangeName, exchange);
        }
        String queueName = applyPrefix(prefix, baseQueueName);
        boolean partitioned = !anonymous && properties.isPartitioned();
        boolean durable = !anonymous && properties.getExtension().isDurableSubscription();
        Queue queue;
        if (anonymous) {
            queue = new Queue(queueName, false, true, true, queueArgs(queueName, properties.getExtension(), false));
        }
        else {
            if (partitioned) {
                String partitionSuffix = "-" + properties.getInstanceIndex();
                queueName += partitionSuffix;
            }
            if (durable) {
                queue = new Queue(queueName, true, false, false,
                        queueArgs(queueName, properties.getExtension(), false));
            }
            else {
                queue = new Queue(queueName, false, false, true,
                        queueArgs(queueName, properties.getExtension(), false));
            }
        }
        declareQueue(queueName, queue);
        Binding binding = null;
        if (properties.getExtension().isBindQueue()) {
            binding = declareConsumerBindings(name, properties, exchange, partitioned, queue);
        }
        if (durable) {
            autoBindDLQ(applyPrefix(properties.getExtension().getPrefix(), baseQueueName), queueName,
                    properties.getExtension());
        }
        return new RabbitConsumerDestination(queue, binding);
    }

可以看到 group 参数为空的时候,就会自动创建匿名的排他队列。

那么为什么第二个报错变成了 rabbit 返回 404 NOT FOUND?

首先,第一段报错和第二段报错的间隔一般很稳定,三次报 405 后就会变成 404,跟异常栈对应的源码,可以发现这段间隔对应的配置:

private int declarationRetries = 3;

这个字段是 spring 封装的 rabbit mq 包的 BlockingQueueConsumer 类,这个封装的消费者类,会在与指定队列绑定消费连接时,试图重声明队列,重试间隔默认 5000ms,在它试图重新声明这个匿名的排他队列时,会被无情的返回 405 拒绝连接,即使这个排他队列是它之前创建...
在三次重试过后差不多的时间点,这个队列会自动删除,没错,这个匿名队列不但是排他性 的,而且是 自动删除 的。在 rabbit 之后的消费者重连尝试中,就会返回 404 找不到指定队列的报错。

注意:需要区分 rabbit 消费者重连 重试和 队列重声明 重试机制。

  • 消费者重连 若不进行手动配置,在 RabbitAdmin 中就可以看到其实也是代码中写死的——5次尝试,但不管怎样肯定要比队列的重试周期长。

解决方案:

废了这么多话,各位很容易就能想出一个解决方案:把 队列重声明 的次数配置多一些就好了嘛,等旧的匿名排他队列自动删掉了,就可以正常的重声明出新的匿名排他队列了。那让我们看看怎么配、配完效果是什么?

尝试解决:加大 队列重声明 的次数

配置
在 github 的 spring cloud bus 的仓库并没有找到相关配置,但在 spring cloud stream binder rabbit 的仓库找到了:


该配置项是:spring.cloud.stream.rabbit.bingings..consumer.queue-declaration-retries,那么问题来了,这个 填什么???
只好从配置项注入代码附近入手,通过打断点在运行时的拿参数出来,可以知道 spring cloud bus 通过 spring cloud stream binder rabbit 创建队列的 channelName 属性值为 springCloudBusInput,那么带入配置一下,设一个很大数如999999即可。
结果
运行测试,依然在几次 405 报错后返回 404......
继续从源码分析入手,队列重声明调用的是 Channel.queueDeclarePassive 方法,搜了下发现用这个方法声明队列时,如果队列不存在就会报404....WTF....

最终解决:配置指定名称队列

那么现在还能怎样解决这个问题?第一次声明匿名队列的源码中有一段(RabbitExchangeQueueProvisioner.provisionConsumerDestination):

boolean anonymous = !StringUtils.hasText(group);
.......
if (anonymous) {
     queue = new Queue(queueName, false, true, true, queueArgs(queueName, properties.getExtension(), false));
}else {
     if (partitioned) {
         String partitionSuffix = "-" + properties.getInstanceIndex();
         queueName += partitionSuffix;
     }
     if (durable) {
         queue = new Queue(queueName, true, false, false, queueArgs(queueName, properties.getExtension(), false));
     }else {
         queue = new Queue(queueName, false, false, queueArgs(queueName, properties.getExtension(), false));
      }
}

可以看出非匿名队列是不会设置 排他性自动删除 的,而group 这个参数不为空时,就会用 group 为队列名进行声明。这个参数同样是可以配置的,同样在之前的github仓库中有介绍:

简言之:如果该值设为true,则使用 group 作为队列的名称。

为指定 channel 配置 group 没有找到可用配置,但可以通过设置一个全局默认 group 做到同样效果:

spring.cloud.stream.default.group=springCloudBus-${spring.application.name}-${spring.cloud.client.ip-address}-${server.port}

完整配置

spring:
  cloud:
    stream:
      default:
        group: ${spring.application.name}-${spring.cloud.client.ip-address}-${server.port}
      rabbit:
        bindings:
          springCloudBusInput:
            consumer:
              # 队列声明重试次数
              queue-declaration-retries: 2000
              # 重试间隔(ms)
              recovery-interval: 5000
              # 为true时,使用‘group’作为配置刷新队列的名称
              queue-name-group-only: true

经测试,网络断连恢复后,程序即时地恢复了消费连接,没有报错。

小小的感悟

其实整个问题解决下来,最后只用了一段配置就搞定了。官方文档里几十个配置项,一个个读下去每个都像是解决的方式,甚至有一些官方文档没介绍的配置,其实都直接可以写在配置文件里,启动时会自动注入。
总而言之,现在的开发工作很少离得开功能完善的开源库了,阅读源码是发现、解决问题的不二手段,甚至通过源码,你可以发现一些巧妙的间接的解决方式。

你可能感兴趣的:(【spring cloud 配置中心 + rabbit mq】网络断连恢复引起的配置无法动态刷新)