服务暴露与服务发现
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 启动时,通常会发生下面这些事:
- 扫描到要暴露的服务实现
- 读取服务配置(接口、版本、分组、协议、端口等)
- 在本地启动网络监听
- 生成服务元数据
- 向注册中心注册服务地址
你在代码层面看到的可能只是:
@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 启动后,也不是第一次调用时才临时去找地址。
通常流程是:
- Consumer 根据接口信息订阅目标服务
- 注册中心返回当前可用 Provider 地址列表
- Consumer 在本地维护一份地址缓存
- 后续地址有变化时,注册中心再推送变更通知
- Consumer 更新本地地址列表
这样做的好处是:
- 每次调用不需要都去查注册中心
- 调用时可以直接从本地地址列表挑可用 Provider
- 地址变化时又能及时更新
这就是为什么:
注册中心主要参与“发现”和“变更通知”,而不是每次调用的中转。
6. 一次完整链路,用顺序串起来
下面这条链路最好反复记:
Provider 启动
↓
暴露本地服务
↓
向注册中心注册地址和元数据
↓
Consumer 启动
↓
向注册中心订阅目标服务
↓
拿到 Provider 地址列表并缓存在本地
↓
业务代码发起调用
↓
Consumer 从本地地址列表里挑一个 Provider
↓
发起远程调用
↓
Provider 返回结果这条链路里有两个特别容易忽略的点:
- 注册中心主要管地址信息
- Consumer 通常会有本地缓存
这两个点一旦没建立起来,很多线上问题都会看不清。
7. 最小示例:暴露一个服务、引用一个服务
7.1 定义接口
public interface UserService {
UserDTO getById(Long userId);
}7.2 Provider 暴露服务
@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 发现并引用服务
@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:
docker run --name nacos-standalone -e MODE=standalone -p 8848:8848 -d nacos/nacos-server:v2.3.2如果你用 ZooKeeper:
docker run --name zk -p 2181:2181 -d zookeeper:3.98.2 启动 Provider,观察日志
你重点看这些信息:
- Dubbo 服务有没有 export 成功
- 监听端口有没有起来
- 注册中心连接是否成功
- 服务地址有没有注册成功
8.3 去注册中心确认是否真注册成功
如果是 Nacos,可以登录控制台看服务列表。
如果是 ZooKeeper,可以看对应服务节点是否出现。
8.4 再启动 Consumer
重点看:
- Consumer 是否成功订阅服务
- 是否拿到实例地址列表
- 是否存在版本/分组不匹配
8.5 最后发起一次调用
观察:
- 调用是否成功
- 是否命中正确版本/分组
- Provider 是否收到了请求
这一步的关键不是“跑通一次”本身,而是你要知道:
- 暴露成功看哪里
- 注册成功看哪里
- 发现成功看哪里
9. 注册中心到底存了什么
注册中心一般会存这些信息:
- 服务名
- Provider 地址
- 协议
- 端口
- 分组
- 版本
- 权重
- 其他元数据
例如一个服务可能不是只有一个实例,而是:
UserService -> 10.0.0.11:20880
UserService -> 10.0.0.12:20880
UserService -> 10.0.0.13:20880Consumer 拿到这些地址后,后续调用时就可以:
- 做负载均衡
- 做故障转移
- 做路由过滤
所以注册中心不只是“记一个 IP”,而是服务治理的基础信息源。
10. 为什么 Consumer 需要订阅地址变更
因为服务实例不是静态不变的。
真实环境里经常会发生:
- 服务扩容
- 服务缩容
- 实例重启
- 节点故障
- 灰度发布
- 版本切换
如果 Consumer 不能及时感知这些变化,会出现:
- 还在调已经下线的地址
- 新实例加进来却一直没流量
- 灰度版本无法正确路由
所以服务发现不是“查一次地址就完了”,而是:
- 持续订阅
- 动态更新
11. 本地缓存为什么重要
Consumer 通常不会每次调用都去问注册中心:“现在有哪些 Provider?”
它会在本地维护地址缓存。
这样做有两个好处:
- 调用路径更短,性能更好
- 注册中心短暂抖动时,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 服务发现异常”,建议按这个顺序排:
- Provider 服务有没有成功 export
- Provider 有没有成功注册到注册中心
- 注册中心里有没有这条服务记录
- Consumer 有没有成功订阅该服务
- Consumer 配置的 group/version/interface 是否匹配
- Consumer 本地地址列表是否已更新
- 网络是否能直连到 Provider
这个顺序非常实用,比一上来就怀疑业务代码更有效。
14. 练习建议
练习 1:画流程图
把“服务暴露”和“服务发现”画成两条箭头方向相反的流程线。
练习 2:模拟服务下线
步骤:
- 启动注册中心
- 启动 Provider
- 启动 Consumer
- 调用一次成功
- 停掉 Provider
- 观察 Consumer 的后续表现
重点思考:
- 地址变化是怎么传播的
- Consumer 本地缓存会发生什么
练习 3:故意配错版本号
让 Provider 和 Consumer 的 version 不一致,然后观察:
- 注册中心里明明有服务
- 为什么 Consumer 还是调不到
这样你会更快理解“发现不到”和“根本没有服务”并不是一回事。
15. 自测问题
- 服务暴露和服务发现分别在做什么?
- 为什么注册中心不是每次请求的中转站?
- Consumer 为什么通常需要本地地址缓存?
- 为什么服务实例变化后要及时通知 Consumer?
- 服务注册成功但仍然调不到时,应该优先检查哪些配置项?
16. 这一章你至少要带走什么
如果你看完只记住 4 件事,就记这 4 件:
- 服务暴露是 Provider 把能力和地址放出来
- 服务发现是 Consumer 拿到并维护可用实例列表
- 注册中心负责地址与元数据管理,不负责每次请求中转
- 排查时一定要分清:暴露、注册、发现、调用 这四个阶段到底卡在哪一层
把这条链路看清,后面学负载均衡、容错、治理配置时,你就知道它们分别插在什么位置了。