RPC(Remote Procedure Call,远程过程调用)的目标只有一个:让调用远程服务像调用本地方法一样自然,业务代码只面对接口,序列化、网络传输、寻址、容错全部交给框架。本文拆解一次调用的完整链路,并横向对比 Dubbo、gRPC、OpenFeign 三种主流方案。

RPC 的本质

本地调用是同一进程内的方法跳转:参数在栈上传递,失败就是抛异常。远程调用要跨过进程边界:参数得变成字节流、字节流得走网络、对端的方法得先被找到,连失败的语义都变得模糊。RPC 框架做的事,是用动态代理在调用方生成一个”长得像本地接口”的 Stub,把这些差异全部挡在代理背后。

它解决的不是”能不能通信”——裸 HTTP 客户端也能通——而是把通信细节标准化、可治理化,让业务代码不需要感知网络的存在。

一次调用的完整链路

调用方                                     服务方
接口方法调用
→ 动态代理(Stub 拦截方法调用)
→ 服务发现:从注册中心取可用地址列表
→ 负载均衡:选定一个实例
→ 序列化:参数对象 → 字节流
→ 网络传输:长连接复用、IO 线程
───────── TCP ─────────>
→ 反序列化还原请求
→ 线程池分发到业务实现
→ 执行并序列化结果
<───────── TCP ─────────
← 反序列化为返回值或异常

链路上四个环节缺一不可:

  • 动态代理(JDK 动态代理、ByteBuddy、Javassist)把方法签名和参数打包成调用描述对象,业务无感知,同时给框架留出插入治理逻辑的切面;
  • 序列化决定跨语言能力与包体大小:Java 原生序列化只限 JVM 且体积大,Hessian2、Kryo 快但偏 Java 体系,Protobuf 跨语言且紧凑,代价是要维护 IDL;
  • 网络传输普遍基于 Netty 做异步 NIO,长连接复用避免反复建连的开销;
  • 服务发现让地址不再写死:Provider 注册、Consumer 订阅,实例上下线由注册中心实时推送,负载均衡在列表中挑选实例。

超时、重试、熔断这类治理策略统一挂在调用链路上,而不是散落在每个业务方法里——这正是框架相对裸 HTTP 的核心价值。

主流框架对比

维度 Dubbo gRPC Spring Cloud OpenFeign
传输协议 私有 TCP(Dubbo2)/Triple(HTTP/2) HTTP/2 HTTP/1.1
契约形式 Java 接口 Protobuf IDL Java 接口注解
跨语言能力 弱,Triple 在改善 强,官方多语言 弱,绑定 JVM 体系
治理能力 内置注册发现、路由、限流熔断、监控 核心精简,治理依赖 xDS/服务网格 依赖 Spring Cloud 组件拼装
典型场景 国内 Java 微服务内部调用 多语言、移动端、需穿透网关 Spring Cloud 体系内声明式 HTTP 调用

选型逻辑很直接:纯 Java 且要求治理开箱即用选 Dubbo;多语言栈或流量要走网关选 gRPC;已经在 Spring Cloud 生态里、性能要求不极端,OpenFeign 最省心。

工程实践与常见误区

  • 必须显式设置超时,框架默认值通常和你的下游 P99 对不上;
  • 重试只给幂等接口,非幂等接口重试会在故障时放大事故;
  • 接口演进保持向后兼容:只加字段、不改语义、不删方法,否则序列化层会替你把事故变成 ClassNotFound;
  • 别迷信”RPC 一定比 HTTP 快”:gRPC 就跑在 HTTP/2 上,差距主要在序列化效率、连接管理和治理成本,不在协议名字。

面试视角

为什么 RPC 框架普遍用动态代理? 因为入参打包、网络收发、结果解析都是模板代码,代理把它们收进 Stub,业务才能像调本地方法一样调远程服务,治理逻辑也有了统一的插入点。

RPC 和 HTTP 是什么关系? 两者不在同一层:RPC 是调用模型,HTTP 是传输协议。RPC 可以跑在私有 TCP 上,也可以跑在 HTTP/2 上(gRPC 就是例子);反过来,手写 HTTP 客户端调接口只是在用 HTTP 协议裸做远程调用,缺契约也缺治理。

小结

  • RPC 的本质是用代理屏蔽远程细节,链路核心四件套:动态代理、序列化、网络传输、服务发现。
  • 框架选型看语言栈与治理诉求:Dubbo 重治理、gRPC 重跨语言、OpenFeign 重 Spring 生态整合。
  • 超时显式化、重试幂等化、接口向后兼容,是 RPC 生产化的三条及格线。