☰
Dubbo 3.x 集成 Nacos 2.x 最小案例:服务注册发现与踩坑实录
2026/10/14 22:21:59 网站建设 项目流程

最近接了一个服务拆分选型的验证任务,需要快速确认 Dubbo 3.x 和 Nacos 2.x 能不能平稳地组合在一起跑通整个注册发现链路。我没有直接去翻官方文档,而是先搭了一个最简单的 Dubbo+Nacos 案例:一个 provider 把服务注册到 Nacos,一个 consumer 从 Nacos 拉地址列表,然后像调本地方法一样完成一次远程调用。这个案例看起来很小,但服务治理里最核心的几件事——服务注册、服务发现、健康检查、负载均衡——全都涉及了。这篇文章就把整个搭建过程原原本本记录下来,包括工程结构、关键配置、启动验证,还有三个我实际踩进去又爬出来的坑。如果你正在做服务化改造,或者刚接触 Dubbo 和 Nacos,想找一条最短的路径把环境跑起来,这篇可以作为参考。

1. 先弄明白这个小案例到底在干什么

1.1 服务提供者与服务消费者是什么关系

在聊代码之前,先建立一些基础概念。Dubbo 是一个 RPC 框架,核心解决“远程调用像本地调用一样简单”这件事。在分布式系统里,两个服务之间的调用不再是通过一个固定的 IP 加端口写死在代码里,而是通过一个注册中心来完成动态寻址。

我把这个模型拆成三个角色:服务提供者(Provider)、服务消费者(Consumer)、注册中心(Registry)。Provider 启动后把自己暴露的接口和地址注册到 Registry;Consumer 启动后订阅自己需要的服务列表,拿到 Provider 的地址,然后建立连接发起调用。

用一个生活场景类比:服务调用就像你找一家餐馆。传统写死地址的方式就像背诵每家餐馆的门牌号,门店一换地址你就找不到。注册中心相当于一个城市的美食 App,所有餐馆把店名、地址、电话登记上去,顾客打开 App 搜索就能找到,餐馆装修换地址也只需要在 App 上更新信息,顾客下次搜还是能搜到。

对应到技术细节:Provider 注册的是服务名、IP、Dubbo 协议端口;Consumer 订阅的是服务名的变化。当 Provider 新增或下线,注册中心会推送变更给订阅者,Consumer 动态更新自己的可用地址列表。这个过程在 Dubbo 里叫作服务注册与服务发现。

1.2 为什么推荐用 Nacos 作注册中心

Dubbo 官方支持的注册中心有好几个,常见的是 ZooKeeper、Nacos、Consul 和 Redis。我这个案例选择 Nacos,不是因为它是最强的,而是它在中小团队里综合成本最低。

对比一下主流选项:

注册中心部署难度自带控制台配置中心能力临时实例机制社区活跃度
ZooKeeper中等,集群管理复杂弱,需要额外工具无基于会话过期,不稳定高
Nacos低,单机一条命令有,服务列表直观可见自带,无需额外组件支持临时实例,心跳超时自动移除高
Consul中等有有,但使用习惯偏海外支持中等

ZooKeeper 本身是一个分布式协调系统,做注册中心属于“借力打力”。虽然也能用,但它的临时节点依赖会话保活,网络抖动容易导致服务被误删;而且它没有可视化控制台,排查问题不够直观。Eureka 早期用得多,但已经进入维护状态,新项目基本不再推荐。Consul 功能全面,不过部署和概念模型比 Nacos 重一些。

Nacos 最吸引我的两点,一是它把注册中心和配置中心合在了一起,一个小团队少维护一套系统;二是控制台做得清楚,服务注册没注册、实例健康不健康,打开浏览器一眼就能确认。对第一次跑通 Dubbo 环境的人来说,这种可视化反馈非常关键。

1.3 案例的工程结构设计

我设计了三个 Maven 模块:

  • api 模块:只放接口定义和公共模型
  • provider 模块:实现接口,把服务暴露到注册中心
  • consumer 模块:引用接口,通过注册中心完成远程调用

接口单独抽一个模块,是很多 Dubbo 实践经验里推荐的做法。因为 Provider 和 Consumer 都要依赖同一个接口定义,如果各自复制一份接口代码,只要包名或者方法签名稍微不一样,调用时就会出现“方法找不到”之类的诡异问题。接口作为独立模块,相当于双方共同遵守的一份契约,在代码层面就避免了这个坑。

2. 版本选型与环境准备

2.1 一套求稳的版本组合

Dubbo 和 Nacos 的版本兼容性在官方文档有说明,但真实开发里我建议直接抄一套被验证过的组合,而不是自己从零排列组合。

我的选择:

