一、开篇:踩过Dubbo+Nacos的坑才懂的道理

做分布式开发的人,基本都绕不开Dubbo和Nacos这俩组合——一个管服务调用,一个管服务注册发现,俩搭起来就像给项目装了“服务通讯录”:各个服务在Nacos上留好自己的“电话”(地址、端口),谁要调用谁,先去Nacos查号码,再拨号就行。但我前阵子在一个电商项目里踩了个大坑:明明服务都在Nacos上显示正常,调用时却疯狂抛“找不到服务”的错,查了三天才搞定。这篇就把这次踩坑的全流程拆解清楚,帮你少走弯路。

二、核心技术铺垫(用大白话讲)

在讲坑之前,得先把俩工具的核心逻辑掰明白,不然排查时会像没拿钥匙找门。

2.1 Dubbo到底干啥的?

简单说,Dubbo是“远程方法调用框架”:你在A服务里写了个getOrderList()的方法,B服务想调用,不用自己写HTTP请求、处理序列化反序列化,直接像调用本地方法一样写orderService.getOrderList()就行,剩下的网络传输、服务寻址全由Dubbo搞定。

2.2 Nacos和Dubbo怎么配合?

Nacos的核心是“服务注册中心”,Dubbo默认支持用Nacos做注册中心,流程是:

  1. 服务A启动时,主动把自己的服务名、IP、端口、版本号这些信息注册到Nacos;
  2. 服务B启动时,先去Nacos查服务A的信息,拿到之后存在本地;
  3. 服务B调用服务A时,直接用本地存的地址发起调用,同时Dubbo会定时同步Nacos上的服务状态。

三、踩坑场景还原(电商项目真实案例)

我负责的电商项目里有两个核心服务:

  • 订单服务(order-service):对外提供“获取订单列表”的接口,服务名是order-service
  • 商品服务(goods-service):需要调用订单服务的接口,来给商品页展示“用户已购订单”。

项目上线前一切正常,上线后商品服务每隔10分钟就会抛一次org.apache.dubbo.rpc.RpcException: No provider available for the service com.example.order.OrderService from the registry nacos://xxx:8848的错,Nacos控制台里订单服务明明显示“健康”,重启服务后能正常调用10分钟,之后又报错。

四、排查过程(一步步拆)

4.1 第一步:排除Nacos本身的问题

首先怀疑Nacos是不是出问题了,比如服务注册失败、健康检查逻辑有问题。

4.1.1 验证Nacos健康检查

先去Nacos控制台看订单服务的详情,发现健康检查状态是“健康”,然后用Nacos的API查服务详情:

# 调用Nacos API查询order-service的所有实例
curl "http://xxx:8848/nacos/v1/ns/instance/list?serviceName=order-service"

返回的结果里,订单服务的IP、端口都正常,没有被剔除的情况。

4.1.2 排除Nacos配置问题

检查Nacos的配置中心,有没有对order-service做过错误的配置,比如限流、熔断,结果发现配置都是默认的,没有问题。

4.2 第二步:排除Dubbo的基础配置问题

接下来怀疑Dubbo的配置,比如服务名写错、注册中心地址错、版本号不匹配。

4.2.1 检查服务名和注册中心地址

看两个服务的Dubbo配置:

# order-service的Dubbo配置(application.yml)
dubbo:
  registry:
    address: nacos://xxx:8848 # 注册中心地址
  application:
    name: order-service # 服务名
  protocol:
    name: dubbo
    port: 20880 # Dubbo协议端口

# goods-service的Dubbo配置(application.yml)
dubbo:
  registry:
    address: nacos://xxx:8848
  application:
    name: goods-service
  protocol:
    name: dubbo
    port: 20881

配置完全一致,没有问题。

4.2.2 检查版本号和分组

Dubbo的服务调用会匹配“服务名+版本号+分组”,如果调用方和提供方的版本号不一致,就会找不到服务。检查两个服务的接口定义:

// order-service里的接口定义(服务提供方)
@DubboService(version = "1.0.0")
public class OrderServiceImpl implements OrderService {
    @Override
    public List<Order> getOrderList(Long userId) {
        // 业务逻辑:从数据库查订单
        return orderMapper.selectByUserId(userId);
    }
}

