• 记一次dubbo整合nacos no Provider排查


    版本说明

    dubbo使用 org.apache.dubbo:dubbo:2.7.6及以后的版本,注册中心使用nacos

    问题说明

    生产者端先启动注册到注册中心后,然后再启动消费者端,消费者提示no provider,经过检查,消费者,注册中心,生产者之间的网络没问题,消费者服务也能正常注册到。注册中心。此时已经束手无测了,只能深入源码探究了。
    这里先说结论,在2.7.6版本后,如果服务url中未配置group,version字段,注册中心使用nacos。会导致在生产者先启动的情况下,再启动消费者再可能导致 no Provider的错误

    源码分析

    在消费者订阅生产者服务时会根据消费者配置的生产者接口信息,生成生产中的服务名称(dubbo服务的唯一标识)。然后向注册中心进行订阅
    那么直接来看关键方法
    org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener)

     @Override
        public void doSubscribe(final URL url, final NotifyListener listener) {
            Set<String> serviceNames = getServiceNames(url, listener);
    
            //Set corresponding serviceNames for easy search later
            if (isServiceNamesWithCompatibleMode(url)) {
                for (String serviceName : serviceNames) {
                    NacosInstanceManageUtil.setCorrespondingServiceNames(serviceName, serviceNames);
                }
            }
    
            doSubscribe(url, listener, serviceNames);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    获取服务名称之后进行订阅
    再看服务名称的创建
    org.apache.dubbo.registry.nacos.NacosRegistry#getServiceNames0

     private Set<String> getServiceNames0(URL url) {
            NacosServiceName serviceName = createServiceName(url);
    
            final Set<String> serviceNames;
    
            if (serviceName.isConcrete()) { // is the concrete service name
                serviceNames = new LinkedHashSet<>();
                serviceNames.add(serviceName.toString());
                // Add the legacy service name since 2.7.6
                String legacySubscribedServiceName = getLegacySubscribedServiceName(url);
                if (!serviceName.toString().equals(legacySubscribedServiceName)) {
                    //avoid duplicated service names
                    serviceNames.add(legacySubscribedServiceName);
                }
            } else {
                serviceNames = filterServiceNames(serviceName);
            }
    
            return serviceNames;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    继续看创建过程

    public NacosServiceName(URL url) {
            serviceInterface = url.getParameter(INTERFACE_KEY);
            category = isConcrete(serviceInterface) ? DEFAULT_CATEGORY : url.getParameter(CATEGORY_KEY);
            version = url.getParameter(VERSION_KEY, DEFAULT_PARAM_VALUE);
            group = url.getParameter(GROUP_KEY, DEFAULT_PARAM_VALUE);
            value = toValue();
        }
    private String toValue() {
            return category +
                    NAME_SEPARATOR + serviceInterface +
                    NAME_SEPARATOR + version +
                    NAME_SEPARATOR + group;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
        public static final String NAME_SEPARATOR = ":";
    
    • 1

    可以看出来dubbo服务的唯一标识名称 由 NAME_SEPARATOR(其实就是冒号:)作为间隔,最终是这个样子
    category:serviceInterface:version:group的形式构成,并且如果version,group等可能为空的字段为空。冒号也不会省略
    回到getServiceNames0中有一段为了兼容2.7.3版本之前的代码,因为在2.7.3之前如果没有category,需要给一个默认值。

       // Add the legacy service name since 2.7.6
         String legacySubscribedServiceName = getLegacySubscribedServiceName(url);
    
    • 1
    • 2
     /**
         * Get the legacy subscribed service name for compatible with Dubbo 2.7.3 and below
         *
         * @param url {@link URL}
         * @return non-null
         * @since 2.7.6
         */ 
    private String getLegacySubscribedServiceName(URL url) {
            StringBuilder serviceNameBuilder = new StringBuilder(DEFAULT_CATEGORY);
            appendIfPresent(serviceNameBuilder, url, INTERFACE_KEY);
            appendIfPresent(serviceNameBuilder, url, VERSION_KEY);
            appendIfPresent(serviceNameBuilder, url, GROUP_KEY);
            return serviceNameBuilder.toString();
        }
       private void appendIfPresent(StringBuilder target, URL url, String parameterName) {
            String parameterValue = url.getParameter(parameterName);
            if (!StringUtils.isBlank(parameterValue)) {
                target.append(SERVICE_NAME_SEPARATOR).append(parameterValue);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其实和新版的类似,不过在appendIfPresent中可以看出来如果 version,group为空的话,间隔符冒号也会被省略

    那么问题来了

     if (!serviceName.toString().equals(legacySubscribedServiceName)) {
                    //avoid duplicated service names
                    serviceNames.add(legacySubscribedServiceName);
                }
    
    • 1
    • 2
    • 3
    • 4

    即使新版的 category未在url中指定,使用默认的provider,由于version,group为空。也会被认为,serviceName和legacySubscribedServiceName不相等
    因为新版生成服务名称,version,group为空,不会忽略冒号,而legacySubscribedServiceName会忽略。
    而此时我们的生产者也是2.7.6之后的版本注册到注册中心的服务也是不忽略冒号的形如
    com.xishan.store.usercenter.userapi.facade.XXReadFacade::
    而按照legacySubscribedServiceName订阅的是没有冒号的路径
    com.xishan.store.usercenter.userapi.facade.XXReadFacade
    而注册中心实际上是没有这个服务的。

    在项目启动时会先后根据添加的者两个服务名称进行从nacos服务器上拉取实例信息。
    看一个关键方法
    org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker

       private void refreshInvoker(List<URL> invokerUrls) {
            Assert.notNull(invokerUrls, "invokerUrls should not be null");
    
            if (invokerUrls.size() == 1
                    && invokerUrls.get(0) != null
                    && EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
                this.forbidden = true; // Forbid to access
                this.invokers = Collections.emptyList();
                routerChain.setInvokers(this.invokers);
                destroyAllInvokers(); // Close all invokers
            } else {
               
                this.forbidden = false; // Allow to access
                ····省略代码····
    
            }
    
            // notify invokers refreshed
            this.invokersChanged();
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看出来对于同一个RegistryDirectory,如果使用旧版服务名com.xishan.store.usercenter.userapi.facade.XXReadFacade晚于新版服务名com.xishan.store.usercenter.userapi.facade.XXReadFacade::来到refreshInvoker,会使invoker被清空,forbidden设置为true,当我们要使用dubbo服务时,报出no Provider

      
        public List<Invoker<T>> doList(Invocation invocation) {
            if (forbidden) {
                // 1. No service provider 2. Service providers are disabled
                throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry " +
                        getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +
                        NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() +
                        ", please check status of providers(disabled, not registered or in blacklist).");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    Javaweb笔记:第05章_HTML & CSS & JavaScript & XML
    jenkins安装和配置(一):ubuntu 20.04 jenkins安装
    在 Kubernetes 上最小化安装 KubeSphere
    linux下磁盘分区和逻辑卷管理
    Docker部署GoLang程序,保姆级教程!
    leveldb-FilterBlock实现
    2024最新最全【网络安全/渗透测试】面试题汇总
    附录7-使用bootstrap组件
    pyspark连接mysql数据库报错
    【2024秋招】2023-9-20 度小满信贷系统平台部二面
  • 原文地址:https://blog.csdn.net/qq_37436172/article/details/126084178