Skip to content

Dubbo 与 RPC 基础 ​

S4-018建议学高级约 90 分钟JDK 21企业开发进阶 · 微服务

1. 本节定位 ​

《Netty 与网络编程概览》讲网络编程;本节讲**"微服务的通信方式"**:

text
RPC 的全部秘密只有一句话:
把「方法调用」编码成「网络请求」,收到响应再解码成返回值  ✓

★ 本节的实测把"远程调用贵在哪"量出来 ✓✓:

text
  本地调用:   0 微秒/次(JIT 把空方法优化掉了 —— 见下方说明)
  RPC 调用: 828 微秒/次(0.828 ms)
  HTTP/JSON(每次新建连接):27511 微秒/次(27.511 ms)✗✗

2. 学习目标 ​

  • 能说清一次 RPC 调用的完整链路 ★★★;
  • 知道远程调用比本地贵在哪四件事上 ★★★;
  • 能对比四种序列化格式的体积与跨语言能力 ★★★;
  • 能说出 Dubbo 与 REST / gRPC 的取舍 ★★★;
  • 知道 Dubbo 客户端必须配的四个参数 ★★★;
  • 知道"你写的 RPC 只覆盖了通信部分,Dubbo 难的是服务治理" ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看本节的序列化实测 ✓✓:

text
  ① Java 原生序列化:149 字节  ← 带类名与方法签名,最臃肿 ✗✗
  ② JSON 文本:       58 字节(0.4 倍)
  ③ 手写二进制:      33 字节(0.2 倍)✓✓

★ 同一个用户对象,编码体积差 4.5 倍 ✓ ——在高并发下这就是带宽与 GC 的差距 ✓✓

5. 核心概念 ​

5.1 一次 RPC 的完整链路 ★★★ ​

★ 本节的 200 行实现就是这条链路 ✓:

text
客户端                                      服务端
  │
  ├─ ① 动态代理拦截方法调用(UserService.findName(id))
  ├─ ② 序列化:方法名 + 参数 → 字节流
  ├─ ③ 网络传输:长度字段 + 字节流(解决粘包)
  │  ───────────────── 网络 ─────────────────→
  │                                    ④ 反序列化 → 方法名 + 参数
  │                                    ⑤ 反射调用真实实现
  │                                    ⑥ 序列化返回值
  │  ←─────────────── 网络 ──────────────────
  ├─ ⑦ 反序列化响应
  └─ ⑧ 返回给调用方(看起来就像本地方法)

★ 关键设计 ✓✓:

环节用什么实现为什么
① 拦截调用 ★★★动态代理调用方只依赖接口(《设计模式:结构型》)✓✓
② 编解码 ★★自定义二进制协议比 JSON 小、比 Java 序列化跨语言 ✓
③ 传输 ★★长度字段解决粘包(《TCP 与 UDP》)✓✓
⑤ 服务端通用化 ★★★反射调用不用为每个方法写 if-else ✓✓

5.2 远程调用贵在哪四件事上 ★★★ ​

实测 ✓:

text
  本地调用:   0 微秒/次
  RPC 调用: 828 微秒/次(0.828 ms)

★★ "本地 0 微秒"要如实解释 ✓✓:

那是 JIT 把空方法调用优化掉了(呼应 《编程语言的发展与分类》 的"死代码消除")

真实业务方法的本地调用约 10~100 纳秒(比 0 微秒大,但仍是纳秒级)✓

所以正确的结论是:RPC 有约 0.8 ms 的固定成本(本机回环) ✓✓

★ 这 0.8 ms 花在四件事上 ✓✓:

环节优化手段
序列化 / 反序列化 ★★换更快的序列化(Protobuf / Hessian)✓
网络往返 ★★★连接复用(长连接) ✓✓
协议编解码 ★★定长头 + 紧凑编码 ✓
反射调用 ★缓存 Method 或生成字节码 ✓

★★ 注意这是"本机回环" ✓✓:

跨机房时,网络往返会占绝大部分(1~50 ms) ✗

所以 RPC 的第一优化永远是"减少调用次数与网络跳数" ✓✓

(呼应 《微服务架构与拆分原则》:一次业务跨 5 次服务,延迟就叠加 5 次)

5.3 序列化:四个格式的取舍 ★★★ ​

★ 实测与对照 ✓:

格式体积跨语言代表
Java 原生 ✗✗最大(149 字节)否 ✗JDK Serializable
JSON ✓中(58 字节)是 ✓✓Jackson / Fastjson
Protobuf ✓✓最小 ✓✓是 ✓✓gRPC
Hessian ✓小 ✓部分Dubbo 默认 ✓

★★ 两条实践提醒 ✓✓:

