RPC 的系统化认知:本地黑盒隐喻 + 网络透明化 + 工业三大件
RPC 的终极使命:让你在调用"部署在远端服务器上的方法"时,体验就像在调用自己本地内存里的方法一样简单。
第一层:确立"本地黑盒"隐喻(RPC 的核心动机)
在单体应用时代,所有代码都在同一个 JVM 内存里,调用一个方法只需几纳秒:
User user = userService.getUserById(123L);
但微服务架构出现后,你的项目被拆分成独立运行在不同服务器上的服务。订单服务的 JVM 内存里根本没有 userService 这个对象——它跑在几百公里外的另一台服务器上。
不用 RPC 的硬编码写法:手动拼 URL、发 HTTP 请求、等响应、反序列化 JSON——臃肿且容易出错。
RPC 的救场:RPC 框架在内存中生成了一个傀儡替身(代理对象 Proxy),让你可以继续像写单体应用一样,一行代码呼叫远端方法。你以为你在调本地,其实傀儡替身在幕后悄悄把请求通过网络发给了远端。
第二层:拆解"跨海大桥的八个步骤"(RPC 的底层执行流)
Client A Server B
┌────────────┐ ┌────────────┐
│ 业务代码 │ │ 真实实现 │
│ userService│ │ getUserById│
│ .getUser │ │ (查数据库) │
│ ById(123L) │ └─────┬──────┘
└─────┬──────┘ │
│ ① 本地调用 │
▼ │
┌────────────┐ │
│ Client │ │
│ Stub │ ② 序列化 │
│ (代理对象) │ ──字节流──→ ③ 网络传输 │
└────────────┘ │
▼
┌────────────┐
│ Server │
│ Stub │ ←── ④ 接收 + ⑤ 反序列化
└────────────┘
│ ⑥ 调用真实方法
▼
┌────────┐
│ 数据库 │
└────────┘
⑦ 结果原路返回(序列化 → 网络 → 反序列化)
⑧ Client Stub 把结果吐给业务代码
- Client:像平常一样调用
userService.getUserById(123L) - Client Stub(客户端存根/代理):傀儡替身截获调用,序列化方法名和参数为二进制字节流
- Transport(网络传输):通过 TCP 或 HTTP/2 把字节流发送给远端
- Server Transport:远端服务器网络层接收字节流
- Server Stub(服务端存根):反序列化字节流,还原方法名和参数
- Server:真实执行本地方法(查数据库),拿到结果
- 数据返回:按反向流程(序列化 → 网络 → 反序列化)把结果送回客户端
- 结果呈现:傀儡替身把结果吐给业务代码,你全程毫无感知
第三层:降维击破"工业三大件"(RPC 的核心技术栈)
工业级 RPC 框架(Dubbo、gRPC)的所有设计都围绕以下三个核心:
1. 动态代理——如何生成傀儡替身?
底层用的就是反射与动态代理。框架在启动时自动扫描你的接口,利用反射在内存中动态生成代理类。你调用的所有方法,都会被路由到代理类的 invoke() 方法里去触发网络请求。
2. 序列化协议——如何把数据打碎包装?
| 类型 | 代表 | 特点 |
|---|---|---|
| 文本序列化 | JSON、XML | 可读性好,但体积大,大量无用符号,解析慢 |
| 二进制序列化 | Protobuf(gRPC标配)、Hessian、Kryo | 体积极小,速度快一个数量级 |
3. 网络通信模型——如何高并发?
传统的同步阻塞 I/O(BIO)为每个请求开一个线程,线程一多内存直接爆掉。工业级 RPC 底层无一例外采用 NIO(非阻塞 I/O) 模型,通常基于 Netty 框架构建,实现单台服务器极少线程同时处理数十万并发。
第四层:RPC vs HTTP/REST(工业选型对比)
| 维度 | 普通 HTTP (RESTful) | 工业级 RPC (Dubbo / gRPC) |
|---|---|---|
| 视角 | 面向资源。URL 对应网络资源(/users/123),强调标准化协议 |
面向动作。直奔主题调用远端方法(getUserById()),强调方法执行 |
| 序列化 | JSON/XML 文本格式,体积大、解析耗 CPU | Protobuf 等二进制格式,体积小、速度快 |
| 传输层 | HTTP/1.1,每次请求可能重新建连,Header 臃肿 | TCP 或 HTTP/2,长连接复用,一次建连多次复用 |
| 适用场景 | 对外接口。给前端或第三方提供 API,通用标准 | 内部通信。微服务网关之后的高频调用,追求极致性能 |
实战检测:RPC 调用的容错设计
场景:订单服务通过 RPC 调用库存服务的 deductStock(Long skuId, int count)。某天库存服务的服务器突然宕机。
Q1:这行 RPC 代码会发生什么?
网络层要么超时(TCP 连接迟迟没有响应),要么连接被拒绝(Connection refused)。RPC 框架会抛出一个继承自 RuntimeException 的异常——Dubbo 抛 RpcException,gRPC 抛 StatusRuntimeException。
按照异常专题的血缘认知:这是一个非受检异常,不会强迫你改方法签名,但如果不处理,它会沿调用栈一路往上飙,最终到达全局异常处理器或直接导致当前请求失败。
Q2:两个经典的微服务容错防御机制
机制一:超时控制(Timeout)
给 RPC 调用设置一个超时阈值(如 500ms)。超过这个时间还没拿到响应,直接抛超时异常,不要让线程无限等待下去。如果没有超时控制,一个下游故障会把整个线程池的线程全部卡死,引发雪崩效应。
机制二:熔断器 + 降级(Circuit Breaker + Fallback)
熔断器有三个状态:Closed(关闭)→ Open(打开)→ Half-Open(半开)。
正常运转 (Closed)
│ 失败次数超过阈值(如 10 秒内 50 次失败)
▼
熔断打开 (Open)
│ 后续请求不再真实发送,直接走降级逻辑
│ 等待一段冷静时间
▼
半开试探 (Half-Open)
│ 放一个试探请求过去
├─ 成功 → 关闭熔断器,恢复正常
└─ 失败 → 继续保持打开状态
降级逻辑示例:
// 库存服务挂掉时,直接返回兜底数据,不让用户干等
RpcContext rpcContext = RpcContext.getContext();
try {
return stockService.deductStock(skuId, count);
} catch (RpcException e) {
// 熔断或超时后的降级兜底
log.error("库存服务异常,执行降级: skuId={}", skuId, e);
return false; // 扣减失败,走后续的库存不足流程
}
为什么这两个机制不可或缺?
没有超时控制,一个故障会把整个线程池耗死。没有熔断器,所有请求都会持续涌向已宕机的服务,既浪费资源又无法恢复。两者配合,才能实现微服务架构中的快速失败(Fail Fast)和优雅降级(Graceful Degradation)。
🏁 你建立起的 RPC 系统化认知
- 核心动机:让远程调用像本地调用一样自然,让网络对程序员透明
- 八大步骤:本地调用 → 序列化 → 网络传输 → 接收 → 反序列化 → 真实执行 → 结果返回 → 呈现
- 三大件:动态代理 / 二进制序列化 / NIO+Netty
- RPC vs REST:对外用 HTTP/REST(通用标准),对内用 RPC(极致性能)
- 容错双保险:超时控制 + 熔断降级,防止单点故障雪崩扩散
评论
发表评论