找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4140

积分

0

好友

547

主题
发表于 3 天前 | 查看: 15| 回复: 0

背景

最近,有读者反馈在使用 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.serviceInstancenull。在原来的代码中,先对这个字段赋值,再执行 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 块中加了一行日志记录,让异常能更直观地被开发者看到。

Dubbo EventPublishingServiceDiscovery 类 executeWithEvents 方法代码截图,展示了添加 logger.error 日志的改动

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 微服务框架的底层原理和问题排查感兴趣,欢迎到云栈社区后端 & 架构板块交流讨论,那里有更多关于系统设计和分布式技术的深度内容。




上一篇:Dubbo3应用级服务发现详解:从接口级迁移、原理到实践
下一篇:Go协程池实战指南:何时使用以优化性能与内存?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 12:17 , Processed in 0.493740 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表