Skip to content

服务暴露与服务发现

1. 这是什么

在 Dubbo 里,“服务暴露”和“服务发现”是两件方向相反、但彼此配套的事。

你可以先用最简单的人话理解:

  • 服务暴露:Provider 告诉外界“我这里有这个服务,别人可以调我”
  • 服务发现:Consumer 想办法知道“这个服务现在有哪些机器可用,我该连谁”

如果只记一句话:

暴露是服务提供方把能力放出来,发现是服务消费方把地址找出来。

很多人刚学 Dubbo 时,会把它们混成一句话,结果后面一遇到“服务启动了但调不通”就不知道该从哪查。

其实排查时要先分清:

  • 是 Provider 没暴露成功?
  • 还是暴露成功了,但没注册上去?
  • 还是注册上去了,但 Consumer 没发现到?
  • 还是发现到了,但地址缓存、路由、网络有问题?

这篇就是把这一条链路拆开讲明白。


2. 为什么重要

Dubbo 整个服务体系能动态运转,靠的就是这套机制。

如果没有服务暴露与发现,会发生什么?

  • 每个 Consumer 都得手工写死 Provider 地址
  • Provider 扩容缩容时,Consumer 需要逐个改配置
  • 服务实例挂掉后,Consumer 可能还在调死地址
  • 多环境、多版本、多分组管理会非常痛苦

所以服务化的核心价值之一,就是:

  • Provider 能自动上报自己
  • Consumer 能自动感知可用实例变化

这也是为什么注册中心在微服务里这么重要。


3. 先建立整体直觉

你可以把 Dubbo + 注册中心理解成一个“共享通讯录系统”。

Provider 在做什么

Provider 像是在通讯录里登记:

  • 我是谁
  • 我提供什么服务
  • 我监听什么地址和端口
  • 我属于什么分组/版本

Consumer 在做什么

Consumer 像是在通讯录里查:

  • UserService 现在有哪些可用实例
  • 哪些地址可连
  • 哪个版本符合要求

注册中心在做什么

注册中心像是:

  • 维护服务名到实例地址的映射
  • 监听实例上下线
  • 在变化时通知订阅者

所以一句话:

注册中心更像动态通讯录,不像流量中转站。


4. 服务暴露到底发生了什么

服务暴露不是“代码里加了个注解”就结束了。

在 Provider 启动时,通常会发生下面这些事:

  1. 扫描到要暴露的服务实现
  2. 读取服务配置(接口、版本、分组、协议、端口等)
  3. 在本地启动网络监听
  4. 生成服务元数据
  5. 向注册中心注册服务地址

你在代码层面看到的可能只是:

java
@DubboService
public class UserServiceImpl implements UserService {
    @Override
    public UserDTO getById(Long userId) {
        UserDTO user = new UserDTO();
        user.setId(userId);
        user.setName("Alice");
        return user;
    }
}

但在框架层面,Dubbo 还要替你完成:

  • 服务导出(export)
  • 协议绑定
  • 端口监听
  • 地址注册

所以“暴露服务”不只是“声明一个 Bean”,而是让远程世界能访问到这个能力


5. 服务发现到底发生了什么

Consumer 启动后,也不是第一次调用时才临时去找地址。

通常流程是:

  1. Consumer 根据接口信息订阅目标服务
  2. 注册中心返回当前可用 Provider 地址列表
  3. Consumer 在本地维护一份地址缓存
  4. 后续地址有变化时,注册中心再推送变更通知
  5. Consumer 更新本地地址列表

这样做的好处是:

  • 每次调用不需要都去查注册中心
  • 调用时可以直接从本地地址列表挑可用 Provider
  • 地址变化时又能及时更新

这就是为什么:

注册中心主要参与“发现”和“变更通知”,而不是每次调用的中转。


6. 一次完整链路,用顺序串起来

下面这条链路最好反复记:

text
Provider 启动

暴露本地服务

向注册中心注册地址和元数据

Consumer 启动

向注册中心订阅目标服务

拿到 Provider 地址列表并缓存在本地

业务代码发起调用

Consumer 从本地地址列表里挑一个 Provider

发起远程调用

Provider 返回结果

这条链路里有两个特别容易忽略的点:

  1. 注册中心主要管地址信息
  2. Consumer 通常会有本地缓存

这两个点一旦没建立起来,很多线上问题都会看不清。


7. 最小示例:暴露一个服务、引用一个服务

7.1 定义接口

java
public interface UserService {
    UserDTO getById(Long userId);
}

7.2 Provider 暴露服务

java
@DubboService(version = "1.0.0", group = "user")
public class UserServiceImpl implements UserService {
    @Override
    public UserDTO getById(Long userId) {
        UserDTO user = new UserDTO();
        user.setId(userId);
        user.setName("Alice");
        return user;
    }
}

7.3 Consumer 发现并引用服务

java
@Service
public class OrderAppService {

    @DubboReference(version = "1.0.0", group = "user")
    private UserService userService;

    public String createOrder(Long userId) {
        UserDTO user = userService.getById(userId);
        return "create order for " + user.getName();
    }
}

从业务代码看:

  • Consumer 只是注入了一个接口

但背后已经依赖:

  • Provider 成功暴露
  • Provider 成功注册
  • Consumer 成功订阅并发现服务

否则这段调用根本跑不通。


8. 动手验证:怎么判断暴露和发现是否正常

如果你本地有 Dubbo 项目,可以按下面顺序验证。

8.1 先启动注册中心

如果你用 Nacos:

bash
docker run --name nacos-standalone -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:v2.3.2

如果你用 ZooKeeper:

bash
docker run --name zk -p 2181:2181 -d zookeeper:3.9

8.2 启动 Provider,观察日志

