资讯动态

Dubbo 3.x 集成 Nacos 2.x 最小案例:服务注册发现与踩坑实录

发布时间:2026/10/14 22:22:02 来源:尧图企业网站定制
最近接了一个服务拆分选型的验证任务需要快速确认 Dubbo 3.x 和 Nacos 2.x 能不能平稳地组合在一起跑通整个注册发现链路。我没有直接去翻官方文档而是先搭了一个最简单的 DubboNacos 案例一个 provider 把服务注册到 Nacos一个 consumer 从 Nacos 拉地址列表然后像调本地方法一样完成一次远程调用。这个案例看起来很小但服务治理里最核心的几件事——服务注册、服务发现、健康检查、负载均衡——全都涉及了。这篇文章就把整个搭建过程原原本本记录下来包括工程结构、关键配置、启动验证还有三个我实际踩进去又爬出来的坑。如果你正在做服务化改造或者刚接触 Dubbo 和 Nacos想找一条最短的路径把环境跑起来这篇可以作为参考。1. 先弄明白这个小案例到底在干什么1.1 服务提供者与服务消费者是什么关系在聊代码之前先建立一些基础概念。Dubbo 是一个 RPC 框架核心解决“远程调用像本地调用一样简单”这件事。在分布式系统里两个服务之间的调用不再是通过一个固定的 IP 加端口写死在代码里而是通过一个注册中心来完成动态寻址。我把这个模型拆成三个角色服务提供者Provider、服务消费者Consumer、注册中心Registry。Provider 启动后把自己暴露的接口和地址注册到 RegistryConsumer 启动后订阅自己需要的服务列表拿到 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 的版本兼容性在官方文档有说明但真实开发里我建议直接抄一套被验证过的组合而不是自己从零排列组合。我的选择组件版本JDK8Maven3.6Spring Boot2.7.18Dubbo3.2.xNacos Server2.3.xnacos-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 groupIdorg.apache.dubbo/groupId artifactIddubbo-spring-boot-starter/artifactId version3.2.11/version /dependency dependency groupIdcom.alibaba.nacos/groupId artifactIdnacos-client/artifactId version2.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.portDubbo RPC 通信的协议和端口。协议默认就是 dubbo端口默认 20880。dubbo.scan.base-packages告诉 Dubbo 扫描哪个包下的DubboService注解。漏掉这个配置服务就无法暴露。这里有一个新手最容易懵的点server.port: 8081是 Spring Boot 内置的 Web 容器端口而dubbo.protocol.port: 20880是 RPC 通信端口。它们是两个独立的东西。Provider 注册到 Nacos 的地址是 IP 20880Consumer 也是通过这个端口来建立 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 目录下执行Windowsstartup.cmd -m standaloneLinux / macOSsh startup.sh -m standaloneNacos 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?namezhangsan正常情况下浏览器会返回类似这样的内容Hello, zhangsan! This is provider at 1690012345678返回内容中的时间戳每次应该不一样因为每次请求都会真的发送到 Provider 执行一次。到这里最小闭环已经跑通了注册中心、服务发现、RPC 调用三个环节全部正常。4.3 用多实例验证负载均衡单实例跑通说明不了什么问题我建议多做一步启动第二个 Provider 实例让程序自由选择一个可用的提供者。第二个实例只需要改两个参数Dubbo 协议端口改成 20881Web 端口改成 8083。application.name 保持demo-provider不变。启动后回到 Nacos 控制台刷新服务列表demo-provider的健康实例会变成两个分别对应 20880 和 20881。然后连续刷新多次http://127.0.0.1:8082/greet?namezhangsan观察返回结果。Dubbo 默认的负载均衡策略是加权随机理论上两次调用的结果会随机落在不同的 Provider 上。如果我在返回内容里加入机器标识字段就能直观看到请求被分发到了不同的实例。这一步验证的价值在于Consumer 并没有重启它就自动感知到了 Provider 实例数量从一到二的变化。这就是注册中心动态服务发现的实际意义也是服务扩容时不需要修改调用端的核心原因。4.4 顺带理解 Nacos 的配置中心能力Nacos 除了做注册中心还能做配置中心。在这个案例里我没有把配置动态刷新完整写进去因为那需要额外引入配置中心的相关依赖会让最小案例变复杂。但我建议你打开 Nacos 控制台的“配置管理”看一眼新建一条配置然后想想如果 Provider 或 Consumer 运行时读取的是 Nacos 上的配置意味着改动配置后不需要重新部署服务。这一点对生产环境非常重要。传统方式改配置需要重新打包、重启服务引入配置中心后改配置和发配置通知几乎是实时的。Dubbo 的注册中心地址本身也可以放在 Nacos 配置里进一步减少配置文件里的环境差异。5. 常见问题与排查心得5.1 服务一直注册不上优先查端口和网络我这个案例里最常碰到的问题就是 Provider 启动时没有任何报错但 Nacos 控制台服务列表是空的。这种问题通常不是代码逻辑问题而网络和端口的问题。排查顺序我固定为以下几步确认 Provider 配置的nacos://127.0.0.1:8848能通telnet 127.0.0.1 8848确认 9848 端口也通。Nacos 2.x 的 gRPC 客户端一定会连这个端口只检查 8848 是不全面的。看 Provider 日志里有没有nacos相关报错比如连接超时、注册失败。检查同一个 Dubbo 协议端口是不是被其他进程占用。本地起多个 Provider 时如果都用了 20880后面的进程会直接报端口绑定失败。还有一个小经验本地调试时如果 Nacos 地址写了机器主机名而 hosts 里没配好映射也会注册不上。统一用127.0.0.1最省事。5.2 调用时报 No provider available先从包名和注解查起No provider available是 Dubbo 调用失败时最典型、也最让人头疼的报错。它表示 Consumer 在注册中心没有找到可用的服务提供者。可能的原因我按出现频率排序接口包名不一致。Consumer 引用的 api 模块和 Provider 实现的接口包名不同注册到 Nacos 的服务名虽然一样但 dubbo 内部做服务匹配时要求接口全限定名一致。DubboService没被扫描到。dubbo.scan.base-packages配置的包路径写错或者实现类上用的是 Spring 的Service而不是 Dubbo 的注解。Provider 启动后很快就退出。比如启动时发生异常或者进程被本地 IDE 中断Nacos 上的实例会在一段时间后被移除。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. 实际体会与可扩展的方向整套 DubboNacos 最小案例跑下来我最大的感受是Dubbo 对新手最友好的地方在于它把复杂的远程通信细节全部封装掉了。从代码层面看你写的就是一个接口、一个实现、一个注解注入但背后涉及的服务注册、心跳续约、地址推送、连接管理、负载均衡每一个环节都是可以拿出来单独深入研究的。如果你想把这套案例往生产方向推进我建议按这个顺序继续扩展给 Provider 做优雅上下线让服务在下线前先把实例标记为不健康等请求排空后再退出。引入 Nacos 配置中心把线程池参数、开关类配置放到 Nacos 上动态刷新而不是每次改代码重启。在 Consumer 侧接入限流和熔断组件避免单个 Provider 故障导致全链路雪崩。给服务调用加上链路追踪这样多级调用时能一眼看清每个环节的耗时。最后分享一个我自己坚持的习惯像这种框架集成案例第一遍一定要亲手从空工程开始搭建不要直接复制现成代码。只有自己一个个依赖引进去、一行行配置写上去遇到那几个典型的坑并亲自解决你才会真正理解注册中心和 RPC 之间的关系。跑通之后再考虑抽象和封装就会顺手很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