dubbo icon indicating copy to clipboard operation
dubbo copied to clipboard

2.7.6版本之后nacos作为注册中心,version,group为空导致 no Provider

Open 404008945 opened this issue 3 years ago • 4 comments

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Environment

  • Dubbo version: 2.7.6及以后
  • Operating System version: windows 10
  • Java version: 1.8

Steps to reproduce this issue

  1. 生产者和消费者服务地址url的group,version为空
  2. 先启动生产者,服务正常注册到注册中心
  3. 启动消费者,可能导致服务调用 报错No provider

Expected Behavior

服务正常调用

Actual Behavior

org.apache.dubbo.registry.integration.DynamicDirectory#doList

   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).");

My suggestion

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;
    }

2.7.6及以后生成规则

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;
    }

getLegacySubscribedServiceName生成规则

 /**
     * 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);
        }
    }

2.7.6版本之后生成的serviceName,即使group,version为空也不会省略间隔符NAME_SEPARATOR,而getLegacySubscribedServiceName方法中,如果group,version为空会省略冒号,也就是说serviceName和legacySubscribedServiceName在应该相等的情况下,也因为NAME_SEPARATOR被判断为了不相等,重复添加了,legacySubscribedServiceName也作为服务名。那么就会有两个服务名形如 provider:com.xishan.store.usercenter.userapi.facade.XXReadFacade:: provider:com.xishan.store.usercenter.userapi.facade.XXReadFacade

生产者也使用的2.7.6后的版本,那么注册中心只会有provider:com.xishan.store.usercenter.userapi.facade.XXReadFacade:: 而 provider:com.xishan.store.usercenter.userapi.facade.XXReadFacade是不存在的

那么如果如果兼容旧版的legacySubscribedServiceName晚于实际存在的服务名进入 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();
    }

导致invoker被清空,forbidden设置为true,当我们要使用dubbo服务时,报出no Provider

404008945 avatar Jul 31 '22 04:07 404008945

升级到 2.7 最新版本试下

AlbumenJ avatar Aug 01 '22 06:08 AlbumenJ

实测在最新2.7.16版本下仍有这个问题

升级到 2.7 最新版本试下

404008945 avatar Aug 01 '22 10:08 404008945

@404008945 provider 和 consumer 都升级,2.7 的早期有个版本注册是有问题的

AlbumenJ avatar Aug 04 '22 03:08 AlbumenJ

@404008945 provider 和 consumer 都升级,2.7 的早期有个版本注册是有问题的

@AlbumenJ 问题找到了。 生产和消费者都是升级了最新版的2.7.16,另外nacos-client为1.3.3时有问题,测试nacos-client版本为2.0.4已经没有问题了 问题记录 生产者注册服务名称逻辑为 org.apache.dubbo.common.URL#getColonSeparatedKey

  public String getColonSeparatedKey() {
        StringBuilder serviceNameBuilder = new StringBuilder();
        serviceNameBuilder.append(this.getServiceInterface());
        append(serviceNameBuilder, VERSION_KEY, false);
        append(serviceNameBuilder, GROUP_KEY, false);
        return serviceNameBuilder.toString();
    }

当version为空时并不会省略掉间隔符冒号 于是注册的服务名称是 io.seata.samples.tcc.dubbo.action.TccActionOne::tcc image

然后我又看了消费者启动时初始化invoke时,生成的订阅服务名称逻辑 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;
    }

image version为空,新版服务名version为空,不会省略间隔符冒号,根据注释,为了兼容2.7.3以前的版本,另外生成服务名 version为空并且把冒号也省略掉了。最终结果就是新版比旧版多了一个冒号。 后面会继续分别订阅这两个服务名称,其中一个服务名在注册中心不存在,是空服务。 org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.Set<java.lang.String>)

 private void doSubscribe(final URL url, final NotifyListener listener, final Set<String> serviceNames) {
        execute(namingService -> {
            if (isServiceNamesWithCompatibleMode(url)) {
                List<Instance> allCorrespondingInstanceList = Lists.newArrayList();

                /**
                 * Get all instances with serviceNames to avoid instance overwrite and but with empty instance mentioned
                 * in https://github.com/apache/dubbo/issues/5885 and https://github.com/apache/dubbo/issues/5899
                 *
                 * namingService.getAllInstances with {@link org.apache.dubbo.registry.support.AbstractRegistry#registryUrl}
                 * default {@link DEFAULT_GROUP}
                 *
                 * in https://github.com/apache/dubbo/issues/5978
                 */
                for (String serviceName : serviceNames) {
                    List<Instance> instances = namingService.getAllInstances(serviceName,
                            getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP));
                    NacosInstanceManageUtil.initOrRefreshServiceInstanceList(serviceName, instances);
                    allCorrespondingInstanceList.addAll(instances);
                }
                notifySubscriber(url, listener, allCorrespondingInstanceList);
                for (String serviceName : serviceNames) {
                    subscribeEventListener(serviceName, url, listener);
                }
            } else {
                List<Instance> instances = new LinkedList<>();
                for (String serviceName : serviceNames) {
                    instances.addAll(namingService.getAllInstances(serviceName
                            , getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP)));
                    notifySubscriber(url, listener, instances);
                    subscribeEventListener(serviceName, url, listener);
                }
            }

        });
    }