你重点看这些信息:

  • Dubbo 服务有没有 export 成功
  • 监听端口有没有起来
  • 注册中心连接是否成功
  • 服务地址有没有注册成功

8.3 去注册中心确认是否真注册成功

如果是 Nacos,可以登录控制台看服务列表。

如果是 ZooKeeper,可以看对应服务节点是否出现。

8.4 再启动 Consumer

重点看:

  • Consumer 是否成功订阅服务
  • 是否拿到实例地址列表
  • 是否存在版本/分组不匹配

8.5 最后发起一次调用

观察:

  • 调用是否成功
  • 是否命中正确版本/分组
  • Provider 是否收到了请求

这一步的关键不是“跑通一次”本身,而是你要知道:

  • 暴露成功看哪里
  • 注册成功看哪里
  • 发现成功看哪里

9. 注册中心到底存了什么

注册中心一般会存这些信息:

  • 服务名
  • Provider 地址
  • 协议
  • 端口
  • 分组
  • 版本
  • 权重
  • 其他元数据

例如一个服务可能不是只有一个实例,而是:

text
UserService -> 10.0.0.11:20880
UserService -> 10.0.0.12:20880
UserService -> 10.0.0.13:20880

Consumer 拿到这些地址后,后续调用时就可以:

  • 做负载均衡
  • 做故障转移
  • 做路由过滤

所以注册中心不只是“记一个 IP”,而是服务治理的基础信息源。


10. 为什么 Consumer 需要订阅地址变更

因为服务实例不是静态不变的。

真实环境里经常会发生:

  • 服务扩容
  • 服务缩容
  • 实例重启
  • 节点故障
  • 灰度发布
  • 版本切换

如果 Consumer 不能及时感知这些变化,会出现:

  • 还在调已经下线的地址
  • 新实例加进来却一直没流量
  • 灰度版本无法正确路由

所以服务发现不是“查一次地址就完了”,而是:

  • 持续订阅
  • 动态更新

11. 本地缓存为什么重要

Consumer 通常不会每次调用都去问注册中心:“现在有哪些 Provider?”

它会在本地维护地址缓存。

这样做有两个好处:

  1. 调用路径更短,性能更好
  2. 注册中心短暂抖动时,Consumer 仍能继续调用已知实例

但本地缓存也会带来一个理解点:

  • 如果地址变更通知没及时同步
  • Consumer 可能会短时间继续使用旧地址

所以排查问题时,不能只看注册中心“现在是什么”,还要考虑:

  • Consumer 本地缓存是否已更新

12. 最容易踩的坑

12.1 服务启动了,但其实没暴露成功

很多人看到应用启动成功,就以为服务一定可调。

但实际上可能:

  • Dubbo 注解没生效
  • 端口没监听起来
  • 服务没 export 成功

12.2 服务暴露了,但没注册成功

常见原因:

  • 注册中心地址配置错
  • 注册中心不可达
  • 鉴权失败
  • 环境隔离错误

这类问题的表现是:

  • Provider 自己看起来没报大错
  • 但 Consumer 根本发现不到它

12.3 Consumer 引用了服务,但分组/版本不匹配

比如 Provider 是:

  • group = user
  • version = 1.0.0

而 Consumer 配成了:

  • group = default
  • version = 2.0.0

那即使注册中心里有地址,也可能发现不到符合条件的实例。

12.4 以为注册中心挂了,调用就一定全挂

不一定。

如果 Consumer 已经拿到并缓存了可用地址,短时间内可能还能继续调。

但:

  • 无法感知新变化
  • 长期看还是有风险

12.5 把注册中心当流量代理

这个误解会导致你对:

  • 性能瓶颈
  • 故障定位
  • 网络拓扑

全都判断错。


13. 一套实用排查顺序

以后遇到“Dubbo 服务发现异常”,建议按这个顺序排:

  1. Provider 服务有没有成功 export
  2. Provider 有没有成功注册到注册中心
  3. 注册中心里有没有这条服务记录
  4. Consumer 有没有成功订阅该服务
  5. Consumer 配置的 group/version/interface 是否匹配
  6. Consumer 本地地址列表是否已更新
  7. 网络是否能直连到 Provider

这个顺序非常实用,比一上来就怀疑业务代码更有效。


14. 练习建议

练习 1:画流程图

把“服务暴露”和“服务发现”画成两条箭头方向相反的流程线。

练习 2:模拟服务下线

步骤:

  1. 启动注册中心
  2. 启动 Provider
  3. 启动 Consumer
  4. 调用一次成功
  5. 停掉 Provider
  6. 观察 Consumer 的后续表现

重点思考:

  • 地址变化是怎么传播的
  • Consumer 本地缓存会发生什么

练习 3:故意配错版本号

让 Provider 和 Consumer 的 version 不一致,然后观察:

  • 注册中心里明明有服务
  • 为什么 Consumer 还是调不到

这样你会更快理解“发现不到”和“根本没有服务”并不是一回事。


15. 自测问题

  • 服务暴露和服务发现分别在做什么?
  • 为什么注册中心不是每次请求的中转站?
  • Consumer 为什么通常需要本地地址缓存?
  • 为什么服务实例变化后要及时通知 Consumer?
  • 服务注册成功但仍然调不到时,应该优先检查哪些配置项?

16. 这一章你至少要带走什么

如果你看完只记住 4 件事,就记这 4 件:

  1. 服务暴露是 Provider 把能力和地址放出来
  2. 服务发现是 Consumer 拿到并维护可用实例列表
  3. 注册中心负责地址与元数据管理,不负责每次请求中转
  4. 排查时一定要分清:暴露、注册、发现、调用 这四个阶段到底卡在哪一层

把这条链路看清,后面学负载均衡、容错、治理配置时,你就知道它们分别插在什么位置了。