Skip to content

基础架构与调用链路

1. 这是什么

如果只用一句话解释 Dubbo,可以这样记:

  • Dubbo 是一个帮助 Java 服务之间做 RPC 调用的框架
  • 它想解决的问题是:让你调远程服务时,尽量像在调本地方法

很多人第一次学 Dubbo,会记住这些词:

  • Provider
  • Consumer
  • Registry
  • Protocol
  • Proxy
  • LoadBalance

但背完一圈还是容易迷糊:

  • 为什么说 Dubbo 是“像本地调用一样”的远程调用?
  • 注册中心到底是不是流量中转?
  • 一次请求到底经过了哪些环节?
  • 出问题时应该先看哪里?

这篇就先把这张“总图”搭起来。


2. 为什么重要

Dubbo 后面的很多知识点,其实都挂在这条主链路上:

  • 服务暴露
  • 注册发现
  • 负载均衡
  • 集群容错
  • 序列化
  • 通信协议
  • 配置治理

如果你一开始没把整体架构看清,后面学每个模块都会像散点知识。

所以这一章的目标不是死记组件名,而是先回答两个关键问题:

  1. Dubbo 的角色分工是什么?
  2. 一次 RPC 调用从发起到返回,实际走了什么链路?

3. 先用人话理解 Dubbo

假设有两个服务:

  • 订单服务
  • 用户服务

订单服务里要查用户昵称,代码可能写成:

java
UserDTO user = userService.getById(1001L);

表面看起来像普通 Java 方法调用。

但实际上:

  • userService 可能根本不在本进程里
  • 它可能在另一台机器
  • 调用中间还会经过服务发现、负载均衡、网络传输、序列化、反序列化

也就是说:

Dubbo 做的事情,是把一整套远程调用细节包起来,让业务代码用起来更像本地接口。

注意:

  • “像本地调用”不等于“真的就是本地调用”
  • 它只是调用体验像
  • 本质仍然是网络请求

这一点非常重要,因为很多误用都来自忘了这件事。


4. Dubbo 基础架构的几个核心角色

4.1 Provider:服务提供者

Provider 就是:

  • 真实提供业务能力的服务端
  • 真正实现接口逻辑的一方

例如:

  • 用户服务提供 getUserById
  • 库存服务提供 deductStock

它负责:

  • 暴露服务
  • 接收请求
  • 执行业务逻辑
  • 返回结果

4.2 Consumer:服务消费者

Consumer 就是:

  • 调用别人服务的一方

例如:

  • 订单服务调用用户服务
  • 支付服务调用账户服务

它在业务代码里通常看到的是一个接口代理对象,而不是自己手写 HTTP 请求。

4.3 Registry:注册中心

注册中心最容易被误解。

它的核心职责是:

  • 帮 Consumer 找到可用 Provider 地址
  • 帮 Provider 上报自己的服务信息
  • 在地址变化时通知 Consumer

不是

  • 请求转发中转站
  • 每次调用都要经过的代理网关

一句话记忆:

注册中心负责“找人”,不是“代你打电话”。

常见注册中心:

  • ZooKeeper
  • Nacos
  • Redis(某些轻量方案)

4.4 Monitor / Admin / ConfigCenter

这些属于治理和辅助能力,主要负责:

  • 监控调用情况
  • 管理路由规则
  • 下发配置
  • 观察服务健康状态

入门阶段先知道:

  • 它们很重要
  • 但不是一条最小调用链必经的核心路径

5. 一次 RPC 调用到底怎么走

先看最简版流程:

text
Consumer 启动

向注册中心订阅某个服务地址

拿到 Provider 列表并缓存在本地

业务代码调用代理对象方法

Dubbo 选择一个 Provider

把请求做序列化并通过网络发出去

Provider 接收请求,反序列化

执行本地业务方法

结果再序列化返回给 Consumer

Consumer 反序列化后得到结果

这里最关键的是要看清:

  • 注册中心主要在服务发现阶段起作用
  • 真正调用时,通常是 Consumer 直接连 Provider

所以 Dubbo 更像:

  • 注册中心先给你通讯录
  • 真打电话时你自己直拨

而不是:

  • 每次都让注册中心帮你转接整通电话

6. 为什么远程调用能“看起来像本地调用”

核心原因是:代理对象

在 Consumer 侧,你注入的往往不是远程服务本体,而是一个代理。

例如:

java
@DubboReference
private UserService userService;

你在代码里写:

java
UserDTO user = userService.getById(1001L);

看起来像直接调接口实现。

但实际上背后做了这些事:

  1. 代理拦截方法调用
  2. 组装请求对象
  3. 查可用 Provider 地址
  4. 做负载均衡
  5. 发网络请求
  6. 等待响应
  7. 解析返回结果

所以你看到的是“本地调用语法”,实际发生的是“远程调用流程”。


7. 一个最小 Dubbo 场景

假设你有两个服务:

  • user-service:提供用户查询
  • order-service:下单时查用户信息

7.1 Provider 定义接口

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

7.2 Provider 实现接口并暴露

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;
    }
}

7.3 Consumer 引用服务

java
@Service
public class OrderAppService {

    @DubboReference
    private UserService userService;

    public String createOrder(Long userId) {
        UserDTO user = userService.getById(userId);
        return "为用户 " + user.getName() + " 创建订单成功";
    }
}