上面有段很好的注释 ,Get all instances with serviceNames to avoid instance overwrite and but with empty instance mentioned获取所有的serviceNames 的实例,防止被空的实例覆盖。看起来好像没有问题。 但是下面的循环serviceNames订阅

 for (String serviceName : serviceNames) {
                    subscribeEventListener(serviceName, url, listener);
  }

但是注册中心为nacos时 这里来到了nacos客户端 版本为1.3.3时

public void addListener(ServiceInfo serviceInfo, String clusters, EventListener listener) {
        
        NAMING_LOGGER.info("[LISTENER] adding " + serviceInfo.getName() + " with " + clusters + " to listener map");
        List<EventListener> observers = Collections.synchronizedList(new ArrayList<EventListener>());
        observers.add(listener);
        
        observers = observerMap.putIfAbsent(ServiceInfo.getKey(serviceInfo.getName(), clusters), observers);
        if (observers != null) {
            observers.add(listener);
        }
        
        serviceChanged(serviceInfo);
    }
    

会发现在第一次添加监听器时还是会触发serviceChange,进而触发notify,重新刷新invoker,由于实例为空的服务名也被订阅,那么这时候还是可能会发生被空的实例覆盖的情况。

404008945 avatar Aug 05 '22 15:08 404008945

This issue has been fixed in the latest 2.7.17 and 3.0.11 bugfix versions. Also fixed in the 3.1.0 feature branch.

chickenlj avatar Sep 02 '22 02:09 chickenlj

@404008945 provider 和 consumer 都升级,2.7 的早期有个版本注册是有问题的

@AlbumenJ 问题找到了。 生产和消费者都是升级了最新版的2.7.16,另外nacos-client为1.3.3时有问题,测试nacos-client版本为2.0.4已经没有问题了 问题记录 生产者注册服务名称逻辑为 org.apache.dubbo.common.URL#getColonSeparatedKey

  public String getColonSeparatedKey() {
        StringBuilder serviceNameBuilder = new StringBuilder();
        serviceNameBuilder.append(this.getServiceInterface());
        append(serviceNameBuilder, VERSION_KEY, false);
        append(serviceNameBuilder, GROUP_KEY, false);
        return serviceNameBuilder.toString();
    }

当version为空时并不会省略掉间隔符冒号 于是注册的服务名称是 io.seata.samples.tcc.dubbo.action.TccActionOne::tcc image

然后我又看了消费者启动时初始化invoke时,生成的订阅服务名称逻辑 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;
    }

image version为空,新版服务名version为空,不会省略间隔符冒号,根据注释,为了兼容2.7.3以前的版本,另外生成服务名 version为空并且把冒号也省略掉了。最终结果就是新版比旧版多了一个冒号。 后面会继续分别订阅这两个服务名称,其中一个服务名在注册中心不存在,是空服务。 org.apache.dubbo.registry.nacos.NacosRegistry#doSubscribe(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.Set<java.lang.String>)

 private void doSubscribe(final URL url, final NotifyListener listener, final Set<String> serviceNames) {
        execute(namingService -> {
            if (isServiceNamesWithCompatibleMode(url)) {
                List<Instance> allCorrespondingInstanceList = Lists.newArrayList();

                /**
                 * Get all instances with serviceNames to avoid instance overwrite and but with empty instance mentioned
                 * in https://github.com/apache/dubbo/issues/5885 and https://github.com/apache/dubbo/issues/5899
                 *
                 * namingService.getAllInstances with {@link org.apache.dubbo.registry.support.AbstractRegistry#registryUrl}
                 * default {@link DEFAULT_GROUP}
                 *
                 * in https://github.com/apache/dubbo/issues/5978
                 */
                for (String serviceName : serviceNames) {
                    List<Instance> instances = namingService.getAllInstances(serviceName,
                            getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP));
                    NacosInstanceManageUtil.initOrRefreshServiceInstanceList(serviceName, instances);
                    allCorrespondingInstanceList.addAll(instances);
                }
                notifySubscriber(url, listener, allCorrespondingInstanceList);
                for (String serviceName : serviceNames) {
                    subscribeEventListener(serviceName, url, listener);
                }
            } else {
                List<Instance> instances = new LinkedList<>();
                for (String serviceName : serviceNames) {
                    instances.addAll(namingService.getAllInstances(serviceName
                            , getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP)));
                    notifySubscriber(url, listener, instances);
                    subscribeEventListener(serviceName, url, listener);
                }
            }

        });
    }

