主题
界面
主题
《Netty 与网络编程概览》讲网络编程;本节讲**"微服务的通信方式"**:
RPC 的全部秘密只有一句话:
把「方法调用」编码成「网络请求」,收到响应再解码成返回值 ✓★ 本节的实测把"远程调用贵在哪"量出来 ✓✓:
本地调用: 0 微秒/次(JIT 把空方法优化掉了 —— 见下方说明)
RPC 调用: 828 微秒/次(0.828 ms)
HTTP/JSON(每次新建连接):27511 微秒/次(27.511 ms)✗✗先看本节的序列化实测 ✓✓:
① Java 原生序列化:149 字节 ← 带类名与方法签名,最臃肿 ✗✗
② JSON 文本: 58 字节(0.4 倍)
③ 手写二进制: 33 字节(0.2 倍)✓✓★ 同一个用户对象,编码体积差 4.5 倍 ✓ ——在高并发下这就是带宽与 GC 的差距 ✓✓
★ 本节的 200 行实现就是这条链路 ✓:
客户端 服务端
│
├─ ① 动态代理拦截方法调用(UserService.findName(id))
├─ ② 序列化:方法名 + 参数 → 字节流
├─ ③ 网络传输:长度字段 + 字节流(解决粘包)
│ ───────────────── 网络 ─────────────────→
│ ④ 反序列化 → 方法名 + 参数
│ ⑤ 反射调用真实实现
│ ⑥ 序列化返回值
│ ←─────────────── 网络 ──────────────────
├─ ⑦ 反序列化响应
└─ ⑧ 返回给调用方(看起来就像本地方法)★ 关键设计 ✓✓:
| 环节 | 用什么实现 | 为什么 |
|---|---|---|
| ① 拦截调用 ★★★ | 动态代理 | 调用方只依赖接口(《设计模式:结构型》)✓✓ |
| ② 编解码 ★★ | 自定义二进制协议 | 比 JSON 小、比 Java 序列化跨语言 ✓ |
| ③ 传输 ★★ | 长度字段 | 解决粘包(《TCP 与 UDP》)✓✓ |
| ⑤ 服务端通用化 ★★★ | 反射调用 | 不用为每个方法写 if-else ✓✓ |
实测 ✓:
本地调用: 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 次)
★ 实测与对照 ✓:
| 格式 | 体积 | 跨语言 | 代表 |
|---|---|---|---|
| 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 ✓
实测(用 HTTP 每次新建连接做对照)✓:
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 的真正优势是"为高频小请求设计的协议 + 内置服务治理" ✓✓
★ 本实现与 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(不限) | 限制并发 ——防止单个接口把线程池打满 ✓ |
// 客户端只认识这个接口(与 S4-004 的 DIP 一致)
UserService userService = RpcClient.create(UserService.class, "127.0.0.1", 20880);
String name = userService.findName(9527L); // 看起来就是本地调用 ✓// 服务端
@DubboService(timeout = 2000, retries = 0) // ★ 写接口 retries=0
public class UserServiceImpl implements UserService { ... }
// 消费端
@DubboReference(timeout = 1000, cluster = "failfast")
private UserService userService;// ✗ 下游卡住 30 秒,上游线程被占住 30 秒 → 上游线程池被打满 → 雪崩
orderService.create(request);★ 这是"雪崩"最常见的起点 ✗✗ ——呼应 《Nginx 安装与反向代理》 的"超时是安全带" ✓
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/MiniRpcLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.MiniRpcLesson实测输出(关键部分):
===== 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 ✓✓ |
Method:观察耗时下降 ✓;tcpdump 或代理抓一次 RPC 流量:看协议长什么样(比 HTTP 紧凑得多)★★。| # | 错误 | 说明 |
|---|---|---|
| 1 | 不配超时 ✗✗ | 下游慢会拖死上游 ✓✓ |
| 2 | 对非幂等接口开重试 ✗✗ | 重复下单 ✓ |
| 3 | 用 Java 原生序列化做对外协议 ✗✗ | 体积大 + RCE 风险 ✓ |
| 4 | 每次调用新建连接 ✗✗ | 差 33 倍 ✓ |
| 5 | 以为"HTTP 一定比 Dubbo 慢" ✗ | 复用长连接后差距缩小 ✓ |
| 6 | 以为"写完通信层就懂了 RPC" ✗✗ | 难的是服务治理 ✓✓ |
| 7 | 在 RPC 接口里暴露实体类而非 DTO ✗ | 一改实体就两端不兼容 ✓ |
目标:亲手把"方法调用"变成"网络请求"。
步骤:
MiniRpcLesson,记录 RPC 的固定成本 ✓;验收方式:能说出"你的 RPC 调用链上有几跳、每跳多少毫秒"。
扩展任务:给最小 RPC 加上两个服务治理能力:
@DubboReference
private UserService userService;
@DubboService
public class OrderServiceImpl implements OrderService { ... }这两处各缺了什么关键配置? 5. (设计)给"下单"设计 RPC 调用方案:要扣库存、扣优惠券、加积分,要求不重复扣减。
@DubboReference 缺 timeout(默认 1000 ms 可能不符合业务) ✗; @DubboService 缺 retries=0(写接口默认重试 2 次,会导致重复下单) ✗✗ ★retries=0 ✗;非核心的加积分可异步化(MQ)✓; 三个调用能并行就并行(缩短链路延迟,《微服务架构与拆分原则》)✓✓《微服务架构与拆分原则》:拆分的收益大家都知道,但代价很少被量化。
暂无