背景
最近,有读者反馈在使用 Dubbo 2.7.x 版本的应用级服务发现时遇到了问题。关于 Dubbo 应用级服务发现的基础概念,可以参考之前的文章《dubbo应用级服务发现初体验》,这里不再赘述。
读者提到,他们正在基于 Dubbo 2.7 的应用级服务发现功能开发一个 Dubbo 网关,但在按照文章写了 Demo 进行测试时,却遇到了 no provider 的错误。
首先,我觉得这个想法很有创意,真正将 Dubbo 应用级服务发现投入生产环境的公司并不多。其次,我自己当初写文章测试时并没有遇到这个问题,但为了帮助读者解决问题,我决定重新写一个 Demo 来复现和排查。
问题定位
我随手拿了一个平时测试用的 Dubbo Demo 工程(注意,这不是 Dubbo 源码中自带的 Demo),发现服务确实无法注册到 ZooKeeper 上。接着,我测试了不同的 Dubbo 2.7 子版本,发现都存在注册失败的问题。具体现象是:在 2.7.5 到 2.7.11 版本中,控制台不报错但注册失败;而在 2.7.12 版本中,则会抛出如下 NullPointerException (NPE) 错误:
2021-06-16 13:17:31,086 [Dubbo-framework-scheduler-thread-1] ERROR org.apache.dubbo.config.bootstrap.DubboBootstrap (DubboBootstrap.java:1172) - [DUBBO] refresh metadata and instance failed, dubbo version: 2.7.12, current host: 172.23.233.52
java.lang.NullPointerException
at org.apache.dubbo.registry.client.metadata.ServiceInstanceMetadataUtils.calInstanceRevision(ServiceInstanceMetadataUtils.java:249)
at org.apache.dubbo.registry.client.metadata.ServiceInstanceMetadataUtils.lambda$refreshMetadataAndInstance$6(ServiceInstanceMetadataUtils.java:272)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at org.apache.dubbo.registry.client.metadata.ServiceInstanceMetadataUtils.refreshMetadataAndInstance(ServiceInstanceMetadataUtils.java:271)
at org.apache.dubbo.config.bootstrap.DubboBootstrap.lambda$registerServiceInstance$20(DubboBootstrap.java:1170)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
这个报错提示我们,服务在注册环节可能存在问题。顺着这个错误堆栈进行调试,很快就定位到了问题的根源。
直接引发 NPE 的代码位于 org.apache.dubbo.registry.client.AbstractServiceDiscovery#register 方法。
在 2.7.11 及更早版本 中,代码是这样的:
@Override
public final void register(ServiceInstance serviceInstance) throws RuntimeException {
this.serviceInstance = serviceInstance;
doRegister(serviceInstance);
}
而在 2.7.12 版本 中,代码顺序被调整成了这样:
@Override
public final void register(ServiceInstance serviceInstance) throws RuntimeException {
doRegister(serviceInstance);
this.serviceInstance = serviceInstance;
}
为什么仅仅是调整了代码顺序,就会导致报错呢?进一步追踪发现,NPE 的源头是 this.serviceInstance 为 null。在原来的代码中,先对这个字段赋值,再执行 doRegister。而调整后的代码先执行 doRegister 再赋值。不巧的是,doRegister 方法执行时抛出了异常,更不幸的是,这个异常被“吃掉”了。
doRegister 方法的实现如下:
@Override
public final void register(ServiceInstance serviceInstance) throws RuntimeException {
assertDestroyed(REGISTER_ACTION);
assertInitialized(REGISTER_ACTION);
executeWithEvents(
of(new ServiceInstancePreRegisteredEvent(serviceDiscovery, serviceInstance)),
() -> serviceDiscovery.register(serviceInstance),
of(new ServiceInstanceRegisteredEvent(serviceDiscovery, serviceInstance))
);
}
其中关键的 executeWithEvents 方法会将捕获的异常以事件的形式分发出去:
protected final void executeWithEvents(Optional<? extends Event> beforeEvent,
ThrowableAction action,
Optional<? extends Event> afterEvent) {
beforeEvent.ifPresent(this::dispatchEvent);
try {
action.execute();
} catch (Throwable e) {
dispatchEvent(new ServiceDiscoveryExceptionEvent(this, serviceDiscovery, e));
}
afterEvent.ifPresent(this::dispatchEvent);
}
但是,这个分发出去的 ServiceDiscoveryExceptionEvent 事件并没有被任何监听器处理,也就是说,原始异常被默默地忽略了。这就是为什么在 2.7.11 及更早版本中,虽然控制台没有抛出异常,但服务也无法成功注册的真正原因。
那么,被吞掉的这个异常到底是什么呢?
java.lang.NoClassDefFoundError: org/apache/curator/x/discovery/ServiceDiscovery
其实,问题非常简单,就是缺少了一个必要的 JAR 包依赖。在项目的 pom.xml 中加入以下依赖就能解决:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>${version}</version>
</dependency>
为什么我当初写文章时没有遇到这个问题呢?因为那篇文章的 Demo 是直接在 Dubbo 源码工程中修改和运行的,源码里已经引入了所有相关的依赖。
更进一步
这虽然是个小问题,但对用户来说却相当困惑:为什么程序不报错,但服务就是注册不上去?如果不是 2.7.12 版本“附赠”了一个 NPE 错误,排查起来会更加困难。
于是,我在 Dubbo 的 GitHub 仓库提了一个 Issue 与社区交流。得到的反馈是,Dubbo 2.7.x 版本的应用级服务发现功能将不再积极维护,主要的开发和维护工作将集中在 3.x 版本上。
https://github.com/apache/dubbo/issues/8061
提这个 Issue 也是为了让后续遇到相同问题的用户能通过搜索找到线索,少走弯路。
同时,我也提交了一个 Pull Request (PR),在 executeWithEvents 方法的 catch 块中加了一行日志记录,让异常能更直观地被开发者看到。