上面有段很好的注释 ,Get all instances with serviceNames to avoid instance overwrite and but with empty instance mentioned获取所有的serviceNames 的实例,防止被空的实例覆盖。看起来好像没有问题。 但是下面的循环serviceNames订阅

 for (String serviceName : serviceNames) {
                    subscribeEventListener(serviceName, url, listener);
  }

但是注册中心为nacos时 这里来到了nacos客户端 版本为1.3.3时

public void addListener(ServiceInfo serviceInfo, String clusters, EventListener listener) {
        
        NAMING_LOGGER.info("[LISTENER] adding " + serviceInfo.getName() + " with " + clusters + " to listener map");
        List<EventListener> observers = Collections.synchronizedList(new ArrayList<EventListener>());
        observers.add(listener);
        
        observers = observerMap.putIfAbsent(ServiceInfo.getKey(serviceInfo.getName(), clusters), observers);
        if (observers != null) {
            observers.add(listener);
        }
        
        serviceChanged(serviceInfo);
    }
    

会发现在第一次添加监听器时还是会触发serviceChange,进而触发notify,重新刷新invoker,由于实例为空的服务名也被订阅,那么这时候还是可能会发生被空的实例覆盖的情况。

问一下您,看您的描述,即使升级到“生产和消费者都是升级了最新版的2.7.16,另外nacos-client为1.3.3时有问题,测试nacos-client版本为2.0.4已经没有问题了”您说的这种情况中的dubbo-2.7.16,nacos-2.0.4,还是会订阅一个空服务,这个空服务对应的接口还是同一个URL,这个时候难道就不会出现No provider available?;换另一个角度来说最好的解决方法还是需要提供group version值是最完美的解决方法是吗?期待您的回复,这个No provider available困扰我很久了

huwenming-saw avatar Sep 30 '22 09:09 huwenming-saw

@huwenming-saw

还是会订阅一个空服务,这个空服务对应的接口还是同一个URL,这个时候难道就不会出现No provider available?

这个取决于 nacos 是否会主动推空,这块取决于 nacos 的缓存与版本。2.7.17 及以后的版本在 Dubbo 侧做了过滤,防止空覆盖。

换另一个角度来说最好的解决方法还是需要提供group version值是最完美的解决方法是吗?

Dubbo 默认就是支持空 group、version 的,只是在实践中指定了 group、version 可以更好地实现服务管理。

AlbumenJ avatar Oct 08 '22 01:10 AlbumenJ

@huwenming-saw

还是会订阅一个空服务,这个空服务对应的接口还是同一个URL,这个时候难道就不会出现No provider available?

这个取决于 nacos 是否会主动推空,这块取决于 nacos 的缓存与版本。2.7.17 及以后的版本在 Dubbo 侧做了过滤,防止空覆盖。

换另一个角度来说最好的解决方法还是需要提供group version值是最完美的解决方法是吗?

Dubbo 默认就是支持空 group、version 的,只是在实践中指定了 group、version 可以更好地实现服务管理。