#提醒原因
①不要用 Java 原生序列化做对外协议 ✗✗体积大、跨语言差,而且反序列化本身就是高危漏洞入口(反序列化任意类 = 可能的 RCE)✓
②体积的意义在于"高并发下省带宽与 GC" ★★每次请求少 100 字节,10 万 QPS 就是每秒少 10 MB 流量 ✓✓

★ 一条选型口诀 ✓✓:

对外 / 跨语言 → JSON 或 Protobuf ✓

内部 Java 微服务、追求性能 → Hessian(Dubbo 默认)或 Protobuf ✓

5.4 三种远程调用的取舍 ★★★ ​

实测(用 HTTP 每次新建连接做对照)✓:

text
  HTTP/JSON(每次新建连接):27511 微秒/次(27.511 ms)✗✗
  (自定义二进制协议 + 长连接:见 5.2 的 0.828 ms)

★ 慢 33 倍 ✗✗ ——但要注意:差距的主因不是"HTTP 不好",而是"每次新建连接" ✓✓

★ 三种方式的取舍 ✓:

方式优势劣势适用
Dubbo(自定义协议) ★★性能高、治理能力强跨语言弱、侵入性强内部 Java 微服务 ✓
REST(HTTP+JSON) ★★★通用、易调试 ✓✓性能一般对外 API、跨语言 ✓✓
gRPC(HTTP/2 + PB) ★★性能好且跨语言 ✓✓调试不如 REST 方便多语言、流式场景 ✓

★ 一条重要提醒 ✓✓:

"HTTP 慢"这个说法要谨慎 ✓

如果 HTTP 也复用长连接,差距会大幅缩小 ✓

Dubbo 的真正优势是"为高频小请求设计的协议 + 内置服务治理" ✓✓

5.5 Dubbo 的核心概念 ★★ ​

★ 本实现与 Dubbo 的对应 ✓:

本类做的事Dubbo 里的对应
接口 + 动态代理@DubboReference / @DubboService ✓
手写字节协议Dubbo 协议(默认)+ 多种序列化 ✓
ServerSocket + 反射调用Netty(《Netty 与网络编程概览》)+ 线程池 + 过滤器链 ✓
手写 service 注册表注册中心(Nacos/ZK,《Nacos 注册与配置中心》)✓✓
没有负载均衡、集群容错、服务降级 ✓✓
没有超时、重试、限流、并发控制 ✓
没有服务治理控制台、链路追踪 ✓

★★ 一句话 ✓✓:

你写的 200 行只覆盖了 Dubbo 的"通信"部分 ✓

而 Dubbo 真正难的是那套"服务治理"(注册发现、路由、容错、治理台)✓✓

★ 四个必配的客户端参数(不配就会踩坑) ✓✓:

参数默认建议
timeout ★★★1000 ms按下游 P99 设 ——不配的话下游慢会拖死上游 ✗
retries ★★★2 次只对幂等接口开 ——否则会重复下单 ✗✗
cluster ★★failover读用 failover、写用 failsafe/failfast ✓
actives ★★0(不限)限制并发 ——防止单个接口把线程池打满 ✓

6. 递进示例 ​

6.1 最小示例:客户端只依赖接口 ​

java
// 客户端只认识这个接口(与 S4-004 的 DIP 一致)
UserService userService = RpcClient.create(UserService.class, "127.0.0.1", 20880);
String name = userService.findName(9527L);      // 看起来就是本地调用 ✓

6.2 业务示例:Dubbo 注解 ​

java
// 服务端
@DubboService(timeout = 2000, retries = 0)     // ★ 写接口 retries=0
public class UserServiceImpl implements UserService { ... }

// 消费端
@DubboReference(timeout = 1000, cluster = "failfast")
private UserService userService;

6.3 反例:不配超时的远程调用 ​

java
// ✗ 下游卡住 30 秒,上游线程被占住 30 秒 → 上游线程池被打满 → 雪崩
orderService.create(request);

★ 这是"雪崩"最常见的起点 ✗✗ ——呼应 《Nginx 安装与反向代理》 的"超时是安全带" ✓

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/MiniRpcLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.MiniRpcLesson

实测输出(关键部分):

text
===== 1. 本地调用 vs RPC 调用 =====
  本地调用:   0 微秒/次(0.000 ms)✓✓
  RPC 调用: 828 微秒/次(0.828 ms)

===== 2. 序列化格式对比 =====
  ① Java 原生序列化:149 字节
  ② JSON 文本:       58 字节(0.4 倍)
  ③ 手写二进制:      33 字节(0.2 倍)✓✓

===== 3. 自定义协议 vs HTTP =====
  HTTP/JSON(每次新建连接):27511 微秒/次(27.511 ms)✗

★ 三条可落地的结论 ✓:

