rpc
Remote Procedure Call 远程方法调用,通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想
如果只看调用姿势,RPC 希望做到像调用本地方法一样调用远程服务。但实际上一次 RPC 调用背后至少包含:代理、编解码、网络传输、服务发现、负载均衡、超时重试、熔断降级等一整套链路。
基本调用流程
- 客户端通过动态代理拿到一个接口代理对象。
- 代理对象把方法名、参数类型、参数值等信息封装成请求。
- 请求经过序列化后,通过网络发送到服务端。
- 服务端反序列化请求,根据服务名和方法名找到本地实现。
- 服务端执行本地方法,把结果或异常再序列化返回。
- 客户端收到响应后反序列化,最终返回给业务代码。
传输协议
传输协议决定数据怎么在网络上传输,常见选择有:
- TCP:性能稳定、连接可复用,适合自定义二进制协议,也是很多 RPC 框架的基础选择。
- HTTP/1.1:可读性和通用性好,调试方便,但文本协议和头部开销会更明显。
- HTTP/2:支持多路复用、头部压缩、流式传输,gRPC 就基于 HTTP/2。
- WebSocket:适合需要长连接、双向通信的场景,但在普通 RPC 里不是最主流选择。
如果是内部高性能调用,可以优先考虑 TCP 或 HTTP/2;如果更看重生态、网关、调试和跨语言接入,HTTP/2、HTTP/JSON 会更舒服。
IO 网络模型
RPC 框架的吞吐量和延迟,和 IO 模型关系很大。
- BIO:一个连接一个线程,模型简单,但连接数上来后线程开销大。
- NIO:非阻塞 IO,通过 selector 管理多个连接,适合高并发场景。
- AIO:异步 IO,由操作系统完成后通知应用,但 Java 生态里实际使用没有 NIO 普遍。
Java RPC 框架里常见的是基于 Netty 的 Reactor 模型:少量线程负责连接、读写事件和编解码,再把业务执行丢给独立线程池,避免 IO 线程被业务逻辑阻塞。
协议设计姿势
自定义 RPC 协议通常会把消息拆成 header 和 body:
- 魔数:快速判断是不是当前协议,避免收到脏数据。
- 版本号:协议升级时做兼容。
- 序列化类型:标识 body 使用 JSON、Hessian、Protobuf 等哪种格式。
- 消息类型:区分请求、响应、心跳、异常等。
- 请求 ID:用于异步调用时把响应和请求对应起来。
- 数据长度:解决 TCP 粘包、半包问题。
- Body:真正的方法调用参数或返回结果。
简单示意:
magic | version | serializer | messageType | requestId | bodyLength | body
协议设计要尽量稳定,字段要能扩展,长度要明确,异常要可表达,最好还要支持心跳和连接空闲检测。
序列化与反序列化
序列化负责把对象转成网络可传输的字节,反序列化则负责把字节还原成对象。技术选型可以从性能、体积、可读性、跨语言、兼容性几个角度看。
| 技术 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| JSON | 可读性好,调试方便,生态成熟 | 体积偏大,性能一般,类型表达较弱 | 对外接口、调试友好场景 |
| Hessian | 使用简单,体积比 JSON 小 | 跨语言生态一般 | Java 内部服务调用 |
| Protobuf | 性能好、体积小、跨语言强 | 需要维护 IDL,字段变更要遵守兼容规则 | 多语言、高性能 RPC |
| Kryo | 性能较好,Java 对象支持丰富 | 跨语言弱,版本兼容要小心 | Java 内部高性能调用 |
如果是公司内部 Java 服务,可以选 Hessian、Kryo 这类上手快的方案;如果要跨语言或长期演进,Protobuf 这类 IDL 方案更稳。
服务治理层
RPC 不是只有一次请求响应,服务多起来后必须处理服务治理问题。
服务注册与发现
服务端启动后,把服务名、IP、端口、权重、版本等元数据注册到注册中心;客户端调用前从注册中心拉取可用服务列表,并缓存到本地。
常见注册中心有 ZooKeeper、Nacos、Consul、Etcd、Eureka 等。注册中心通常还会配合心跳和健康检查,把不可用节点及时摘掉。
负载均衡
当一个服务有多个提供者时,客户端需要选择其中一个节点发起调用。
- 随机:实现简单,适合节点差异不大的场景。
- 轮询:请求均匀分配,但不考虑机器性能差异。
- 加权随机/加权轮询:给配置更高的机器更多流量。
- 最少活跃调用:优先选择当前压力小的节点。
- 一致性 hash:同一类请求尽量打到同一台机器,适合有本地缓存的场景。
集群容错策略
远程调用一定会遇到超时、网络抖动、服务异常,所以需要容错策略。
- Failover:失败自动重试其他节点,适合读请求。
- Failfast:失败立刻报错,适合非幂等写请求。
- Failsafe:失败后吞掉异常,适合日志、监控这类旁路调用。
- Failback:失败后后台定时重试,适合消息通知。
- Forking:并行调多个节点,谁先返回用谁,适合对延迟极敏感但资源消耗更大的场景。
重试要特别注意幂等性,否则一次超时可能变成多次写入。
动态代理
业务代码通常只依赖接口,不直接写网络调用。RPC 框架会为接口生成代理对象,把本地方法调用转换成远程请求。
常见实现方式有:
- JDK 动态代理:基于接口生成代理,简单直接。
- CGLIB:通过继承类生成代理,适合没有接口的类。
- Javassist/ByteBuddy:通过字节码生成代理,性能和灵活性更强。
代理层一般会负责读取方法信息、组装请求、发起网络调用、等待响应、处理异常和超时。
主流 RPC 框架
| 框架 | 特点 |
|---|---|
| Dubbo | Java 生态常见,服务治理能力强,支持多协议、多注册中心 |
| gRPC | Google 开源,基于 HTTP/2 和 Protobuf,跨语言能力强 |
| Thrift | Facebook 开源,IDL 驱动,跨语言支持好 |
| Motan | 微博开源,轻量级 Java RPC 框架 |
| Spring Cloud OpenFeign | 更偏 HTTP 声明式调用,和 Spring Cloud 体系结合紧 |
简单总结:RPC 的重点不是“远程调用”这四个字,而是围绕远程调用把性能、稳定性、治理能力和研发体验都补齐。