https://github.com/apache/dubbo/pull/8066
在新版本(>=2.7.13)中,如果有开发者再遇到缺少依赖的问题,控制台会直接打印出错误日志,就像下面这样,大大降低了排查难度:
2021-06-16 16:58:02,210 [main] ERROR org.apache.dubbo.registry.client.EventPublishingServiceDiscovery (EventPublishingServiceDiscovery.java:287) - [DUBBO] Execute action throws and dispatch a ServiceDiscoveryExceptionEvent, dubbo version: 2.7.12, current host: 172.23.233.52
java.lang.BootstrapMethodError: java.lang.NoClassDefFoundError: org/apache/curator/x/discovery/ServiceDiscovery
at org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery.doRegister(ZookeeperServiceDiscovery.java:92)
at org.apache.dubbo.registry.client.AbstractServiceDiscovery.register(AbstractServiceDiscovery.java:33)
at org.apache.dubbo.registry.client.EventPublishingServiceDiscovery.lambda$register$0(EventPublishingServiceDiscovery.java:159)
at org.apache.dubbo.registry.client.EventPublishingServiceDiscovery.executeWithEvents(EventPublishingServiceDiscovery.java:285)
at org.apache.dubbo.registry.client.EventPublishingServiceDiscovery.register(EventPublishingServiceDiscovery.java:157)
at org.apache.dubbo.config.bootstrap.DubboBootstrap.lambda$doRegisterServiceInstance$21(DubboBootstrap.java:1192)
at java.util.ArrayList.forEach(ArrayList.java:1259)
...
既然 2.7.x 的应用级服务发现不再更新,那么下次或许可以写一篇分析 Dubbo 3.0 版本应用级服务发现源码的文章。如果你对这类 Java 微服务框架的底层原理和问题排查感兴趣,欢迎到云栈社区的后端 & 架构板块交流讨论,那里有更多关于系统设计和分布式技术的深度内容。