组件版本
JDK8
Maven3.6+
Spring Boot2.7.18
Dubbo3.2.x
Nacos Server2.3.x
nacos-client2.3.x

为什么用 Spring Boot 2.7 而不是 3.x?因为 Spring Boot 3 要求 JDK 17,而大部分存量系统的中间件兼容性都还是基于 JDK 8 验证的。虽然 Dubbo 3.x 已经支持 Spring Boot 3,但这次案例的目的是验证核心链路,不是追新,所以选择最稳定的组合。如果你是新项目且依赖很少,用 JDK 17 和 Spring Boot 3 当然也可以,只是遇到问题时可参考的资料会少一些。

2.2 Nacos 2.x 的一个隐藏端口坑

提前说一个 Nacos 2.x 的坑:它默认启用了 gRPC 通信,除了 8848 之外,客户端还会尝试连接 9848 和 9849 端口。9848 是客户端 gRPC 端口,规则是主端口加 1000。如果你部署在带防火墙的服务器上,或者用 Docker 映射端口,千万别只映射 8848,否则服务一直注册不上去,控制台里永远看不到实例。我在本地开发环境第一次遇到这个问题时,排查了半天,最后发现是端口没放通。

2.3 本地环境和依赖引入

先把基础环境确认好:JDK 8、Maven 3.6 以上、本地可以访问网络下载依赖。然后从 Nacos 官方渠道下载 server 压缩包,解压后先不急着启动,晚点在验证环节再启动它。

父工程的 pom 我用 dependencyManagement 统一管理版本,三个子模块都继承父工程。关键依赖写在 provider 和 consumer 里:

<dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>3.2.11</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>2.3.2</version> </dependency> </dependencies>

注意 nacos-client 需要显式引入。dubbo-spring-boot-starter 并不会自动把所有注册中心客户端都带进来,不写这个依赖,启动时就会报找不到 Nacos 相关类的错误。

consumer 额外加一个 spring-boot-starter-web,因为我要对外暴露一个 HTTP 接口来手动触发远程调用,方便在浏览器里反复验证。provider 其实不需要 web 容器,但加上也无妨,不影响核心逻辑。

3. 核心代码实现:从接口定义到远程调用

3.1 接口模块:把契约先定下来

在 api 模块定义一个最简单的服务接口:

package com.demo.api; public interface GreetingService { String sayHello(String name); }

这个接口没有复杂逻辑,只负责定义方法签名。有一点要特别强调:Provider 和 Consumer 中接口的包名、类名、方法签名必须完全一致。实际开发中,有人把接口类复制到两个工程,然后稍微改了一下包名,结果 Consumer 启动不报错,一调用就报No provider available,排查半天才发现包名不对。使用独立 api 模块就能从源头规避这个问题。

如果将来接口要传自定义对象,比如用户信息,那么这个对象也放在 api 模块,并且必须实现java.io.Serializable。因为 Dubbo 默认的协议在网络上传输数据时用的是二进制序列化,对象不实现序列化接口,运行时会抛出NotSerializableException。

3.2 Provider 模块:把实现类暴露为远程服务

provider 模块先引入 api 模块的依赖,然后实现接口:

package com.demo.provider.impl; import com.demo.api.GreetingService; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.stereotype.Component; @Component @DubboService public class GreetingServiceImpl implements GreetingService { @Override public String sayHello(String name) { return "Hello, " + name + "! This is provider at " + System.currentTimeMillis(); } }

注意注解用的是@DubboService,这是 Dubbo 3.x 的注解。如果是 Dubbo 2.7,对应的是@Service。这两个注解容易混,尤其网上旧文章很多,直接复制旧代码到新项目里,注解扫描不到,服务就不会暴露。

再看 provider 的核心配置:

server: port: 8081 dubbo: application: name: demo-provider registry: address: nacos://127.0.0.1:8848 protocol: name: dubbo port: 20880 scan: base-packages: com.demo.provider.impl

逐行解释一下:

  • dubbo.application.name:当前应用名,也是注册到 Nacos 上的服务标识之一,要保证在注册中心里唯一。
  • dubbo.registry.address:注册中心地址,格式是nacos://ip:port。这个前缀很关键,换注册中心时只需要改这里,业务代码完全不用动。
  • dubbo.protocol.name和dubbo.protocol.port:Dubbo RPC 通信的协议和端口。协议默认就是 dubbo,端口默认 20880。
  • dubbo.scan.base-packages:告诉 Dubbo 扫描哪个包下的@DubboService注解。漏掉这个配置,服务就无法暴露。