@AlbumenJ 今天看了2.7.17版本,看起来是将两种形式的url共用实例,只要其中一个url存在实例,那么都不会no Provider,但是测了一下还是有问题,这里的serviceName是否应该为 event.serviceName 因为虽然分别订阅两种url,但是添加监听器使用的逻辑是使用的computeIfAbsent那么实际上两种url实际上共用了一个监听器

 listeners.computeIfAbsent(listener, k -> {  ....................

org.apache.dubbo.registry.nacos.NacosRegistry#subscribeEventListene image

这里因为两种url都公用一个监听器,而监听器的的变量serviceName方法入参传递过来,那么肯定只能是其中一个url,这样两种url服务的变化都反应到了这一个入参url,那么还是有可能会发生空覆盖

404008945 avatar Oct 08 '22 14:10 404008945

org.apache.dubbo.registry.nacos.NacosAggregateListener 内部会聚合

AlbumenJ avatar Oct 09 '22 01:10 AlbumenJ

org.apache.dubbo.registry.nacos.NacosAggregateListener 内部会聚合

NacosAggregateListener 内部会聚合这个我明白,我的意思是两种url共用了同一个nacosListener(因为computeIfAbsent),都会触发同一个nacosListener,而这个nacosListener中的serviceName又是固定的(先进入方法的那个serviceName)。

现在假设 同一服务的两种serviceName,A1(有实例)和A2(空实例)

  1. A1首先进入subscribeEventListener方法,那么会注册添加一个nacos监听器EventListener,使用匿名内部类的形式创建,内部类中的serviceName就是A1,然后使用这个nacosListener监听A1
  2. A2之后再进入subscribeEventListener ,A2会和A1共用同一个nacosListener,那么这个nacosListener的serviceName仍然是A1 同样使用这个nacosListener监听A2

那么当 nacos通知A2无实例时,调用nacosListener,监听器中调用notifySubscriber(url, serviceName, listener, instances); 但是这个serviceNames是A1,变成了通知A1无实例 image

404008945 avatar Oct 09 '22 01:10 404008945

这一块看了下 2.7 里面是会存在 serviceName 覆盖的问题的,3.0 及以后的版本是有以 serviceName 作为 key 的,应该没这个问题。 2.7 版本目前已经进行仅安全维护阶段了,bugfix 只会在 3.0 3.1 分支中进行了。

https://github.com/apache/dubbo/blob/15f4a7d7cbbd4ba1c580f4c3673b2854cef52f0b/dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/NacosRegistry.java#L137

AlbumenJ avatar Oct 10 '22 02:10 AlbumenJ

那么当 nacos通知A2无实例时,调用nacosListener,监听器中调用notifySubscriber(url, serviceName, listener, instances); 但是这个serviceNames是A1,变成了通知A1无实例

@404008945 是的,我也认为是这样的,共用同一个EventListener,但是因为匿名内部类的原因导致调用notifySubscriber(url, serviceName, listener, instances)时的serviceName相同。@AlbumenJ 虽然NacosAggregateListener内部会聚合,但是因为公用同一个EventListener的serviceName相同都是A1,导致NacosAggregateListener聚合时出现全部为空的A1无实例

    private void notifySubscriber(URL url, String serviceName, NacosAggregateListener listener, Collection<Instance> instances) {
        List<Instance> enabledInstances = new LinkedList<>(instances);
        if (enabledInstances.size() > 0) {
            //  Instances
            filterEnabledInstances(enabledInstances);
        }
        //saveAndAggregateAllInstances方法被触发,返回的是多个serviceName的聚合结果
        List<URL> aggregatedUrls = toUrlWithEmpty(url, listener.saveAndAggregateAllInstances(serviceName, enabledInstances));
        NacosRegistry.this.notify(url, listener.getNotifyListener(), aggregatedUrls);
    }

    public List<Instance> saveAndAggregateAllInstances(String serviceName, List<Instance> instances) {
        serviceNames.add(serviceName);
        serviceInstances.put(serviceName, instances);//serviceInstances本来是{A1:instances,A2:emptyinstances},因为EventListener的serviceName相同都是A1,Nacos根据订阅通知时导致serviceInstances里边全部被替换为{A1:emptyinstances,A2:emptyinstances}
        return serviceInstances.values().stream().flatMap(List::stream).collect(Collectors.toList());
    }

huwenming-saw avatar Oct 10 '22 02:10 huwenming-saw

@404008945 想问一下,这个问题如果做到必现的话,是不是取决于Set<String> serviceNames = getServiceNames(url, nacosAggregateListener);的迭代顺序?

huwenming-saw avatar Oct 10 '22 02:10 huwenming-saw

@404008945 想问一下,这个问题如果做到必现的话,是不是取决于Set<String> serviceNames = getServiceNames(url, nacosAggregateListener);的迭代顺序?

@huwenming-saw 是的,首先进入这个方法的serviceName是非空实例,第二个进入的是空实例,那么就必现,并且nacos-cli的版本用1.3.1或者1.3.3。另外我试了1.4.x,nacos不会在订阅的时候主动触发服务变动,这种情况就不会有问题了

404008945 avatar Oct 10 '22 02:10 404008945

@404008945 谢谢解答~

huwenming-saw avatar Oct 10 '22 02:10 huwenming-saw

@huwenming-saw 那最终解决这个问题的方法是什么呢

samky-sq avatar Oct 25 '23 09:10 samky-sq

@404008945 现在这边也遇到这个问题,重启服务器就偶尔会发生这种情况,跟你你们的方法也debug了源码去看了,还是对的上的,这边使用的是dubbo版本2.7.22,然后nacos-client版本是1.4.4。重启后消费者调用的时候,时不时还是报了no provider avaliable的错

samky-sq avatar Oct 25 '23 09:10 samky-sq