#结论依据
①RPC 的固定成本约 0.8 ms(本机),跨机房会到毫秒~几十毫秒828 微秒 ✓
②序列化体积差 4.5 倍,高并发下要选紧凑格式149 vs 33 字节 ✓
③"每次新建连接"是最大的浪费(33 倍)27.5 ms vs 0.83 ms ✓✓

8. Debug 观察点 ​

  1. 把"本地调用"换成"真实业务方法":再测一次,看真实的纳秒级数据 ★★;
  2. 给 RPC 客户端换成"每次新建连接":观察耗时飙升 ✓✓;
  3. 把序列化改成 JSON 字符串:对比字节数与耗时 ✓;
  4. 在服务端反射前加缓存 Method:观察耗时下降 ✓;
  5. 用 tcpdump 或代理抓一次 RPC 流量:看协议长什么样(比 HTTP 紧凑得多)★★。

9. 常见错误 ​

#错误说明
1不配超时 ✗✗下游慢会拖死上游 ✓✓
2对非幂等接口开重试 ✗✗重复下单 ✓
3用 Java 原生序列化做对外协议 ✗✗体积大 + RCE 风险 ✓
4每次调用新建连接 ✗✗差 33 倍 ✓
5以为"HTTP 一定比 Dubbo 慢" ✗复用长连接后差距缩小 ✓
6以为"写完通信层就懂了 RPC" ✗✗难的是服务治理 ✓✓
7在 RPC 接口里暴露实体类而非 DTO ✗一改实体就两端不兼容 ✓

10. 动手实验 ​

目标:亲手把"方法调用"变成"网络请求"。

步骤:

  1. 跑 MiniRpcLesson,记录 RPC 的固定成本 ✓;
  2. 给客户端加"连接池"(若已复用则改成"每次新建"再对比),看差距 ★★;
  3. 把序列化换成 JSON,对比体积与耗时 ✓;
  4. 给服务端加上"方法名 → Method 缓存",看反射开销占比 ★;
  5. 思考:你的项目里哪些接口"远程调用次数过多",能合并吗?✓✓

验收方式:能说出"你的 RPC 调用链上有几跳、每跳多少毫秒"。

扩展任务:给最小 RPC 加上两个服务治理能力:

  • 超时(客户端等待超过 N ms 就抛异常)✓;
  • 重试(超时后换一个服务实例重试,且只对幂等接口开)✓;
  • 说明:为什么"重试"在高并发下要配合"退避"与"熔断"(《Sentinel 限流与熔断》)★★

11. 练习题 ​

  1. 判断:RPC 调用和本地调用耗时差不多。(对 / 错)
  2. 填空:一次 RPC 的五个环节是 、、、、______。
  3. 简答:为什么服务端要用"反射调用"而不是为每个方法写 if-else?
  4. (配置阅读)
java
@DubboReference
private UserService userService;

@DubboService
public class OrderServiceImpl implements OrderService { ... }

这两处各缺了什么关键配置? 5. (设计)给"下单"设计 RPC 调用方案:要扣库存、扣优惠券、加积分,要求不重复扣减。

参考答案(要点)
  1. 错。RPC 有约 0.8 ms 的固定成本(本机),跨机房更高 ✗ —— 而本地调用是纳秒级 ✓★
  2. 动态代理拦截 → 序列化 → 网络传输 → 反序列化 → 反射调用 ✓
  3. 因为 RPC 框架要"通用" ✓ ——服务端事先不知道会有哪些接口, 反射让"新增方法不用改框架代码" ✓✓(这就是接口+实现分离的价值)
  4. @DubboReference 缺 timeout(默认 1000 ms 可能不符合业务) ✗; @DubboService 缺 retries=0(写接口默认重试 2 次,会导致重复下单) ✗✗ ★
  5. 要点:每个扣减接口都要幂等(用业务唯一键 + 唯一索引,《消息可靠性、幂等与顺序》)✓; 写接口 retries=0 ✗;非核心的加积分可异步化(MQ)✓; 三个调用能并行就并行(缩短链路延迟,《微服务架构与拆分原则》)✓✓

12. 掌握标准 ​

  • [ ] 能画出一次 RPC 的完整链路 ★★★;
  • [ ] 知道远程调用贵在四件事上,且"连接复用"收益最大 ★★★;
  • [ ] 能对比四种序列化格式并选型 ★★★;
  • [ ] 知道不要用 Java 原生序列化做对外协议(含 RCE 风险) ★★★;
  • [ ] 能说出 Dubbo / REST / gRPC 的取舍 ★★★;
  • [ ] 知道四个必配的客户端参数(尤其超时与重试) ★★★;
  • [ ] 知道"通信只是 RPC 的一小部分,服务治理才是大头" ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《微服务架构与拆分原则》:拆分的收益大家都知道,但代价很少被量化。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引