这里有一个新手最容易懵的点:server.port: 8081是 Spring Boot 内置的 Web 容器端口,而dubbo.protocol.port: 20880是 RPC 通信端口。它们是两个独立的东西。Provider 注册到 Nacos 的地址是 IP + 20880,Consumer 也是通过这个端口来建立 RPC 连接。我一开始学 Dubbo 时,以为调用的端口就是 Web 端口,结果惊险地发现 Provider 注册的是 20880,跟 Web 端口一点关系没有。

3.3 Consumer 模块:远程调用像本地调用

consumer 模块同样引入 api 模块,然后在需要调用的地方使用@DubboReference注入接口:

package com.demo.consumer.controller; import com.demo.api.GreetingService; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class GreetingController { @DubboReference private GreetingService greetingService; @GetMapping("/greet") public String greet(String name) { return greetingService.sayHello(name); } }

consumer 的配置:

server: port: 8082 dubbo: application: name: demo-consumer registry: address: nacos://127.0.0.1:8848

可以看到 consumer 端完全没有配置 Dubbo 协议端口,因为它不需要对外提供 RPC 服务,只需要作为客户端发起调用。@DubboReference会从注册中心拉取服务地址,创建一个远程服务的代理对象,调用时把方法参数序列化后通过网络发送给 Provider,之后再把结果反序列化返回给调用方。对调用方来说,整个过程确实就像调用一个本地方法。

Consumer 不依赖 provider 模块,只依赖 api 模块。这是一个很重要的设计——服务双方只通过接口契约耦合,具体实现完全隔离。这就是接口下沉带来的依赖倒置。

4. 启动与验证全流程

4.1 先把 Nacos 服务器跑起来

本地开发用单机模式启动 Nacos 就够。在解压目录的 bin 目录下执行:

Windows:

startup.cmd -m standalone

Linux / macOS:

sh startup.sh -m standalone

Nacos 2.x 启动比 1.x 慢一些,不要心急。看到日志输出Nacos started successfully才表示启动完成。然后打开控制台:http://127.0.0.1:8848/nacos,默认账号密码都是nacos。

控制台的“服务管理 -> 服务列表”是后续判断注册是否成功的主要依据。第一次打开时列表是空的,等 Provider 启动后再刷新。

4.2 依次启动 Provider 和 Consumer

启动顺序上建议先 Provider 后 Consumer。严格来说,Dubbo 的 Consumer 可以在 Provider 没有启动时先启动,因为@DubboReference默认不会在启动阶段强制检查 Provider 是否存在,但为了第一次演示时减少干扰,我习惯把 Provider 先拉起来。

Provider 启动成功后,在 Nacos 控制台上刷新服务列表,能看到demo-provider这个服务名,点击详情可以看到一个健康实例,IP 是本地地址,端口是 20880。

再启动 Consumer。启动完成后,在浏览器访问:

http://127.0.0.1:8082/greet?name=zhangsan

正常情况下浏览器会返回类似这样的内容:

Hello, zhangsan! This is provider at 1690012345678

返回内容中的时间戳每次应该不一样,因为每次请求都会真的发送到 Provider 执行一次。到这里,最小闭环已经跑通了:注册中心、服务发现、RPC 调用三个环节全部正常。

4.3 用多实例验证负载均衡

单实例跑通说明不了什么问题,我建议多做一步:启动第二个 Provider 实例,让程序自由选择一个可用的提供者。

第二个实例只需要改两个参数:Dubbo 协议端口改成 20881,Web 端口改成 8083。application.name 保持demo-provider不变。启动后回到 Nacos 控制台,刷新服务列表,demo-provider的健康实例会变成两个,分别对应 20880 和 20881。

然后连续刷新多次http://127.0.0.1:8082/greet?name=zhangsan,观察返回结果。Dubbo 默认的负载均衡策略是加权随机,理论上两次调用的结果会随机落在不同的 Provider 上。如果我在返回内容里加入机器标识字段,就能直观看到请求被分发到了不同的实例。

这一步验证的价值在于:Consumer 并没有重启,它就自动感知到了 Provider 实例数量从一到二的变化。这就是注册中心动态服务发现的实际意义,也是服务扩容时不需要修改调用端的核心原因。

4.4 顺带理解 Nacos 的配置中心能力

Nacos 除了做注册中心,还能做配置中心。在这个案例里我没有把配置动态刷新完整写进去,因为那需要额外引入配置中心的相关依赖,会让最小案例变复杂。但我建议你打开 Nacos 控制台的“配置管理”看一眼,新建一条配置,然后想想如果 Provider 或 Consumer 运行时读取的是 Nacos 上的配置,意味着改动配置后不需要重新部署服务。

这一点对生产环境非常重要。传统方式改配置需要重新打包、重启服务,引入配置中心后,改配置和发配置通知几乎是实时的。Dubbo 的注册中心地址本身也可以放在 Nacos 配置里,进一步减少配置文件里的环境差异。