// goods-service里的接口引用(服务调用方)
@DubboReference(version = "1.0.0")
private OrderService orderService;

版本号都是1.0.0,分组也都没配置(默认分组),没有问题。

4.3 第三步:定位到Nacos服务订阅的刷新逻辑

这时候我突然想到:Dubbo会定时去Nacos刷新服务实例列表,那会不会是刷新的间隔出问题了?

4.3.1 检查Dubbo的订阅刷新配置

查Dubbo的官方文档,发现有个配置叫dubbo.registry.subscribe.refresh.interval,意思是“订阅服务实例的刷新间隔”,默认值是600000毫秒(也就是10分钟)。

再看两个服务的配置,都没有配置这个参数,所以用的是默认值10分钟。那问题会不会出在Nacos的服务实例TTL(存活时间)上?

4.3.2 检查Nacos的服务实例TTL

Nacos的服务实例有个TTL配置,意思是“实例多久没上报心跳就会被标记为不健康”,默认值是15000毫秒(15秒)。那流程就通了:

  1. 订单服务启动后,每15秒给Nacos上报一次心跳,Nacos记录最后一次心跳时间;
  2. 商品服务每10分钟去Nacos刷新一次服务实例列表;
  3. 假设订单服务在第9分59秒的时候,突然因为网络波动没上报心跳,Nacos在第10分14秒的时候把它标记为不健康;
  4. 商品服务在第10分钟的时候,去Nacos刷新实例列表,拿到的是“不健康”的订单服务实例,所以就找不到服务了。

为了验证这个猜想,我去Nacos的日志里查,发现每次报错前15秒,都有订单服务的心跳超时记录。

五、修复方案(两个可选)

5.1 方案一:调整Dubbo的刷新间隔(推荐)

把Dubbo的订阅刷新间隔改小,比如改成30秒,这样商品服务每30秒就会去Nacos刷新一次实例列表,即使Nacos把订单服务标记为不健康,商品服务也会在30秒内拿到最新的状态。

修改后的配置:

# goods-service的Dubbo配置(application.yml)
dubbo:
  registry:
    address: nacos://xxx:8848
    subscribe:
      refresh:
        interval: 30000 # 刷新间隔改成30秒
  application:
    name: goods-service
  protocol:
    name: dubbo
    port: 20881

5.2 方案二:调整Nacos的TTL配置

把Nacos的TTL改大,比如改成120秒,这样订单服务没上报心跳的时间要超过120秒才会被标记为不健康,而商品服务的刷新间隔是10分钟,只要订单服务在10分钟内有一次心跳,就不会被标记为不健康。

修改Nacos的TTL配置(在Nacos的配置中心里修改nacos.core.instance.ttl):

nacos.core.instance.ttl=120000 # TTL改成120秒

六、方案对比与注意事项

6.1 方案对比

方案 优点 缺点 适用场景
调整Dubbo刷新间隔 不影响Nacos全局配置,灵活 刷新间隔太小会增加Nacos的压力 大多数场景,尤其是服务数量多的情况
调整Nacos TTL 减少Nacos的心跳检查压力 会影响所有服务的健康检查逻辑 服务数量少,且网络波动频繁的场景

6.2 注意事项

  1. 配置刷新间隔时,不能太小,比如改成1秒,会导致Nacos被大量的刷新请求打垮;
  2. 配置TTL时,不能太大,比如改成1小时,会导致不健康的服务长时间留在Nacos上,影响调用;
  3. 除了刷新间隔和TTL,还要注意Dubbo的服务降级配置,当找不到服务时,不要直接抛错,而是返回默认值,提升用户体验。

七、总结

这次踩坑的核心原因是“Dubbo的订阅刷新间隔”和“Nacos的服务实例TTL”不匹配,导致商品服务拿到的是过时的服务状态。排查分布式问题时,不能只看表面的错误信息,还要深入了解各个组件之间的配合逻辑,一步步拆解,才能找到问题的根源。