从业务代码角度看,像是:

  • 直接调用了一个 Java 接口

但真实链路其实包含:

  • 服务发现
  • 代理
  • 编码
  • 网络 I/O
  • 解码
  • 执行
  • 返回

8. 动手验证:跑一个最小调用链

如果你的项目里已经接了 Spring Boot + Dubbo,可以按下面验证。

8.1 启动注册中心

例如本地有 Nacos:

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

如果你用 ZooKeeper,也可以先启动 ZooKeeper。

8.2 启动 Provider

启动后重点观察日志:

  • 服务是否成功暴露
  • 是否成功注册到注册中心

8.3 启动 Consumer

启动后重点观察:

  • 是否成功订阅服务
  • 是否拿到 Provider 地址

8.4 发起一次调用

例如通过接口或单元测试触发:

java
System.out.println(orderAppService.createOrder(1001L));

你要确认三件事:

  1. Consumer 能正常发起调用
  2. Provider 收到请求并执行
  3. Consumer 成功拿到响应

这一步的意义是:

  • 把“抽象架构图”变成“可观察运行链路”

9. 一次调用里常见的关键环节

理解架构时,建议把下面几个关键环节串起来:

9.1 服务暴露

Provider 启动后,需要把自己的接口能力暴露出去。

9.2 服务注册

Provider 把自己的地址、协议、版本等信息注册到注册中心。

9.3 服务发现

Consumer 订阅目标服务,并拿到可用实例列表。

9.4 代理调用

业务代码调的是代理对象,不是远程对象本体。

9.5 负载均衡

当 Provider 有多个实例时,Consumer 会挑一个去调用。

9.6 序列化与网络传输

请求对象要变成字节流发出去,响应也要再转回来。

9.7 结果返回

Provider 执行业务逻辑,把结果回给 Consumer。

把这 7 个词串起来,你对 Dubbo 的整体理解就已经搭起来了。


10. Dubbo 和普通 HTTP 调用有什么差异

很多人会问:

  • 那我直接 HTTP 调不就行了?

可以,但两者关注点不一样。

HTTP 方式常见特点

  • 协议通用
  • 调试方便
  • 前后端都能接
  • 生态非常广

Dubbo 方式常见特点

  • 更偏 Java 服务间调用
  • 更强调服务治理能力
  • 对接口、注册发现、路由、集群容错支持更强
  • 通常更适合内部服务调用场景

你可以先这样粗略理解:

  • 对外开放接口:很多时候更偏 HTTP / REST
  • 内部 Java 微服务高频调用:Dubbo 常更合适

当然,真实架构里常常是两者并存。


11. 最容易踩的坑

11.1 把远程调用当成本地调用

这是最大误区。

表面语法虽然像本地方法,但它依然会遇到:

  • 网络超时
  • 重试
  • 服务不可用
  • 序列化失败
  • 版本不兼容

所以你必须带着“这是远程调用”的意识写代码。

11.2 误以为注册中心是流量中转

这会导致你对调用链路的理解完全偏掉。

要记住:

  • 注册中心主要负责地址发现
  • 调用时通常是直连 Provider

11.3 忽略接口和数据模型兼容性

如果 Provider 和 Consumer 的接口版本、字段结构不一致,就容易出现:

  • 反序列化失败
  • 字段缺失
  • 调用异常

11.4 只关心代码,不看启动日志

Dubbo 很多问题,启动阶段日志就已经给答案了,比如:

  • 服务有没有暴露成功
  • 有没有注册成功
  • Consumer 有没有订阅到地址

12. 一套排查总思路

以后遇到“Dubbo 调不通”,可以先按下面顺序排:

  1. Provider 是否成功启动
  2. 服务是否成功暴露
  3. 是否成功注册到注册中心
  4. Consumer 是否订阅到了服务地址
  5. 网络是否连通
  6. 序列化/协议是否匹配
  7. 超时、重试、负载均衡配置是否合理

这个顺序比一上来就盯业务代码更高效。


13. 练习建议

练习 1:画架构图

至少画出这几个角色:

  • Consumer
  • Provider
  • Registry
  • 代理层
  • 网络传输层

练习 2:复述一次调用链

要求你自己不用看资料,完整说出:

  • Consumer 怎么找到 Provider
  • 代理怎么拦截调用
  • 请求怎么发出去
  • 结果怎么返回

练习 3:故意制造一次失败

比如:

  • 停掉 Provider
  • 或故意配错注册中心地址

然后观察:

  • Consumer 启动日志怎么报错
  • 调用时报什么异常

这一步对理解实际排障非常有帮助。


14. 自测问题

  • Dubbo 为什么能让远程调用“看起来像本地调用”?
  • Provider、Consumer、Registry 各自负责什么?
  • 注册中心为什么不是请求中转站?
  • 一次 RPC 调用通常会经过哪些关键环节?
  • 为什么说写 Dubbo 业务代码时,必须始终记得它本质是远程调用?

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

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

  1. Dubbo 本质上是在帮你管理服务间 RPC 调用
  2. “像本地调用”只是体验像,本质仍然是网络调用
  3. 注册中心负责发现地址,不负责中转每次请求
  4. 一次调用链路里至少要能看清:发现、代理、负载、传输、执行、返回

把这张总图建立起来,后面再学服务发现、容错、序列化、治理能力,就不会散了。