2.7.6版本之后nacos作为注册中心,version,group为空导致 no Provider
- [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
- 生产者和消费者服务地址url的group,version为空
- 先启动生产者,服务正常注册到注册中心
- 启动消费者,可能导致服务调用 报错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
升级到 2.7 最新版本试下
实测在最新2.7.16版本下仍有这个问题
升级到 2.7 最新版本试下
@404008945 provider 和 consumer 都升级,2.7 的早期有个版本注册是有问题的
@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

然后我又看了消费者启动时初始化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;
}
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,由于实例为空的服务名也被订阅,那么这时候还是可能会发生被空的实例覆盖的情况。
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.
@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
然后我又看了消费者启动时初始化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; }
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
还是会订阅一个空服务,这个空服务对应的接口还是同一个URL,这个时候难道就不会出现No provider available?
这个取决于 nacos 是否会主动推空,这块取决于 nacos 的缓存与版本。2.7.17 及以后的版本在 Dubbo 侧做了过滤,防止空覆盖。
换另一个角度来说最好的解决方法还是需要提供group version值是最完美的解决方法是吗?
Dubbo 默认就是支持空 group、version 的,只是在实践中指定了 group、version 可以更好地实现服务管理。
@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

这里因为两种url都公用一个监听器,而监听器的的变量serviceName方法入参传递过来,那么肯定只能是其中一个url,这样两种url服务的变化都反应到了这一个入参url,那么还是有可能会发生空覆盖
org.apache.dubbo.registry.nacos.NacosAggregateListener 内部会聚合
org.apache.dubbo.registry.nacos.NacosAggregateListener 内部会聚合
NacosAggregateListener 内部会聚合这个我明白,我的意思是两种url共用了同一个nacosListener(因为computeIfAbsent),都会触发同一个nacosListener,而这个nacosListener中的serviceName又是固定的(先进入方法的那个serviceName)。
现在假设 同一服务的两种serviceName,A1(有实例)和A2(空实例)
- A1首先进入subscribeEventListener方法,那么会注册添加一个nacos监听器EventListener,使用匿名内部类的形式创建,内部类中的serviceName就是A1,然后使用这个nacosListener监听A1
- A2之后再进入subscribeEventListener ,A2会和A1共用同一个nacosListener,那么这个nacosListener的serviceName仍然是A1 同样使用这个nacosListener监听A2
那么当 nacos通知A2无实例时,调用nacosListener,监听器中调用notifySubscriber(url, serviceName, listener, instances);
但是这个serviceNames是A1,变成了通知A1无实例

这一块看了下 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
那么当 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());
}
@404008945 想问一下,这个问题如果做到必现的话,是不是取决于Set<String> serviceNames = getServiceNames(url, nacosAggregateListener);的迭代顺序?
@404008945 想问一下,这个问题如果做到必现的话,是不是取决于
Set<String> serviceNames = getServiceNames(url, nacosAggregateListener);的迭代顺序?
@huwenming-saw 是的,首先进入这个方法的serviceName是非空实例,第二个进入的是空实例,那么就必现,并且nacos-cli的版本用1.3.1或者1.3.3。另外我试了1.4.x,nacos不会在订阅的时候主动触发服务变动,这种情况就不会有问题了
@404008945 谢谢解答~
@huwenming-saw 那最终解决这个问题的方法是什么呢
@404008945 现在这边也遇到这个问题,重启服务器就偶尔会发生这种情况,跟你你们的方法也debug了源码去看了,还是对的上的,这边使用的是dubbo版本2.7.22,然后nacos-client版本是1.4.4。重启后消费者调用的时候,时不时还是报了no provider avaliable的错