5. 常见问题与排查心得

5.1 服务一直注册不上:优先查端口和网络

我这个案例里最常碰到的问题,就是 Provider 启动时没有任何报错,但 Nacos 控制台服务列表是空的。这种问题通常不是代码逻辑问题,而网络和端口的问题。

排查顺序我固定为以下几步:

  1. 确认 Provider 配置的nacos://127.0.0.1:8848能通:
    telnet 127.0.0.1 8848
  2. 确认 9848 端口也通。Nacos 2.x 的 gRPC 客户端一定会连这个端口,只检查 8848 是不全面的。
  3. 看 Provider 日志里有没有nacos相关报错,比如连接超时、注册失败。
  4. 检查同一个 Dubbo 协议端口是不是被其他进程占用。本地起多个 Provider 时,如果都用了 20880,后面的进程会直接报端口绑定失败。

还有一个小经验:本地调试时如果 Nacos 地址写了机器主机名,而 hosts 里没配好映射,也会注册不上。统一用127.0.0.1最省事。

5.2 调用时报 No provider available:先从包名和注解查起

No provider available是 Dubbo 调用失败时最典型、也最让人头疼的报错。它表示 Consumer 在注册中心没有找到可用的服务提供者。

可能的原因我按出现频率排序:

  1. 接口包名不一致。Consumer 引用的 api 模块和 Provider 实现的接口包名不同,注册到 Nacos 的服务名虽然一样,但 dubbo 内部做服务匹配时要求接口全限定名一致。
  2. @DubboService没被扫描到。dubbo.scan.base-packages配置的包路径写错,或者实现类上用的是 Spring 的@Service而不是 Dubbo 的注解。
  3. Provider 启动后很快就退出。比如启动时发生异常,或者进程被本地 IDE 中断,Nacos 上的实例会在一段时间后被移除。
  4. Consumer 启动时 Provider 还没完全注册成功。如果你的验证方式是用 ApplicationRunner 在启动阶段就发起远程调用,很容易踩到这个时序问题。所以我建议用 Controller 加手动浏览器访问的方式验证,避开这个坑。

5.3 本地多实例启动的端口冲突与序列化问题

本地验证负载均衡时会启动多个 Provider,这里有两个容易忽略的冲突。

第一个是 Dubbo QoS 端口冲突。Dubbo 3.x 默认会在 22222 端口启动 QoS 服务,同一台机器启动多个 Dubbo 进程时,第二个会报端口被占用。解决办法是在配置里显式关闭或改端口:

dubbo: application: qos-enable: false

或者给每个应用配置不同的 QoS 端口。

第二个是 Dubbo 协议端口冲突。之前提到过,需要保证每个 Provider 的dubbo.protocol.port不相同,比如第一个用 20880,第二个用 20881。

序列化问题同样值得提醒。当接口方法参数是自定义对象时,对象必须实现Serializable。我看到过一个很典型的报错:

java.io.NotSerializableException: com.demo.api.UserInfo

原因就是接口方法接收一个UserInfo对象,但这个对象没有实现序列化接口。Dubbo 默认 dubbo 协议走的是 TCP 二进制流,任何跨进程传输的对象都逃不过序列化这一步。这种情况下,就算本地测试不出问题,一旦真正远程调用就会立刻暴露。

6. 实际体会与可扩展的方向

整套 Dubbo+Nacos 最小案例跑下来,我最大的感受是:Dubbo 对新手最友好的地方,在于它把复杂的远程通信细节全部封装掉了。从代码层面看,你写的就是一个接口、一个实现、一个注解注入,但背后涉及的服务注册、心跳续约、地址推送、连接管理、负载均衡,每一个环节都是可以拿出来单独深入研究的。

如果你想把这套案例往生产方向推进,我建议按这个顺序继续扩展:

  • 给 Provider 做优雅上下线,让服务在下线前先把实例标记为不健康,等请求排空后再退出。
  • 引入 Nacos 配置中心,把线程池参数、开关类配置放到 Nacos 上动态刷新,而不是每次改代码重启。
  • 在 Consumer 侧接入限流和熔断组件,避免单个 Provider 故障导致全链路雪崩。
  • 给服务调用加上链路追踪,这样多级调用时能一眼看清每个环节的耗时。

最后分享一个我自己坚持的习惯:像这种框架集成案例,第一遍一定要亲手从空工程开始搭建,不要直接复制现成代码。只有自己一个个依赖引进去、一行行配置写上去,遇到那几个典型的坑并亲自解决,你才会真正理解注册中心和 RPC 之间的关系。跑通之后再考虑抽象和封装,就会顺手很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询